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/// Resolve the consumer's local name for the `rustango` crate so the
18/// macro-emitted code keeps compiling when a downstream `Cargo.toml`
19/// renames the dep (`[dependencies] orm = { package = "rustango", … }`)
20/// or when the standalone `rustango-orm` crate ships in a future
21/// slice of the [orm-extract epic](https://github.com/ujeenet/rustango/issues/149).
22///
23/// Returns one of:
24/// - `quote!(::rustango)` — the consumer IS the `rustango` crate
25/// itself. We emit the absolute path `::rustango` (NOT `crate`)
26/// because examples and integration tests inside the rustango
27/// package compile as separate binaries — their own `crate::`
28/// namespace is the example/test file, not rustango's lib root.
29/// The absolute `::rustango` resolves correctly in both contexts
30/// (rustango lib code AND rustango/examples/*.rs).
31/// - `quote!(::ident)` — the consumer renamed the dep; emit the
32/// user's chosen ident.
33/// - `quote!(::rustango)` — fallback when `proc-macro-crate` can't
34/// read the manifest (rare; preserves today's behavior).
35///
36/// Issue [#142](https://github.com/ujeenet/rustango/issues/142).
37fn rustango_root() -> TokenStream2 {
38 use proc_macro_crate::{crate_name, FoundCrate};
39 match crate_name("rustango") {
40 Ok(FoundCrate::Itself) => quote!(::rustango),
41 Ok(FoundCrate::Name(name)) => {
42 let ident = proc_macro2::Ident::new(&name, proc_macro2::Span::call_site());
43 quote!(::#ident)
44 }
45 Err(_) => quote!(::rustango),
46 }
47}
48
49/// Derive a `Model` impl. See crate docs for the supported attributes.
50#[proc_macro_derive(Model, attributes(rustango))]
51pub fn derive_model(input: TokenStream) -> TokenStream {
52 let input = parse_macro_input!(input as DeriveInput);
53 expand(&input)
54 .unwrap_or_else(syn::Error::into_compile_error)
55 .into()
56}
57
58/// Derive a `router(prefix, pool) -> axum::Router` associated method on a
59/// marker struct, wiring the full CRUD ViewSet in one annotation.
60///
61/// ```ignore
62/// #[derive(ViewSet)]
63/// #[viewset(
64/// model = Post,
65/// fields = "id, title, body, author_id",
66/// filter_fields = "author_id",
67/// search_fields = "title, body",
68/// ordering = "-published_at",
69/// page_size = 20,
70/// )]
71/// pub struct PostViewSet;
72///
73/// // Mount into your app:
74/// let app = Router::new()
75/// .merge(PostViewSet::router("/api/posts", pool.clone()));
76/// ```
77///
78/// Attributes:
79/// * `model = TypeName` — *required*. The `#[derive(Model)]` struct whose
80/// `SCHEMA` constant drives the endpoints.
81/// * `fields = "a, b, c"` — scalar fields included in list/retrieve JSON
82/// and accepted on create/update (default: all scalar fields).
83/// * `filter_fields = "a, b"` — fields filterable via `?a=v` query params.
84/// * `search_fields = "a, b"` — fields searched by `?search=...`.
85/// * `ordering = "a, -b"` — default list ordering; prefix `-` for DESC.
86/// * `page_size = N` — default page size (default: 20, max: 1000).
87/// * `read_only` — flag; wires only `list` + `retrieve` (no mutations).
88/// * `serializer = SomeSerializer` — render list / retrieve / create
89/// responses through a `#[derive(Serializer)]` type instead of the
90/// default field-level projection (`read_only` / `source` / `method`
91/// / `write_only` overrides apply). Tri-dialect. Requires the
92/// `serializer` feature.
93/// * `permissions(list = "...", retrieve = "...", create = "...",
94/// update = "...", destroy = "...")` — codenames required per action.
95#[proc_macro_derive(ViewSet, attributes(viewset))]
96pub fn derive_viewset(input: TokenStream) -> TokenStream {
97 let input = parse_macro_input!(input as DeriveInput);
98 expand_viewset(&input)
99 .unwrap_or_else(syn::Error::into_compile_error)
100 .into()
101}
102
103/// Derive `rustango::forms::Form` (slice 8.4B). Generates a
104/// `parse(&HashMap<String, String>) -> Result<Self, FormErrors>` impl
105/// that walks every named field and:
106///
107/// * Parses the string value into the field's Rust type (`String`,
108/// `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
109/// nullable case).
110/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
111/// `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
112/// validators in declaration order, returning `FormError::Parse`
113/// on the first failure.
114///
115/// Example:
116///
117/// ```ignore
118/// #[derive(Form)]
119/// pub struct CreateItemForm {
120/// #[form(min_length = 1, max_length = 64)]
121/// pub name: String,
122/// #[form(min = 0, max = 150)]
123/// pub age: i32,
124/// pub active: bool,
125/// pub email: Option<String>,
126/// }
127///
128/// let parsed = CreateItemForm::parse(&form_map)?;
129/// ```
130#[proc_macro_derive(Form, attributes(form))]
131pub fn derive_form(input: TokenStream) -> TokenStream {
132 let input = parse_macro_input!(input as DeriveInput);
133 expand_form(&input)
134 .unwrap_or_else(syn::Error::into_compile_error)
135 .into()
136}
137
138/// Derive `rustango::serializer::ModelSerializer` for a struct.
139/// (intra-doc link disabled — the macro crate doesn't depend on
140/// `rustango` itself, so rustdoc can't resolve the path.)
141///
142/// # Container attribute (required)
143/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
144///
145/// # Field attributes
146/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
147/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
148/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
149/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
150/// - `#[serializer(method = "fn_name")]` — computed field: calls `Self::fn_name(&model)` for the field value; excluded from `writable_fields()`
151/// - `#[serializer(nested)]` / `nested(strict)` — auto-resolves nested serializer from a loaded `ForeignKey`; excluded from `writable_fields()`
152/// - `#[serializer(many = ChildSerializer)]` — collection of nested serializers; populated via macro-emitted `set_<field>(&[Child::Model])`; excluded from `writable_fields()`
153/// - `#[serializer(slug = "name")]` — render a relation as one of its fields: clones `model.<source>.value()?.name`; excluded from `writable_fields()` (v0.44)
154/// - `#[serializer(validate = "fn_name")]` — per-field validator surfaced by `Self::validate(&self)`
155/// - `#[serializer(max_length = N)]` / `min_length` / `min` / `max` — declarative bounds checked on
156/// write; auto-inherit from the model's `FieldSchema` (`max_length`/`min`/`max`/`choices`) when
157/// no attr is given, override it when given (`min_length` is serializer-only)
158///
159/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
160#[proc_macro_derive(Serializer, attributes(serializer))]
161pub fn derive_serializer(input: TokenStream) -> TokenStream {
162 let input = parse_macro_input!(input as DeriveInput);
163 expand_serializer(&input)
164 .unwrap_or_else(syn::Error::into_compile_error)
165 .into()
166}
167
168/// Bake every `*.json` migration file in a directory into the binary
169/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
170/// of `(name, json_content)` pairs, lex-sorted by file stem.
171///
172/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
173/// behaviour as `migrate(pool, dir)` but with no filesystem access.
174/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
175/// (i.e. the crate that invokes the macro). Default is
176/// `"./migrations"` if no argument is supplied.
177///
178/// ```ignore
179/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
180/// // or:
181/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
182///
183/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
184/// ```
185///
186/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
187/// file's `name` field must equal its file stem, every `prev`
188/// reference must point to another migration in the same directory,
189/// and the JSON must parse. A broken chain — orphan `prev`, missing
190/// predecessor, malformed file — fails at macro-expansion time with
191/// a clear `compile_error!`. *No other Rust web framework
192/// validates migration chains at compile time*: Cot's migrations are
193/// imperative Rust code (no static chain), Loco's are SeaORM
194/// up/down (same), Rwf's are raw SQL (no chain at all).
195///
196/// Each migration is included via `include_str!` so cargo's rebuild
197/// detection picks up file *content* changes. **Caveat:** cargo
198/// doesn't watch directory listings, so adding or removing a
199/// migration file inside the dir won't auto-trigger a rebuild — run
200/// `cargo clean` (or just bump any other source file) when you add
201/// new migrations during embedded development.
202#[proc_macro]
203pub fn embed_migrations(input: TokenStream) -> TokenStream {
204 expand_embed_migrations(input.into())
205 .unwrap_or_else(syn::Error::into_compile_error)
206 .into()
207}
208
209/// `Q!()` — `Model.field__lookup = value` filter syntax, resolved at
210/// compile time against typed columns. Issue #269 / T1.7.
211///
212/// Each invocation lowers to the equivalent typed-column method call:
213///
214/// ```ignore
215/// // These expand identically:
216/// Q!(User.email__icontains = "alice")
217/// User::email.ilike("%alice%")
218/// ```
219///
220/// Field-name typos fail the build (the macro emits `User::no_such_field`
221/// which doesn't exist) — the headline ergonomic win of this slice over
222/// filters keyed by a plain string.
223///
224/// # Supported lookup suffixes
225///
226/// * bare `=` / `__exact` → `.eq(value)`
227/// * `__iexact` → `.ilike(value)` (case-insensitive equality, no wildcards)
228/// * `__ne` → `.ne(value)`
229/// * `__gt` / `__gte` / `__lt` / `__lte` → corresponding comparison
230/// * `__contains` / `__icontains` → `.like("%v%")` / `.ilike("%v%")`
231/// * `__startswith` / `__istartswith` → `.like("v%")` / `.ilike("v%")`
232/// * `__endswith` / `__iendswith` → `.like("%v")` / `.ilike("%v")`
233/// * `__in` → `.is_in(iterable)`
234/// * `__not_in` → `.not_in(iterable)`
235/// * `__isnull = true` → `.is_null()`; `__isnull = false` → `.is_not_null()`
236/// * `__between` accepts a tuple literal `(lo, hi)` → `.between(lo, hi)`
237/// * `__regex` / `__iregex` → `.regex(pattern)` / `.iregex(pattern)`
238///
239/// Unknown suffixes fail the build with a `compile_error!` pointing at
240/// the lookup token.
241///
242/// # Combine
243///
244/// Each `Q!()` returns a `TypedFilter<Model>` — chain via the existing
245/// `.and()` / `.or()` / `.not()` methods:
246///
247/// ```ignore
248/// User::objects()
249/// .where_(
250/// Q!(User.active = true)
251/// .and(Q!(User.email__icontains = "alice"))
252/// )
253/// .fetch(&pool).await?;
254/// ```
255///
256/// All emitted code routes through existing per-dialect writers — no new
257/// SQL emission machinery. Tri-dialect support is inherent.
258#[allow(non_snake_case)]
259#[proc_macro]
260pub fn Q(input: TokenStream) -> TokenStream {
261 expand_q(input.into())
262 .unwrap_or_else(syn::Error::into_compile_error)
263 .into()
264}
265
266/// `#[rustango::main]` — the runserver entrypoint. Wraps
267/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
268/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
269/// functions are zero-boilerplate:
270///
271/// ```ignore
272/// #[rustango::main]
273/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
274/// rustango::server::Builder::from_env().await?
275/// .migrate("migrations").await?
276/// .api(my_app::urls::api())
277/// .seed_with(my_app::seed::run).await?
278/// .serve("0.0.0.0:8080").await
279/// }
280/// ```
281///
282/// Optional `flavor = "current_thread"` passes through to
283/// `#[tokio::main]`; default is the multi-threaded runtime.
284///
285/// `logging = false` suppresses the default subscriber, for apps
286/// that install their own. Needed whenever the body reaches
287/// `Cli::with_logging()` or `logging::Setup`: the subscriber this
288/// macro installs gets there first, and `tracing` keeps the first
289/// one, so the `[logging]` section would otherwise be read and
290/// discarded (#1465).
291///
292/// ```ignore
293/// #[rustango::main(logging = false)]
294/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
295/// rustango::manage::Cli::new()
296/// .with_settings_from_env()
297/// .with_logging() // now the first installer
298/// .run().await
299/// }
300/// ```
301///
302/// Pulls `tracing-subscriber` into the rustango crate behind the
303/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
304/// out get plain `#[tokio::main]` ergonomics without the dependency.
305#[proc_macro_attribute]
306pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
307 expand_main(args.into(), item.into())
308 .unwrap_or_else(syn::Error::into_compile_error)
309 .into()
310}
311
312fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
313 let mut input: syn::ItemFn = syn::parse2(item)?;
314 if input.sig.asyncness.is_none() {
315 return Err(syn::Error::new(
316 input.sig.ident.span(),
317 "`#[rustango::main]` must wrap an `async fn`",
318 ));
319 }
320
321 // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
322 // to `#[tokio::main]`. Tokio's proc-macro internally emits
323 // `::tokio::*` paths that resolve against the user crate's deps,
324 // so calling it through the rustango re-export still requires the
325 // user to add tokio to their own Cargo.toml. Building the
326 // runtime ourselves keeps the dep transitive through the
327 // `runtime` feature on rustango.
328 //
329 // Parse optional `flavor = "current_thread"` / `flavor =
330 // "multi_thread"` from the attribute args. Unknown args are
331 // tolerated (forward-compat with tokio's own arg surface).
332 let root = rustango_root();
333 let flavor = parse_flavor(&args);
334 let builder_call = match flavor {
335 Flavor::CurrentThread => quote! {
336 #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
337 },
338 Flavor::MultiThread => quote! {
339 #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
340 },
341 };
342
343 // Detach the user body and rewrite `main` as a sync fn that
344 // builds the runtime and blocks on the async body.
345 let user_body = input.block.clone();
346 input.sig.asyncness = None;
347
348 // `logging = false` drops the block entirely, so a later
349 // `Cli::with_logging()` is the first installer and its
350 // `[logging]` settings actually take effect (#1465).
351 let logging_prologue = if parse_logging(&args) {
352 quote! {
353 use #root::__private_runtime::tracing_subscriber::{self, EnvFilter};
354 // Colour only when stdout is a terminal, and never under
355 // `NO_COLOR` — `Color::Auto`'s rule, called rather than
356 // copied.
357 //
358 // This was inlined at first, on a comment claiming
359 // `rustango::logging` is gated on `admin` + `tenancy`. It is
360 // not: `pub mod logging` is ungated and its contents are
361 // `runtime`-gated — the same feature that gates this macro
362 // (`pub use rustango_macros::main` is `#[cfg(feature =
363 // "runtime")]`). So wherever this expands, `Color` is
364 // reachable, and the copy was justified by a gate that does
365 // not exist. `should_colour` carries a deliberate
366 // `NO_COLOR`-set-but-empty subtlety that the inline happened
367 // to match today and nothing kept matching tomorrow.
368 //
369 // Without any of it the subscriber inherits
370 // tracing-subscriber's default, `cfg!(feature = "ansi")` —
371 // true since the framework turned that feature on — so
372 // `./app > app.log` and every container redirecting stdout
373 // collected escape codes. `Setup::install` has its own
374 // check; this is the default entrypoint and had none.
375 let __ansi = #root::logging::Color::Auto.should_colour();
376 // `try_init` so duplicate installers (e.g. tests already
377 // holding a subscriber) don't panic.
378 let _ = tracing_subscriber::fmt()
379 .with_env_filter(
380 EnvFilter::try_from_default_env()
381 .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
382 )
383 .with_ansi(__ansi)
384 .try_init();
385 }
386 } else {
387 quote! {}
388 };
389
390 input.block = syn::parse2(quote! {{
391 { #logging_prologue }
392 let __rt = #builder_call
393 .enable_all()
394 .build()
395 .expect("failed to build tokio runtime");
396 __rt.block_on(async move #user_body)
397 }})?;
398
399 Ok(quote! {
400 #input
401 })
402}
403
404enum Flavor {
405 MultiThread,
406 CurrentThread,
407}
408
409fn parse_flavor(args: &TokenStream2) -> Flavor {
410 // Cheap parser: look for the literal token sequence
411 // `flavor = "current_thread"`. Everything else (including
412 // bare `multi_thread` or no args) defaults to multi-thread.
413 let s = args.to_string();
414 if s.contains("current_thread") {
415 Flavor::CurrentThread
416 } else {
417 Flavor::MultiThread
418 }
419}
420
421/// `false` when the attribute carries `logging = false` — the caller
422/// installs their own subscriber, usually via `Cli::with_logging`
423/// (#1465). Same cheap token match as `parse_flavor`.
424fn parse_logging(args: &TokenStream2) -> bool {
425 !args.to_string().replace(' ', "").contains("logging=false")
426}
427
428/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
429struct QInput {
430 base_path: syn::Path,
431 field: syn::Ident,
432 value: syn::Expr,
433}
434
435impl syn::parse::Parse for QInput {
436 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
437 let base_path: syn::Path = input.parse()?;
438 input.parse::<syn::Token![.]>()?;
439 let field: syn::Ident = input.parse()?;
440 input.parse::<syn::Token![=]>()?;
441 let value: syn::Expr = input.parse()?;
442 Ok(QInput {
443 base_path,
444 field,
445 value,
446 })
447 }
448}
449
450fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
451 let q: QInput = syn::parse2(input)?;
452 let root = rustango_root();
453 let field_str = q.field.to_string();
454 let field_span = q.field.span();
455 let (base, suffix) = match field_str.find("__") {
456 Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
457 None => (field_str.as_str(), ""),
458 };
459 if base.is_empty() {
460 return Err(syn::Error::new(
461 field_span,
462 "Q!(): field name is empty before `__` suffix",
463 ));
464 }
465 let base_ident = syn::Ident::new(base, field_span);
466 let value = &q.value;
467 let path = &q.base_path;
468
469 // Most suffixes map directly to a Column method with the value
470 // forwarded unchanged. Some need value-shape massaging (wildcards
471 // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
472 // ISNULL). Unknown suffixes fail the build.
473 let expanded = match suffix {
474 "" | "exact" => quote! {
475 #root::core::Column::eq(#path::#base_ident, #value)
476 },
477 "ne" => quote! {
478 #root::core::Column::ne(#path::#base_ident, #value)
479 },
480 "gt" => quote! {
481 #root::core::Column::gt(#path::#base_ident, #value)
482 },
483 "gte" => quote! {
484 #root::core::Column::gte(#path::#base_ident, #value)
485 },
486 "lt" => quote! {
487 #root::core::Column::lt(#path::#base_ident, #value)
488 },
489 "lte" => quote! {
490 #root::core::Column::lte(#path::#base_ident, #value)
491 },
492 // The escaped-LIKE lookups delegate to the typed Column
493 // methods, which escape the value and pair it with the
494 // *Escaped ops so `%` / `_` match literally on every dialect
495 // (#1257) — the macro no longer owns any pattern wrapping.
496 "iexact" => quote! {
497 #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
498 },
499 "contains" => quote! {
500 #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
501 },
502 "icontains" => quote! {
503 #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
504 },
505 "startswith" => quote! {
506 #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
507 },
508 "istartswith" => quote! {
509 #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
510 },
511 "endswith" => quote! {
512 #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
513 },
514 "iendswith" => quote! {
515 #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
516 },
517 "in" => quote! {
518 #root::core::Column::is_in(#path::#base_ident, #value)
519 },
520 "not_in" => quote! {
521 #root::core::Column::not_in(#path::#base_ident, #value)
522 },
523 "isnull" => {
524 // Must be a bool literal at macro time so we can route to
525 // is_null vs is_not_null without a runtime branch.
526 let b = match value {
527 syn::Expr::Lit(syn::ExprLit {
528 lit: syn::Lit::Bool(b),
529 ..
530 }) => b.value(),
531 _ => {
532 return Err(syn::Error::new_spanned(
533 value,
534 "Q!(): `__isnull` requires a `true` or `false` literal",
535 ));
536 }
537 };
538 if b {
539 quote! { #root::core::Column::is_null(#path::#base_ident) }
540 } else {
541 quote! { #root::core::Column::is_not_null(#path::#base_ident) }
542 }
543 }
544 "between" => {
545 // Accept a tuple literal `(lo, hi)`.
546 let tuple = match value {
547 syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
548 _ => {
549 return Err(syn::Error::new_spanned(
550 value,
551 "Q!(): `__between` requires a tuple literal `(lo, hi)`",
552 ));
553 }
554 };
555 let lo = &tuple.elems[0];
556 let hi = &tuple.elems[1];
557 quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
558 }
559 "regex" => quote! {
560 #root::core::Column::regex(#path::#base_ident, #value)
561 },
562 "iregex" => quote! {
563 #root::core::Column::iregex(#path::#base_ident, #value)
564 },
565 _ => {
566 return Err(syn::Error::new(
567 field_span,
568 format!(
569 "Q!(): unknown lookup suffix `__{}`. Supported: __exact / __iexact / __ne / __gt / __gte / __lt / __lte / __contains / __icontains / __startswith / __istartswith / __endswith / __iendswith / __in / __not_in / __isnull / __between / __regex / __iregex",
570 suffix
571 ),
572 ));
573 }
574 };
575 Ok(expanded)
576}
577
578fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
579 // Default to "./migrations" if invoked without args.
580 let path_str = if input.is_empty() {
581 "./migrations".to_string()
582 } else {
583 let lit: LitStr = syn::parse2(input)?;
584 lit.value()
585 };
586
587 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
588 syn::Error::new(
589 proc_macro2::Span::call_site(),
590 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
591 )
592 })?;
593 let abs = std::path::Path::new(&manifest).join(&path_str);
594
595 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
596 if abs.is_dir() {
597 let read = std::fs::read_dir(&abs).map_err(|e| {
598 syn::Error::new(
599 proc_macro2::Span::call_site(),
600 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
601 )
602 })?;
603 for entry in read.flatten() {
604 let path = entry.path();
605 if !path.is_file() {
606 continue;
607 }
608 if path.extension().and_then(|s| s.to_str()) != Some("json") {
609 continue;
610 }
611 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
612 continue;
613 };
614 entries.push((stem.to_owned(), path));
615 }
616 }
617 entries.sort_by(|a, b| a.0.cmp(&b.0));
618
619 // Compile-time chain validation: read each migration's JSON,
620 // pull `name` and `prev` (file-stem-keyed for the chain check),
621 // and verify every `prev` points to another migration in the
622 // slice. Mismatches between the file stem and the embedded
623 // `name` field — and broken `prev` chains — fail at MACRO
624 // EXPANSION time so a misshapen migration set never compiles.
625 //
626 // This is the v0.4 Slice 5 distinguisher: rustango's JSON
627 // migrations + a Rust proc-macro that reads them is a combo no
628 // other Rust web framework matches (Cot's are imperative Rust
629 // code, Loco's are SeaORM up/down, Rwf's are raw SQL — none have
630 // a static chain to validate).
631 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
632 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
633 for (stem, path) in &entries {
634 let raw = std::fs::read_to_string(path).map_err(|e| {
635 syn::Error::new(
636 proc_macro2::Span::call_site(),
637 format!(
638 "embed_migrations!: cannot read {} for chain validation: {e}",
639 path.display()
640 ),
641 )
642 })?;
643 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
644 syn::Error::new(
645 proc_macro2::Span::call_site(),
646 format!(
647 "embed_migrations!: {} is not valid JSON: {e}",
648 path.display()
649 ),
650 )
651 })?;
652 let name = json
653 .get("name")
654 .and_then(|v| v.as_str())
655 .ok_or_else(|| {
656 syn::Error::new(
657 proc_macro2::Span::call_site(),
658 format!(
659 "embed_migrations!: {} is missing the `name` field",
660 path.display()
661 ),
662 )
663 })?
664 .to_owned();
665 if name != *stem {
666 return Err(syn::Error::new(
667 proc_macro2::Span::call_site(),
668 format!(
669 "embed_migrations!: file stem `{stem}` does not match the migration's \
670 `name` field `{name}` — rename the file or fix the JSON",
671 ),
672 ));
673 }
674 let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
675 chain_names.push(name.clone());
676 prev_refs.push((name, prev));
677 }
678
679 let name_set: std::collections::HashSet<&str> =
680 chain_names.iter().map(String::as_str).collect();
681 for (name, prev) in &prev_refs {
682 if let Some(p) = prev {
683 if !name_set.contains(p.as_str()) {
684 return Err(syn::Error::new(
685 proc_macro2::Span::call_site(),
686 format!(
687 "embed_migrations!: broken migration chain — `{name}` declares \
688 prev=`{p}` but no migration with that name exists in {}",
689 abs.display()
690 ),
691 ));
692 }
693 }
694 }
695
696 let pairs: Vec<TokenStream2> = entries
697 .iter()
698 .map(|(name, path)| {
699 let path_lit = path.display().to_string();
700 quote! { (#name, ::core::include_str!(#path_lit)) }
701 })
702 .collect();
703
704 Ok(quote! {
705 {
706 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
707 __RUSTANGO_EMBEDDED
708 }
709 })
710}
711
712fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
713 let root = rustango_root();
714 let struct_name = &input.ident;
715
716 let Data::Struct(data) = &input.data else {
717 return Err(syn::Error::new_spanned(
718 struct_name,
719 "Model can only be derived on structs",
720 ));
721 };
722 let Fields::Named(named) = &data.fields else {
723 return Err(syn::Error::new_spanned(
724 struct_name,
725 "Model requires a struct with named fields",
726 ));
727 };
728
729 let container = parse_container_attrs(input)?;
730 let table = container
731 .table
732 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
733 let model_name = struct_name.to_string();
734
735 let collected = collect_fields(named, &table)?;
736
737 // Validate that #[rustango(display = "…")] names a real field.
738 if let Some((ref display, span)) = container.display {
739 if !collected.field_names.iter().any(|n| n == display) {
740 return Err(syn::Error::new(
741 span,
742 format!("`display = \"{display}\"` does not match any field on this struct"),
743 ));
744 }
745 }
746 let display = container.display.map(|(name, _)| name);
747 let app_label = container.app.clone();
748
749 // Validate admin field-name lists against declared field names.
750 // Note: `list_display` is intentionally NOT validated here. As of
751 // v0.32 it may also reference inventory-registered computed
752 // fields (via `register_admin_computed!`) whose existence the
753 // macro can't see at compile time — they're submitted from any
754 // crate that depends on rustango. The runtime list-view resolves
755 // unknown names against the inventory + silently drops the
756 // truly-bogus ones, which is the cheaper trade-off versus
757 // forcing a per-Model attr to opt out.
758 if let Some(admin) = &container.admin {
759 for (label, list) in [
760 ("search_fields", &admin.search_fields),
761 ("readonly_fields", &admin.readonly_fields),
762 ("list_filter", &admin.list_filter),
763 ] {
764 if let Some((names, span)) = list {
765 for name in names {
766 if !collected.field_names.iter().any(|n| n == name) {
767 return Err(syn::Error::new(
768 *span,
769 format!(
770 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
771 ),
772 ));
773 }
774 }
775 }
776 }
777 if let Some((pairs, span)) = &admin.ordering {
778 for (name, _) in pairs {
779 if !collected.field_names.iter().any(|n| n == name) {
780 return Err(syn::Error::new(
781 *span,
782 format!(
783 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
784 ),
785 ));
786 }
787 }
788 }
789 if let Some((groups, span)) = &admin.fieldsets {
790 for (_, fields) in groups {
791 for name in fields {
792 if !collected.field_names.iter().any(|n| n == name) {
793 return Err(syn::Error::new(
794 *span,
795 format!(
796 "`fieldsets`: \"{name}\" is not a declared field on this struct"
797 ),
798 ));
799 }
800 }
801 }
802 }
803 }
804 if let Some(audit) = &container.audit {
805 if let Some((names, span)) = &audit.track {
806 for name in names {
807 if !collected.field_names.iter().any(|n| n == name) {
808 return Err(syn::Error::new(
809 *span,
810 format!(
811 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
812 ),
813 ));
814 }
815 }
816 }
817 }
818
819 // Issue #291 / T2.5 — validate each `default_order` column name
820 // against the model's collected fields. Typos fail at macro-expand
821 // time, not at the database.
822 for (col, _desc, span) in &container.default_order {
823 if !collected.field_names.iter().any(|n| n == col) {
824 return Err(syn::Error::new(
825 *span,
826 format!(
827 "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
828 ),
829 ));
830 }
831 }
832
833 // Build the audit_track list for ModelSchema: None when no audit attr,
834 // Some(empty) when audit present without track, Some(names) when explicit.
835 let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
836 audit
837 .track
838 .as_ref()
839 .map(|(names, _)| names.clone())
840 .unwrap_or_default()
841 });
842
843 // Merge field-level indexes into the container's index list.
844 let mut all_indexes: Vec<IndexAttr> = container.indexes;
845 for field in &named.named {
846 let ident = field.ident.as_ref().expect("named");
847 let col = to_snake_case(&ident.to_string()); // column name fallback
848 // Re-parse field attrs to check for index flag
849 if let Ok(fa) = parse_field_attrs(field) {
850 if fa.index {
851 let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
852 let auto_name = if fa.index_unique {
853 format!("{table}_{col_name}_uq_idx")
854 } else {
855 format!("{table}_{col_name}_idx")
856 };
857 all_indexes.push(IndexAttr {
858 name: fa.index_name.or(Some(auto_name)),
859 columns: vec![col_name],
860 unique: fa.index_unique,
861 method: fa.index_method,
862 where_clause: None,
863 include: Vec::new(),
864 });
865 }
866 }
867 }
868
869 let model_impl = model_impl_tokens(
870 struct_name,
871 &model_name,
872 &table,
873 display.as_deref(),
874 app_label.as_deref(),
875 container.admin.as_ref(),
876 &container.default_order,
877 &collected.field_schemas,
878 collected.soft_delete_column.as_deref(),
879 container.permissions,
880 audit_track_names.as_deref(),
881 &container.m2m,
882 &all_indexes,
883 &container.checks,
884 &container.excludes,
885 &container.composite_fks,
886 &container.generic_fks,
887 container.scope.as_deref(),
888 container.is_view,
889 container.verbose_name.as_deref(),
890 container.verbose_name_plural.as_deref(),
891 container.managed,
892 container.base_manager_name.as_deref(),
893 container.order_with_respect_to.as_deref(),
894 container.proxy,
895 &container.required_db_features,
896 container.required_db_vendor.as_deref(),
897 container.default_related_name.as_deref(),
898 container.db_table_comment.as_deref(),
899 container
900 .get_latest_by
901 .as_ref()
902 .map(|(c, d)| (c.as_str(), *d)),
903 &container.extra_permissions,
904 &container.default_permissions,
905 &container.global_scopes,
906 &container.reverse_has_relations,
907 &container.generic_has_relations,
908 );
909 let module_ident = column_module_ident(struct_name);
910 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
911 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
912 let track_set: Option<std::collections::HashSet<&str>> = audit
913 .track
914 .as_ref()
915 .map(|(names, _)| names.iter().map(String::as_str).collect());
916 collected
917 .column_entries
918 .iter()
919 .filter(|c| {
920 track_set
921 .as_ref()
922 .map_or(true, |s| s.contains(c.name.as_str()))
923 })
924 .collect()
925 });
926 let inherent_impl = inherent_impl_tokens(
927 struct_name,
928 &collected,
929 collected.primary_key.as_ref(),
930 &column_consts,
931 audited_fields.as_deref(),
932 &all_indexes,
933 &container.manager_fns,
934 );
935 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
936 let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
937 let reverse_helpers = reverse_helper_tokens(
938 struct_name,
939 &collected.fk_relations,
940 container.default_related_name.as_deref(),
941 );
942 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
943 let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
944 // Issue #817 — `#[rustango(through(...))]` accessors.
945 let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
946 // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
947 let reverse_has_accessors =
948 reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
949 let generic_fk_accessors = generic_fk_accessor_tokens(
950 struct_name,
951 &container.generic_fks,
952 &collected.column_entries,
953 );
954
955 // Issue #271 / T1.9 — `#[rustango(manager(ext = "FooManagerExt"))]`
956 // emits an empty extension trait so users can add methods via
957 // `impl FooManagerExt for QuerySet<Foo>` without hand-writing the
958 // trait declaration. See `crates/rustango/src/manager.rs` for the
959 // pattern this replaces.
960 let manager_trait = container.manager_ext.as_ref().map(|name| {
961 let model_name_str = struct_name.to_string();
962 let doc = format!(
963 "Custom-Manager extension trait for [`{model_name_str}`]. \
964 Generated by `#[rustango(manager(ext = ...))]`. Add methods \
965 via `impl {name} for QuerySet<{model_name_str}> {{ ... }}`."
966 );
967 quote! {
968 #[doc = #doc]
969 pub trait #name: ::core::marker::Sized {}
970 }
971 });
972
973 Ok(quote! {
974 #model_impl
975 #inherent_impl
976 #from_row_impl
977 #column_module
978 #reverse_helpers
979 #m2m_accessors
980 #generic_m2m_accessors
981 #through_accessors
982 #reverse_has_accessors
983 #generic_fk_accessors
984 #manager_trait
985
986 #root::core::inventory::submit! {
987 // `module_path!()` evaluates at the registration site,
988 // so a Model declared in `crate::blog::models` records
989 // `"<crate>::blog::models"` and `resolved_app_label()`
990 // can infer "blog" without an explicit attribute.
991 #root::core::ModelEntry::new(
992 <#struct_name as #root::core::Model>::SCHEMA,
993 ::core::module_path!(),
994 )
995 }
996 })
997}
998
999/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
1000/// matches `field_name` against the model's FK fields and, for a
1001/// match, decodes the FK target via the parent's macro-generated
1002/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
1003/// `ForeignKey::Loaded` on `self`.
1004///
1005/// Always emitted (with empty arms for FK-less models, which
1006/// return `Ok(false)` for any field name) so the `T: LoadRelated`
1007/// trait bound on `fetch_on` is universally satisfied — users
1008/// never have to think about implementing it.
1009fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1010 let root = rustango_root();
1011 let arms = fk_relations.iter().map(|rel| {
1012 let parent_ty = &rel.parent_type;
1013 let fk_col = rel.fk_column.as_str();
1014 // FK field's Rust ident matches its SQL column name in v0.8
1015 // (no `column = "..."` rename ships on FK fields).
1016 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1017 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1018 let assign = if rel.nullable {
1019 quote! {
1020 self.#field_ident = ::core::option::Option::Some(
1021 #root::sql::ForeignKey::loaded(_pk, _parent),
1022 );
1023 }
1024 } else {
1025 quote! {
1026 self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1027 }
1028 };
1029 quote! {
1030 #fk_col => {
1031 let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
1032 // Audit #451 — multi-hop `select_related("a__b__c")`:
1033 // stitch the deeper relation onto this parent first,
1034 // decoding it at the accumulated `__next_alias`. The
1035 // parent type also impls `LoadRelated`, so this recurses
1036 // the FK chain to arbitrary depth.
1037 if let ::core::option::Option::Some(__r) = __rest {
1038 let _ = #root::sql::LoadRelated::__rustango_load_related(
1039 &mut _parent, row, __r, &__next_alias,
1040 )?;
1041 }
1042 // Loud-in-debug, default-in-release: a divergence
1043 // between the FK field's declared `K` (drives the
1044 // expected `SqlValue::<Variant>`) and the parent's
1045 // `__rustango_pk_value` output is a macro-internal
1046 // invariant break — surfacing the panic in dev
1047 // catches it before users hit silent PK=0 corruption.
1048 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1049 #root::core::SqlValue::#variant_ident(v) => v,
1050 _other => {
1051 ::core::debug_assert!(
1052 false,
1053 "rustango macro bug: load_related on FK `{}` expected \
1054 SqlValue::{} from parent's __rustango_pk_value but got \
1055 {:?} — file a bug at https://github.com/ujeenet/rustango",
1056 #fk_col,
1057 ::core::stringify!(#variant_ident),
1058 _other,
1059 );
1060 #default_expr
1061 }
1062 };
1063 #assign
1064 ::core::result::Result::Ok(true)
1065 }
1066 }
1067 });
1068 quote! {
1069 #[cfg(feature = "postgres")]
1070 impl #root::sql::LoadRelated for #struct_name {
1071 #[allow(unused_variables)]
1072 fn __rustango_load_related(
1073 &mut self,
1074 row: &#root::sql::sqlx::postgres::PgRow,
1075 field_name: &str,
1076 alias: &str,
1077 ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1078 // Audit #451 — split the multi-hop path: `base` is the FK
1079 // on THIS model, `__rest` (if any) is the remaining chain
1080 // to stitch onto the loaded parent. `__next_alias` is the
1081 // accumulated join alias the parent's columns live under
1082 // (`{alias}__{next-hop}`), matching `lower_select_related`.
1083 let (__base, __rest): (&str, ::core::option::Option<&str>) =
1084 match field_name.split_once("__") {
1085 ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1086 ::core::option::Option::None => (field_name, ::core::option::Option::None),
1087 };
1088 let __next_alias: ::std::string::String = match __rest {
1089 ::core::option::Option::Some(__r) => {
1090 let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1091 ::std::format!("{}__{}", alias, __rb)
1092 }
1093 ::core::option::Option::None => ::std::string::String::new(),
1094 };
1095 match __base {
1096 #( #arms )*
1097 _ => ::core::result::Result::Ok(false),
1098 }
1099 }
1100 }
1101 }
1102}
1103
1104/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1105/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1106/// which expands to a `LoadRelatedMy` impl when rustango is built with
1107/// the `mysql` feature, and to nothing otherwise. The decoded parent
1108/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1109/// decoder, also batch8) so the dual emission is symmetric across
1110/// backends.
1111fn load_related_impl_my_tokens(
1112 struct_name: &syn::Ident,
1113 fk_relations: &[FkRelation],
1114) -> TokenStream2 {
1115 let root = rustango_root();
1116 let arms = fk_relations.iter().map(|rel| {
1117 let parent_ty = &rel.parent_type;
1118 let fk_col = rel.fk_column.as_str();
1119 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1120 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1121 let assign = if rel.nullable {
1122 quote! {
1123 __self.#field_ident = ::core::option::Option::Some(
1124 #root::sql::ForeignKey::loaded(_pk, _parent),
1125 );
1126 }
1127 } else {
1128 quote! {
1129 __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1130 }
1131 };
1132 // `self` IS hygiene-tracked through macro_rules — emitted from
1133 // a different context than the `&mut self` parameter inside
1134 // the macro_rules-expanded fn. Pass it through as `__self`
1135 // and let the macro_rules rebind it to the receiver.
1136 quote! {
1137 #fk_col => {
1138 let mut _parent: #parent_ty =
1139 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1140 // Audit #451 — multi-hop: stitch the deeper relation onto
1141 // the parent at the accumulated alias (see PG twin).
1142 if let ::core::option::Option::Some(__r) = __rest {
1143 let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1144 &mut _parent, row, __r, &__next_alias,
1145 )?;
1146 }
1147 // See note in `load_related_impl_tokens` (PG twin) —
1148 // the same loud-in-debug invariant guard.
1149 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1150 #root::core::SqlValue::#variant_ident(v) => v,
1151 _other => {
1152 ::core::debug_assert!(
1153 false,
1154 "rustango macro bug: load_related on FK `{}` expected \
1155 SqlValue::{} from parent's __rustango_pk_value but got \
1156 {:?} — file a bug at https://github.com/ujeenet/rustango",
1157 #fk_col,
1158 ::core::stringify!(#variant_ident),
1159 _other,
1160 );
1161 #default_expr
1162 }
1163 };
1164 #assign
1165 ::core::result::Result::Ok(true)
1166 }
1167 }
1168 });
1169 quote! {
1170 #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1171 #( #arms )*
1172 });
1173 }
1174}
1175
1176/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1177/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1178/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1179fn load_related_impl_sqlite_tokens(
1180 struct_name: &syn::Ident,
1181 fk_relations: &[FkRelation],
1182) -> TokenStream2 {
1183 let root = rustango_root();
1184 let arms = fk_relations.iter().map(|rel| {
1185 let parent_ty = &rel.parent_type;
1186 let fk_col = rel.fk_column.as_str();
1187 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1188 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1189 let assign = if rel.nullable {
1190 quote! {
1191 __self.#field_ident = ::core::option::Option::Some(
1192 #root::sql::ForeignKey::loaded(_pk, _parent),
1193 );
1194 }
1195 } else {
1196 quote! {
1197 __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1198 }
1199 };
1200 quote! {
1201 #fk_col => {
1202 let mut _parent: #parent_ty =
1203 <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1204 // Audit #451 — multi-hop: stitch the deeper relation onto
1205 // the parent at the accumulated alias (see PG twin).
1206 if let ::core::option::Option::Some(__r) = __rest {
1207 let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1208 &mut _parent, row, __r, &__next_alias,
1209 )?;
1210 }
1211 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1212 #root::core::SqlValue::#variant_ident(v) => v,
1213 _other => {
1214 ::core::debug_assert!(
1215 false,
1216 "rustango macro bug: load_related on FK `{}` expected \
1217 SqlValue::{} from parent's __rustango_pk_value but got \
1218 {:?} — file a bug at https://github.com/ujeenet/rustango",
1219 #fk_col,
1220 ::core::stringify!(#variant_ident),
1221 _other,
1222 );
1223 #default_expr
1224 }
1225 };
1226 #assign
1227 ::core::result::Result::Ok(true)
1228 }
1229 }
1230 });
1231 quote! {
1232 #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1233 #( #arms )*
1234 });
1235 }
1236}
1237
1238/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1239/// matches `field_name` against the model's FK fields and returns
1240/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1241/// group children by parent PK.
1242///
1243/// Always emitted (with `_ => None` for FK-less models) so the
1244/// trait bound on `fetch_with_prefetch` is universally satisfied.
1245fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1246 let root = rustango_root();
1247 let arms = fk_relations.iter().map(|rel| {
1248 let fk_col = rel.fk_column.as_str();
1249 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1250 if rel.pk_kind == DetectedKind::I64 {
1251 // i64 FK — return the stored PK so prefetch_related can
1252 // group children by it. Nullable variant unwraps via
1253 // `as_ref().map(...)`: an unset (NULL) FK column yields
1254 // `None` and that child sits out of the grouping (correct
1255 // semantics — it has no parent to attach to).
1256 if rel.nullable {
1257 quote! {
1258 #fk_col => self.#field_ident
1259 .as_ref()
1260 .map(|fk| #root::sql::ForeignKey::pk(fk)),
1261 }
1262 } else {
1263 quote! {
1264 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1265 }
1266 }
1267 } else {
1268 // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1269 // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1270 // i64-keyed grouping path — the trait signature is
1271 // `Option<i64>` and a non-i64 PK can't lower into it.
1272 // The FK still works for everything else (CRUD, lazy
1273 // load via `.get()`, select_related JOINs); only the
1274 // bulk prefetch grouper needs the integer key.
1275 quote! {
1276 #fk_col => ::core::option::Option::None,
1277 }
1278 }
1279 });
1280 // PK-type-agnostic version: every FK arm emits an
1281 // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1282 // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1283 // opt OUT of the legacy i64 method (it returns None) but opt IN
1284 // here.
1285 let value_arms = fk_relations.iter().map(|rel| {
1286 let fk_col = rel.fk_column.as_str();
1287 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1288 if rel.nullable {
1289 quote! {
1290 #fk_col => self.#field_ident
1291 .as_ref()
1292 .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1293 #root::sql::ForeignKey::pk(fk)
1294 )),
1295 }
1296 } else {
1297 quote! {
1298 #fk_col => ::core::option::Option::Some(
1299 ::core::convert::Into::<#root::core::SqlValue>::into(
1300 self.#field_ident.pk()
1301 )
1302 ),
1303 }
1304 }
1305 });
1306 quote! {
1307 impl #root::sql::FkPkAccess for #struct_name {
1308 #[allow(unused_variables)]
1309 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1310 match field_name {
1311 #( #arms )*
1312 _ => ::core::option::Option::None,
1313 }
1314 }
1315 #[allow(unused_variables)]
1316 fn __rustango_fk_pk_value(
1317 &self,
1318 field_name: &str,
1319 ) -> ::core::option::Option<#root::core::SqlValue> {
1320 match field_name {
1321 #( #value_arms )*
1322 _ => ::core::option::Option::None,
1323 }
1324 }
1325 }
1326 }
1327}
1328
1329/// For every `ForeignKey<Parent>` field on `Child`, emit
1330/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1331/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
1332/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
1333/// — the canonical reverse-FK fetch. One round trip, no N+1.
1334///
1335/// **PG-only emission**: the accessor is bounded on
1336/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1337/// both of which are gated behind the `postgres` cargo feature. The
1338/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1339/// model derive itself compiles on tri-dialect / sqlite-only
1340/// downstream builds — the accessor just isn't materialised. A tri-
1341/// dialect `_set_pool` variant is a separate follow-up.
1342fn reverse_helper_tokens(
1343 child_ident: &syn::Ident,
1344 fk_relations: &[FkRelation],
1345 default_related_name: Option<&str>,
1346) -> TokenStream2 {
1347 let root = rustango_root();
1348 if fk_relations.is_empty() {
1349 return TokenStream2::new();
1350 }
1351 // Method-name resolution per FK (issue #816 + follow-up):
1352 // 1. Field-level `#[rustango(related_name = "...")]` on the FK
1353 // itself — wins over everything else.
1354 // 2. Container-level `default_related_name = "..."` on the
1355 // child. Applies to every FK on this model that didn't
1356 // override.
1357 // 3. Fallback: `<child_snake>_set`. `Post` → `post_set`,
1358 // `BlogComment` → `blog_comment_set`. Avoids English-plural
1359 // edge cases.
1360 //
1361 // The PG-on-executor variant keeps the resolved name; the
1362 // tri-dialect `_pool` variant appends `_pool` to it (matches the
1363 // framework's convention for the `&Pool` flavor of every helper).
1364 let default_pg_suffix = default_related_name
1365 .map(str::to_owned)
1366 .unwrap_or_else(|| format!("{}_set", to_snake_case(&child_ident.to_string())));
1367 let impls = fk_relations.iter().map(|rel| {
1368 let pg_suffix = rel
1369 .related_name
1370 .clone()
1371 .unwrap_or_else(|| default_pg_suffix.clone());
1372 let pool_suffix = format!("{}_pool", pg_suffix);
1373 let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1374 let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1375 let parent_ty = &rel.parent_type;
1376 let fk_col = rel.fk_column.as_str();
1377 let doc = format!(
1378 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1379 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1380 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1381 further `{child_ident}::objects()` filters via direct queryset use."
1382 );
1383 let pool_doc = format!(
1384 "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1385 [`#root::sql::Pool`] and dispatches per backend so the \
1386 reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1387 Use this from framework code that holds a `&Pool` (admin, \
1388 tenancy resolver, viewset handlers); reach for the executor- \
1389 bound variant when you already have a typed `sqlx::Executor`."
1390 );
1391 quote! {
1392 #[cfg(feature = "postgres")]
1393 impl #parent_ty {
1394 #[doc = #doc]
1395 ///
1396 /// # Errors
1397 /// Returns [`#root::sql::ExecError`] for SQL-writing
1398 /// or driver failures.
1399 pub async fn #pg_method_ident<'_c, _E>(
1400 &self,
1401 _executor: _E,
1402 ) -> ::core::result::Result<
1403 ::std::vec::Vec<#child_ident>,
1404 #root::sql::ExecError,
1405 >
1406 where
1407 _E: #root::sql::sqlx::Executor<
1408 '_c,
1409 Database = #root::sql::sqlx::Postgres,
1410 >,
1411 {
1412 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1413 #root::query::QuerySet::<#child_ident>::new()
1414 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1415 .fetch_on(_executor)
1416 .await
1417 }
1418 }
1419
1420 impl #parent_ty {
1421 #[doc = #pool_doc]
1422 ///
1423 /// # Errors
1424 /// Returns [`#root::sql::ExecError`] for SQL-writing
1425 /// or driver failures.
1426 pub async fn #pool_method_ident(
1427 &self,
1428 pool: &#root::sql::Pool,
1429 ) -> ::core::result::Result<
1430 ::std::vec::Vec<#child_ident>,
1431 #root::sql::ExecError,
1432 > {
1433 use #root::sql::FetcherPool as _;
1434 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1435 #root::query::QuerySet::<#child_ident>::new()
1436 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1437 .fetch(pool)
1438 .await
1439 }
1440 }
1441 }
1442 });
1443 quote! { #( #impls )* }
1444}
1445
1446/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1447/// relation declared on the model.
1448/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1449/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1450///
1451/// Closes #239 + #240 — `comment.content_object` /
1452/// `comment.content_object = post` ergonomics on top of the existing
1453/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1454///
1455/// `column_entries` is passed so we can resolve each `ct_column` /
1456/// `pk_column` SQL name back to its Rust field ident — the macro
1457/// only sees the column-side strings in the attribute, but the
1458/// emitted accessor needs to read the actual struct field.
1459fn generic_fk_accessor_tokens(
1460 struct_name: &syn::Ident,
1461 generic_fks: &[GenericFkAttr],
1462 column_entries: &[ColumnEntry],
1463) -> TokenStream2 {
1464 let root = rustango_root();
1465 if generic_fks.is_empty() {
1466 return TokenStream2::new();
1467 }
1468 let methods = generic_fks.iter().filter_map(|gfk| {
1469 // Resolve `ct_column` + `pk_column` to the struct's Rust
1470 // field idents. A typo (column name doesn't match any field)
1471 // emits no method for that registration — the user will see
1472 // the compiler reject the SCHEMA literal anyway, so there's
1473 // a clear error path without us double-reporting.
1474 let ct_ident = column_entries
1475 .iter()
1476 .find(|c| c.column == gfk.ct_column)
1477 .map(|c| c.ident.clone())?;
1478 let pk_ident = column_entries
1479 .iter()
1480 .find(|c| c.column == gfk.pk_column)
1481 .map(|c| c.ident.clone())?;
1482
1483 let accessor_ident =
1484 syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1485 let setter_ident =
1486 syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1487 let name_literal = gfk.name.as_str();
1488
1489 Some(quote! {
1490 #[doc = concat!(
1491 "Resolve the polymorphic `",
1492 #name_literal,
1493 "` relation. Reads `self.",
1494 stringify!(#ct_ident),
1495 "` + `self.",
1496 stringify!(#pk_ident),
1497 "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1498 "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1499 #name_literal,
1500 "\", ...))]`."
1501 )]
1502 pub async fn #accessor_ident(
1503 &self,
1504 pool: &#root::sql::Pool,
1505 ) -> ::core::result::Result<
1506 ::core::option::Option<#root::__serde_json::Value>,
1507 #root::sql::ExecError,
1508 > {
1509 let gfk = #root::contenttypes::GenericForeignKey::new(
1510 self.#ct_ident as i64,
1511 self.#pk_ident as i64,
1512 );
1513 gfk.get_object(pool).await
1514 }
1515
1516 #[doc = concat!(
1517 "Set the polymorphic `",
1518 #name_literal,
1519 "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1520 stringify!(#ct_ident),
1521 "` and `self.",
1522 stringify!(#pk_ident),
1523 "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1524 #name_literal,
1525 "\", ...))]`."
1526 )]
1527 pub async fn #setter_ident<T: #root::core::Model>(
1528 &mut self,
1529 pool: &#root::sql::Pool,
1530 target_pk: i64,
1531 ) -> ::core::result::Result<(), #root::sql::ExecError> {
1532 let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1533 pool,
1534 target_pk,
1535 ).await?;
1536 self.#ct_ident = gfk.content_type_id as _;
1537 self.#pk_ident = gfk.object_pk as _;
1538 ::core::result::Result::Ok(())
1539 }
1540 })
1541 });
1542 quote! {
1543 impl #struct_name {
1544 #( #methods )*
1545 }
1546 }
1547}
1548
1549fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1550 let root = rustango_root();
1551 if m2m_relations.is_empty() {
1552 return TokenStream2::new();
1553 }
1554 let methods = m2m_relations.iter().map(|rel| {
1555 let method_name = format!("{}_m2m", rel.name);
1556 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1557 let through = rel.through.as_str();
1558 let src_col = rel.src.as_str();
1559 let dst_col = rel.dst.as_str();
1560 quote! {
1561 pub fn #method_ident(&self) -> #root::sql::M2MManager {
1562 #root::sql::M2MManager {
1563 src_pk: self.__rustango_pk_value(),
1564 through: #through,
1565 src_col: #src_col,
1566 dst_col: #dst_col,
1567 }
1568 }
1569 }
1570 });
1571 quote! {
1572 impl #struct_name {
1573 #( #methods )*
1574 }
1575 }
1576}
1577
1578/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1579/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1580fn generic_m2m_accessor_tokens(
1581 struct_name: &syn::Ident,
1582 relations: &[GenericM2MAttr],
1583) -> TokenStream2 {
1584 let root = rustango_root();
1585 if relations.is_empty() {
1586 return TokenStream2::new();
1587 }
1588 let methods = relations.iter().map(|rel| {
1589 let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1590 let through = rel.through.as_str();
1591 let pk_col = rel.pk_column.as_str();
1592 let ct_col = rel.ct_column.as_str();
1593 let related_col = rel.related_column.as_str();
1594 quote! {
1595 pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1596 #root::sql::GenericM2MManager {
1597 src_pk: self.__rustango_pk_value(),
1598 src_schema: <Self as #root::core::Model>::SCHEMA,
1599 through: #through,
1600 pk_col: #pk_col,
1601 ct_col: #ct_col,
1602 dst_col: #related_col,
1603 }
1604 }
1605 }
1606 });
1607 quote! {
1608 impl #struct_name {
1609 #( #methods )*
1610 }
1611 }
1612}
1613
1614/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1615/// associated functions for each `#[rustango(reverse_has(...))]`
1616/// attribute. Issue #830.
1617///
1618/// The two emitted functions return ready-to-use `WhereExpr` nodes
1619/// that downstream callers drop into
1620/// `QuerySet::<Self>::where_raw(...)`:
1621///
1622/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1623/// <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1624/// `whereHas` parity (without the closure-style sub-predicate
1625/// refinement — that's a follow-up; users can layer additional
1626/// predicates by constructing the SelectQuery themselves and
1627/// calling `where_raw(exists(query))` from `crate::core::subquery`).
1628/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1629/// `whereDoesntHave` parity.
1630///
1631/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1632/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1633/// machinery threads the outer-table reference through automatically
1634/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1635fn reverse_has_accessor_tokens(
1636 struct_name: &syn::Ident,
1637 reverse_has_relations: &[ReverseHasAttr],
1638) -> TokenStream2 {
1639 let root = rustango_root();
1640 if reverse_has_relations.is_empty() {
1641 return TokenStream2::new();
1642 }
1643 let methods = reverse_has_relations.iter().map(|rel| {
1644 let exists_name = format!("{}_exists_expr", rel.name);
1645 let not_exists_name = format!("{}_not_exists_expr", rel.name);
1646 let count_name = format!("{}_count", rel.name);
1647 let fetch_name = format!("{}_fetch", rel.name);
1648 let first_name = format!("{}_first", rel.name);
1649 let pluck_name = format!("{}_pluck", rel.name);
1650 let accessor_name = rel.name.as_str();
1651 let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1652 let not_exists_ident = syn::Ident::new(¬_exists_name, struct_name.span());
1653 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1654 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1655 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1656 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1657 let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1658 let child = &rel.child;
1659 let child_fk_column = rel.child_fk_column.as_str();
1660 let self_pk_column = rel.self_pk_column.as_str();
1661 let exists_doc = format!(
1662 "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1663 FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1664 Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1665 filter to {struct_name}s with at least one matching child.",
1666 );
1667 let not_exists_doc = format!(
1668 "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1669 (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1670 <outer>.{self_pk_column})`. Drop into \
1671 `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1672 {struct_name}s with **no** matching child.",
1673 );
1674 let count_doc = format!(
1675 "Eloquent `$model->{name}->count()` analog — returns \
1676 the number of `{child}` rows whose `{child_fk_column}` \
1677 matches this `{struct_name}` instance's primary key. \
1678 Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1679 {child_fk_column} = <self.pk>`.",
1680 name = rel.name,
1681 );
1682 let accessor_doc = format!(
1683 "Eloquent `$model->{name}` accessor — returns a \
1684 `QuerySet<{child}>` filtered to rows whose \
1685 `{child_fk_column}` matches this `{struct_name}` \
1686 instance's primary key. **Chainable**: compose `.filter()` \
1687 / `.order_by()` / `.limit()` etc. on top, then call \
1688 `.fetch(&pool)` (the QuerySet trait method) when \
1689 done. For the simple \"fetch all\" hot path with no \
1690 further composition, prefer the bare-name \
1691 `{name}_fetch(&pool)` companion.",
1692 name = rel.name,
1693 );
1694 let fetch_doc = format!(
1695 "Eloquent `$model->{name}->get()` — bare-name hot-path \
1696 over `{name}(&self).fetch(&pool)`. Use this when \
1697 you don't need further `.filter()` / `.order_by()` \
1698 composition; falls back to the chainable accessor when \
1699 you do. Avoids the `_pool` suffix on the most common \
1700 call-site shape.",
1701 name = rel.name,
1702 );
1703 quote! {
1704 #[doc = #accessor_doc]
1705 pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1706 #root::query::QuerySet::<#child>::new()
1707 .filter(#child_fk_column, self.__rustango_pk_value())
1708 }
1709
1710 #[doc = #fetch_doc]
1711 pub async fn #fetch_ident(
1712 &self,
1713 pool: &#root::sql::Pool,
1714 ) -> ::core::result::Result<
1715 ::std::vec::Vec<#child>,
1716 #root::sql::ExecError,
1717 > {
1718 use #root::sql::FetcherPool as _;
1719 self.#accessor_ident().fetch(pool).await
1720 }
1721
1722 /// Eloquent `$model->relation->first()` / `hasOne`
1723 /// semantics — bare-name shortcut over
1724 /// `self.<name>().first(&pool)`. Returns `None` when no
1725 /// child rows match. Useful when the relation is
1726 /// nominally many-to-one in shape but at most one row is
1727 /// expected (latest comment, primary tag, etc.).
1728 pub async fn #first_ident(
1729 &self,
1730 pool: &#root::sql::Pool,
1731 ) -> ::core::result::Result<
1732 ::core::option::Option<#child>,
1733 #root::sql::ExecError,
1734 > {
1735 self.#accessor_ident().first(pool).await
1736 }
1737
1738 /// Eloquent `$model->relation->pluck($col)` — project a
1739 /// single column from the child rows into a `Vec<U>`.
1740 /// Skips the typed `Child` decode, which is cheaper when
1741 /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1742 pub async fn #pluck_ident<U>(
1743 &self,
1744 col: &'static str,
1745 pool: &#root::sql::Pool,
1746 ) -> ::core::result::Result<
1747 ::std::vec::Vec<U>,
1748 #root::sql::ExecError,
1749 >
1750 where
1751 U: #root::sql::MaybePgScalar
1752 + #root::sql::MaybeMyScalar
1753 + #root::sql::MaybeSqliteScalar
1754 + ::core::marker::Send
1755 + ::core::marker::Unpin,
1756 {
1757 self.#accessor_ident()
1758 .values_list_flat(col)
1759 .fetch::<U>(pool)
1760 .await
1761 }
1762
1763 #[doc = #exists_doc]
1764 pub fn #exists_ident() -> #root::core::WhereExpr {
1765 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1766 let child_schema =
1767 <#child as #root::core::Model>::SCHEMA;
1768 let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1769 lhs: Expr::Column(#child_fk_column),
1770 op: Op::Eq,
1771 rhs: Expr::OuterRef(#self_pk_column),
1772 });
1773 WhereExpr::Exists(::std::boxed::Box::new(inner))
1774 }
1775
1776 #[doc = #not_exists_doc]
1777 pub fn #not_exists_ident() -> #root::core::WhereExpr {
1778 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1779 let child_schema =
1780 <#child as #root::core::Model>::SCHEMA;
1781 let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1782 lhs: Expr::Column(#child_fk_column),
1783 op: Op::Eq,
1784 rhs: Expr::OuterRef(#self_pk_column),
1785 });
1786 WhereExpr::NotExists(::std::boxed::Box::new(inner))
1787 }
1788
1789 #[doc = #count_doc]
1790 pub async fn #count_ident(
1791 &self,
1792 pool: &#root::sql::Pool,
1793 ) -> ::core::result::Result<
1794 i64,
1795 #root::sql::ExecError,
1796 > {
1797 use #root::sql::CounterPool as _;
1798 #root::query::QuerySet::<#child>::new()
1799 .filter(#child_fk_column, self.__rustango_pk_value())
1800 .count(pool)
1801 .await
1802 }
1803 }
1804 });
1805 quote! {
1806 impl #struct_name {
1807 #( #methods )*
1808 }
1809 }
1810}
1811
1812/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1813/// `#[rustango(through(...))]` attribute. Issue #817.
1814///
1815/// Each accessor builds a correlated subquery via
1816/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1817/// intermediate table, filters on the FK column pointing at this
1818/// model, and projects the intermediate PK. The outer queryset
1819/// filters the far model by its FK-to-intermediate column being in
1820/// that set.
1821///
1822/// The returned `QuerySet<Far>` is **chainable** — the subquery
1823/// lives inside a `where_raw` clause so the user's later
1824/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1825/// disturb it.
1826///
1827/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1828/// SQLite — no LATERAL or backend-specific syntax involved.
1829fn through_accessor_tokens(
1830 struct_name: &syn::Ident,
1831 through_relations: &[ThroughAttr],
1832) -> TokenStream2 {
1833 let root = rustango_root();
1834 if through_relations.is_empty() {
1835 return TokenStream2::new();
1836 }
1837 let methods = through_relations.iter().map(|rel| {
1838 let method_name = format!("{}_through", rel.name);
1839 let count_name = format!("{}_through_count", rel.name);
1840 let fetch_name = format!("{}_through_fetch", rel.name);
1841 let first_name = format!("{}_through_first", rel.name);
1842 let pluck_name = format!("{}_through_pluck", rel.name);
1843 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1844 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1845 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1846 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1847 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1848 let far = &rel.far;
1849 let intermediate = &rel.intermediate;
1850 let far_fk_column = rel.far_fk_column.as_str();
1851 let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1852 let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1853 let doc = format!(
1854 "Eloquent `hasManyThrough` accessor — returns a \
1855 `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1856 instance through the intermediate `{intermediate}` table. \
1857 Generated SQL shape: \
1858 `… WHERE {far_fk_column} IN (SELECT \
1859 {intermediate_pk_column} FROM <{intermediate}> WHERE \
1860 {intermediate_fk_column} = self.pk)`. Chainable like any \
1861 other QuerySet — compose `.filter()` / `.order_by()` / \
1862 `.limit()` etc. on top.",
1863 );
1864 let count_doc = format!(
1865 "Eloquent `$model->{name}->count()` analog for the \
1866 through-relation — returns the number of `{far}` rows \
1867 reachable through `{intermediate}`. Equivalent to \
1868 `self.{name}_through().count(pool)` but spelled \
1869 as a bare instance method for parity with the \
1870 `reverse_has` `<name>_count` shape.",
1871 name = rel.name,
1872 );
1873 quote! {
1874 #[doc = #doc]
1875 pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1876 use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1877 let intermediate_schema =
1878 <#intermediate as #root::core::Model>::SCHEMA;
1879 let sub = SelectQuery::new(intermediate_schema)
1880 .where_clause(WhereExpr::Predicate(Filter::new(
1881 #intermediate_fk_column,
1882 Op::Eq,
1883 self.__rustango_pk_value(),
1884 )))
1885 .projection(::std::vec![#intermediate_pk_column]);
1886 #root::query::QuerySet::<#far>::new().where_raw(
1887 WhereExpr::InSubquery {
1888 column: #far_fk_column,
1889 negated: false,
1890 subquery: ::std::boxed::Box::new(sub),
1891 },
1892 )
1893 }
1894
1895 #[doc = #count_doc]
1896 pub async fn #count_ident(
1897 &self,
1898 pool: &#root::sql::Pool,
1899 ) -> ::core::result::Result<
1900 i64,
1901 #root::sql::ExecError,
1902 > {
1903 use #root::sql::CounterPool as _;
1904 self.#method_ident().count(pool).await
1905 }
1906
1907 /// Eloquent `$model->relation->get()` for the
1908 /// through-relation — bare-name hot-path over
1909 /// `self.<name>_through().fetch(&pool)`. Use this
1910 /// when you don't need further `.filter()` /
1911 /// `.order_by()` composition; falls back to the
1912 /// chainable accessor when you do. Avoids the `_pool`
1913 /// suffix on the most common call-site shape.
1914 pub async fn #fetch_ident(
1915 &self,
1916 pool: &#root::sql::Pool,
1917 ) -> ::core::result::Result<
1918 ::std::vec::Vec<#far>,
1919 #root::sql::ExecError,
1920 > {
1921 use #root::sql::FetcherPool as _;
1922 self.#method_ident().fetch(pool).await
1923 }
1924
1925 /// Eloquent `hasOneThrough` analog — bare-name shortcut
1926 /// over `self.<name>_through().first(&pool)`. Returns
1927 /// `None` when no far rows are reachable through the
1928 /// intermediate. Useful when at most one row is
1929 /// expected (latest comment by country, primary tag,
1930 /// etc.).
1931 pub async fn #first_ident(
1932 &self,
1933 pool: &#root::sql::Pool,
1934 ) -> ::core::result::Result<
1935 ::core::option::Option<#far>,
1936 #root::sql::ExecError,
1937 > {
1938 self.#method_ident().first(pool).await
1939 }
1940
1941 /// Pluck a single column from the far rows into `Vec<U>`
1942 /// — cheaper than the typed `<Far>` decode when only one
1943 /// scalar column is needed.
1944 pub async fn #pluck_ident<U>(
1945 &self,
1946 col: &'static str,
1947 pool: &#root::sql::Pool,
1948 ) -> ::core::result::Result<
1949 ::std::vec::Vec<U>,
1950 #root::sql::ExecError,
1951 >
1952 where
1953 U: #root::sql::MaybePgScalar
1954 + #root::sql::MaybeMyScalar
1955 + #root::sql::MaybeSqliteScalar
1956 + ::core::marker::Send
1957 + ::core::marker::Unpin,
1958 {
1959 self.#method_ident()
1960 .values_list_flat(col)
1961 .fetch::<U>(pool)
1962 .await
1963 }
1964 }
1965 });
1966 quote! {
1967 impl #struct_name {
1968 #( #methods )*
1969 }
1970 }
1971}
1972
1973struct ColumnEntry {
1974 /// The struct field ident, used both for the inherent const name on
1975 /// the model and for the inner column type's name.
1976 ident: syn::Ident,
1977 /// The struct's field type, used as `Column::Value`.
1978 value_ty: Type,
1979 /// Rust-side field name (e.g. `"id"`).
1980 name: String,
1981 /// SQL-side column name (e.g. `"user_id"`).
1982 column: String,
1983 /// `#root::core::FieldType::I64` etc.
1984 field_type_tokens: TokenStream2,
1985}
1986
1987struct CollectedFields {
1988 field_schemas: Vec<TokenStream2>,
1989 from_row_inits: Vec<TokenStream2>,
1990 /// Aliased counterparts of `from_row_inits` — read columns via
1991 /// `format!("{prefix}__{col}")` aliases so a Model can be
1992 /// decoded from a JOINed row's projected target columns.
1993 from_aliased_row_inits: Vec<TokenStream2>,
1994 /// Static column-name list — used by the simple insert path
1995 /// (no `Auto<T>` fields). Aligned with `insert_values`.
1996 insert_columns: Vec<TokenStream2>,
1997 /// Static `Into<SqlValue>` expressions, one per field. Aligned
1998 /// with `insert_columns`. Used by the simple insert path only.
1999 insert_values: Vec<TokenStream2>,
2000 /// Per-field push expressions for the dynamic (Auto-aware)
2001 /// insert path. Each statement either unconditionally pushes
2002 /// `(column, value)` or, for an `Auto<T>` field, conditionally
2003 /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
2004 insert_pushes: Vec<TokenStream2>,
2005 /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
2006 /// when `has_auto == false`.
2007 returning_cols: Vec<TokenStream2>,
2008 /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
2009 /// field. Run after `insert_returning` to populate the model.
2010 auto_assigns: Vec<TokenStream2>,
2011 /// `(ident, column_literal)` pairs for every Auto field. Used by
2012 /// the bulk_insert codegen to rebuild assigns against `_row_mut`
2013 /// instead of `self`.
2014 auto_field_idents: Vec<(syn::Ident, String)>,
2015 /// #1028 — `(ident, column)` for each `generated_as` field. Drives
2016 /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
2017 /// back into the struct after insert (MySQL has no RETURNING → the
2018 /// field stays at its placeholder until the next read).
2019 generated_field_idents: Vec<(syn::Ident, String)>,
2020 /// Inner `T` of the first `Auto<T>` field, for the MySQL
2021 /// `LAST_INSERT_ID()` assignment in `AssignAutoPkPool`.
2022 first_auto_value_ty: Option<Type>,
2023 /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2024 /// by the all-Auto-Unset bulk path (Auto cols dropped from
2025 /// `columns`).
2026 bulk_pushes_no_auto: Vec<TokenStream2>,
2027 /// Bulk-insert per-row pushes for **all fields including Auto**.
2028 /// Used by the all-Auto-Set bulk path (Auto col included with the
2029 /// caller-supplied value).
2030 bulk_pushes_all: Vec<TokenStream2>,
2031 /// Column-name literals for non-Auto fields only (paired with
2032 /// `bulk_pushes_no_auto`).
2033 bulk_columns_no_auto: Vec<TokenStream2>,
2034 /// Column-name literals for every field including Auto (paired
2035 /// with `bulk_pushes_all`).
2036 bulk_columns_all: Vec<TokenStream2>,
2037 /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2038 /// + the loop that asserts every row matches. One pair per Auto
2039 /// field. Empty when `has_auto == false`.
2040 bulk_auto_uniformity: Vec<TokenStream2>,
2041 /// Identifier of the first Auto field, used as the witness for
2042 /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2043 first_auto_ident: Option<syn::Ident>,
2044 /// `true` if any field on the struct is `Auto<T>`.
2045 has_auto: bool,
2046 /// `true` when the primary-key field's Rust type is `Auto<T>`.
2047 /// Gates `save()` codegen — only Auto PKs let us infer
2048 /// insert-vs-update from the in-memory value.
2049 pk_is_auto: bool,
2050 /// `Assignment` constructors for every non-PK column. Drives the
2051 /// UPDATE branch of `save()`.
2052 update_assignments: Vec<TokenStream2>,
2053 /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2054 /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2055 upsert_update_columns: Vec<TokenStream2>,
2056 primary_key: Option<(syn::Ident, String)>,
2057 column_entries: Vec<ColumnEntry>,
2058 /// Rust-side field names, in declaration order. Used to validate
2059 /// container attributes like `display = "…"`.
2060 field_names: Vec<String>,
2061 /// FK fields on this child model. Drives the reverse-relation
2062 /// helper emit — for each FK, the macro adds an inherent
2063 /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2064 /// method on the parent type.
2065 fk_relations: Vec<FkRelation>,
2066 /// SQL column name of the `#[rustango(soft_delete)]` field, if
2067 /// the model has one. Drives emission of the `soft_delete_on` /
2068 /// `restore_on` inherent methods. At most one such column per
2069 /// model is allowed; collect_fields rejects duplicates.
2070 soft_delete_column: Option<String>,
2071 /// Rust field ident of the `#[rustango(soft_delete)]` field —
2072 /// companion to `soft_delete_column` for emitting predicates
2073 /// that need to read the field off `&self` (e.g. `trashed()`).
2074 soft_delete_field_ident: Option<syn::Ident>,
2075}
2076
2077#[derive(Clone)]
2078struct FkRelation {
2079 /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2080 /// helper is emitted as `impl <ParentType> { … }`.
2081 parent_type: Type,
2082 /// SQL column name on the child table for this FK (e.g. `"author"`).
2083 /// Used in the generated `WHERE <fk_column> = $1` clause.
2084 fk_column: String,
2085 /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2086 /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2087 /// default `ForeignKey<T>` (no explicit K); other kinds when the
2088 /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2089 pk_kind: DetectedKind,
2090 /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2091 /// FK column). Drives the `Some(...)` wrapping in load_related
2092 /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2093 /// the codegen matches the field's declared shape.
2094 nullable: bool,
2095 /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2096 /// override. When set, the reverse helper picks this name instead
2097 /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2098 /// #816 (issue's "Related" note re: per-FK override).
2099 related_name: Option<String>,
2100}
2101
2102fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2103 let root = rustango_root();
2104 let cap = named.named.len();
2105 let mut out = CollectedFields {
2106 field_schemas: Vec::with_capacity(cap),
2107 from_row_inits: Vec::with_capacity(cap),
2108 from_aliased_row_inits: Vec::with_capacity(cap),
2109 insert_columns: Vec::with_capacity(cap),
2110 insert_values: Vec::with_capacity(cap),
2111 insert_pushes: Vec::with_capacity(cap),
2112 returning_cols: Vec::new(),
2113 auto_assigns: Vec::new(),
2114 auto_field_idents: Vec::new(),
2115 generated_field_idents: Vec::new(),
2116 first_auto_value_ty: None,
2117 bulk_pushes_no_auto: Vec::with_capacity(cap),
2118 bulk_pushes_all: Vec::with_capacity(cap),
2119 bulk_columns_no_auto: Vec::with_capacity(cap),
2120 bulk_columns_all: Vec::with_capacity(cap),
2121 bulk_auto_uniformity: Vec::new(),
2122 first_auto_ident: None,
2123 has_auto: false,
2124 pk_is_auto: false,
2125 update_assignments: Vec::with_capacity(cap),
2126 upsert_update_columns: Vec::with_capacity(cap),
2127 primary_key: None,
2128 column_entries: Vec::with_capacity(cap),
2129 field_names: Vec::with_capacity(cap),
2130 fk_relations: Vec::new(),
2131 soft_delete_column: None,
2132 soft_delete_field_ident: None,
2133 };
2134
2135 for field in &named.named {
2136 let info = process_field(field, table)?;
2137 out.field_names.push(info.ident.to_string());
2138 out.field_schemas.push(info.schema);
2139 out.from_row_inits.push(info.from_row_init);
2140 out.from_aliased_row_inits.push(info.from_aliased_row_init);
2141 if let Some(parent_ty) = info.fk_inner.clone() {
2142 out.fk_relations.push(FkRelation {
2143 parent_type: parent_ty,
2144 fk_column: info.column.clone(),
2145 pk_kind: info.fk_pk_kind,
2146 nullable: info.nullable,
2147 related_name: info.related_name.clone(),
2148 });
2149 }
2150 if info.soft_delete {
2151 if out.soft_delete_column.is_some() {
2152 return Err(syn::Error::new_spanned(
2153 field,
2154 "only one field may be marked `#[rustango(soft_delete)]`",
2155 ));
2156 }
2157 out.soft_delete_column = Some(info.column.clone());
2158 out.soft_delete_field_ident = Some(info.ident.clone());
2159 }
2160 let column = info.column.as_str();
2161 let ident = info.ident;
2162 // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2163 // skip every write path — Postgres recomputes the value
2164 // from EXPR. Push only the column-entry record (so typed
2165 // column constants still exist for filtering / projection)
2166 // and the schema literal (already pushed above) and move
2167 // on. No insert_columns/values, no insert_pushes, no
2168 // bulk_*, no update_assignments, no upsert_update_columns,
2169 // no returning_cols.
2170 if info.generated_as.is_some() {
2171 out.column_entries.push(ColumnEntry {
2172 ident: ident.clone(),
2173 value_ty: info.value_ty.clone(),
2174 name: ident.to_string(),
2175 column: info.column.clone(),
2176 field_type_tokens: info.field_type_tokens,
2177 });
2178 // #1028 — refresh the DB-computed value after insert on
2179 // RETURNING-capable backends. The column joins `returning_cols`
2180 // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2181 // ident is recorded so the `AssignAutoPkPool` impl decodes it
2182 // back into the struct. MySQL has no `INSERT … RETURNING`, so
2183 // it keeps the placeholder until the next read.
2184 out.returning_cols.push(quote!(#column));
2185 out.generated_field_idents
2186 .push((ident.clone(), info.column.clone()));
2187 continue;
2188 }
2189 out.insert_columns.push(quote!(#column));
2190 out.insert_values.push(quote! {
2191 ::core::convert::Into::<#root::core::SqlValue>::into(
2192 ::core::clone::Clone::clone(&self.#ident)
2193 )
2194 });
2195 if info.auto {
2196 out.has_auto = true;
2197 if out.first_auto_ident.is_none() {
2198 out.first_auto_ident = Some(ident.clone());
2199 out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
2200 }
2201 // `default_uuid_v7` (issue #823) generates the PK Rust-side
2202 // before binding, so the value is already in
2203 // `self.#ident` after the insert_push — RETURNING is
2204 // redundant. Skip adding this column to returning_cols /
2205 // auto_assigns / auto_field_idents to avoid (a) an
2206 // unnecessary RETURNING column on every dialect, and (b)
2207 // the MySQL `LAST_INSERT_ID()` path that can only fill an
2208 // integer PK.
2209 // `auto_now_add` / `auto_now` join `default_uuid_v7` here
2210 // for the same reason: they are filled Rust-side below and
2211 // bound, so the value is already in `self` and RETURNING
2212 // would be a redundant column on every dialect.
2213 if !info.default_uuid_v7 && !info.auto_now_add && !info.auto_now {
2214 out.returning_cols.push(quote!(#column));
2215 out.auto_field_idents
2216 .push((ident.clone(), info.column.clone()));
2217 out.auto_assigns.push(quote! {
2218 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2219 });
2220 }
2221 if info.default_uuid_v7 {
2222 // Rust-side UUIDv7 generation (issue #823, Eloquent
2223 // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2224 // and bind; Auto::Set → bind the user's value. The
2225 // column is ALWAYS present in the INSERT statement —
2226 // no RETURNING / no DB DEFAULT needed.
2227 out.insert_pushes.push(quote! {
2228 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2229 self.#ident = #root::sql::Auto::Set(
2230 #root::__uuid::Uuid::now_v7(),
2231 );
2232 }
2233 if let #root::sql::Auto::Set(_v) = &self.#ident {
2234 _columns.push(#column);
2235 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2236 ::core::clone::Clone::clone(_v)
2237 ));
2238 }
2239 });
2240 } else if info.auto_now_add || info.auto_now {
2241 // Rust-side write timestamp (#1464), mirroring
2242 // `default_uuid_v7` above. `auto_now` has always bound
2243 // `Utc::now()` on UPDATE but not on INSERT, so the
2244 // first write of an `updated_at` took the DB default
2245 // while every later one took the clock — the same
2246 // column, filled two different ways.
2247 //
2248 // The default is the problem. `CREATE TABLE IF NOT
2249 // EXISTS` leaves an upgraded table's
2250 // `DEFAULT CURRENT_TIMESTAMP` in place, and SQLite's
2251 // ALTER TABLE grammar (RENAME/ADD/DROP) has no
2252 // statement that changes it — so every insert relying
2253 // on that default wrote the legacy shape forever, while
2254 // the migrate sweep converted the rows around it. The
2255 // column went permanently mixed and `ORDER BY` inverted
2256 // the admin audit log (#1616 rework review,
2257 // correctness-001 / dialects-005 / security-002).
2258 //
2259 // Binding it here makes the stale default unreachable
2260 // rather than trying to migrate it: the timestamp comes
2261 // from the application, not the database.
2262 out.insert_pushes.push(quote! {
2263 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2264 self.#ident = #root::sql::Auto::Set(
2265 #root::__chrono::Utc::now(),
2266 );
2267 }
2268 if let #root::sql::Auto::Set(_v) = &self.#ident {
2269 _columns.push(#column);
2270 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2271 ::core::clone::Clone::clone(_v)
2272 ));
2273 }
2274 });
2275 } else {
2276 out.insert_pushes.push(quote! {
2277 if let #root::sql::Auto::Set(_v) = &self.#ident {
2278 _columns.push(#column);
2279 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2280 ::core::clone::Clone::clone(_v)
2281 ));
2282 }
2283 });
2284 }
2285 // Bulk: Auto fields appear only in the all-Set path,
2286 // never in the Unset path (we drop them from `columns`).
2287 out.bulk_columns_all.push(quote!(#column));
2288 out.bulk_pushes_all.push(quote! {
2289 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2290 ::core::clone::Clone::clone(&_row.#ident)
2291 ));
2292 });
2293 // …except the timestamp columns, which must appear in BOTH
2294 // paths (#1464). The path is chosen by the *first* Auto
2295 // field — in practice the PK — so a bulk insert of rows
2296 // with an unset PK took the no-auto branch and dropped the
2297 // timestamp column, falling back to the DB default that
2298 // this change exists to stop depending on.
2299 //
2300 // `rows` is borrowed immutably here, so this fills per row
2301 // at push time rather than writing back into the struct. An
2302 // explicitly-Set value is still honoured; only `Unset`
2303 // takes the clock.
2304 if info.auto_now_add || info.auto_now {
2305 out.bulk_columns_no_auto.push(quote!(#column));
2306 out.bulk_pushes_no_auto.push(quote! {
2307 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2308 match &_row.#ident {
2309 #root::sql::Auto::Set(_v) => ::core::clone::Clone::clone(_v),
2310 #root::sql::Auto::Unset => #root::__chrono::Utc::now(),
2311 }
2312 ));
2313 });
2314 }
2315 // Uniformity check: every row's Auto state must match the
2316 // first row's. Mixed Set/Unset within one bulk_insert is
2317 // rejected here so the column list stays consistent.
2318 let ident_clone = ident.clone();
2319 out.bulk_auto_uniformity.push(quote! {
2320 for _r in rows.iter().skip(1) {
2321 if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2322 return ::core::result::Result::Err(
2323 #root::sql::ExecError::Sql(
2324 #root::sql::SqlError::BulkAutoMixed
2325 )
2326 );
2327 }
2328 }
2329 });
2330 } else {
2331 out.insert_pushes.push(quote! {
2332 _columns.push(#column);
2333 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2334 ::core::clone::Clone::clone(&self.#ident)
2335 ));
2336 });
2337 // Bulk: non-Auto fields appear in BOTH paths.
2338 out.bulk_columns_no_auto.push(quote!(#column));
2339 out.bulk_columns_all.push(quote!(#column));
2340 let push_expr = quote! {
2341 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2342 ::core::clone::Clone::clone(&_row.#ident)
2343 ));
2344 };
2345 out.bulk_pushes_no_auto.push(push_expr.clone());
2346 out.bulk_pushes_all.push(push_expr);
2347 }
2348 if info.primary_key {
2349 if out.primary_key.is_some() {
2350 return Err(syn::Error::new_spanned(
2351 field,
2352 "only one field may be marked `#[rustango(primary_key)]`",
2353 ));
2354 }
2355 out.primary_key = Some((ident.clone(), info.column.clone()));
2356 if info.auto {
2357 out.pk_is_auto = true;
2358 }
2359 } else if info.auto_now_add {
2360 // Immutable post-insert: skip from UPDATE entirely.
2361 } else if info.auto_now {
2362 // `auto_now` columns: bind `chrono::Utc::now()` on every
2363 // UPDATE so the column is always overridden with the
2364 // wall-clock at write time, regardless of what value the
2365 // user left in the struct field.
2366 out.update_assignments.push(quote! {
2367 #root::core::Assignment::new(
2368 #column,
2369 ::core::convert::Into::<#root::core::SqlValue>::into(
2370 #root::__chrono::Utc::now()
2371 ),
2372 )
2373 });
2374 out.upsert_update_columns.push(quote!(#column));
2375 } else {
2376 out.update_assignments.push(quote! {
2377 #root::core::Assignment::new(
2378 #column,
2379 ::core::convert::Into::<#root::core::SqlValue>::into(
2380 ::core::clone::Clone::clone(&self.#ident)
2381 ),
2382 )
2383 });
2384 out.upsert_update_columns.push(quote!(#column));
2385 }
2386 out.column_entries.push(ColumnEntry {
2387 ident: ident.clone(),
2388 value_ty: info.value_ty.clone(),
2389 name: ident.to_string(),
2390 column: info.column.clone(),
2391 field_type_tokens: info.field_type_tokens,
2392 });
2393 }
2394 Ok(out)
2395}
2396
2397fn model_impl_tokens(
2398 struct_name: &syn::Ident,
2399 model_name: &str,
2400 table: &str,
2401 display: Option<&str>,
2402 app_label: Option<&str>,
2403 admin: Option<&AdminAttrs>,
2404 default_order: &[(String, bool, proc_macro2::Span)],
2405 field_schemas: &[TokenStream2],
2406 soft_delete_column: Option<&str>,
2407 permissions: bool,
2408 audit_track: Option<&[String]>,
2409 m2m_relations: &[M2MAttr],
2410 indexes: &[IndexAttr],
2411 checks: &[CheckAttr],
2412 excludes: &[ExcludeAttr],
2413 composite_fks: &[CompositeFkAttr],
2414 generic_fks: &[GenericFkAttr],
2415 scope: Option<&str>,
2416 is_view: bool,
2417 verbose_name: Option<&str>,
2418 verbose_name_plural: Option<&str>,
2419 managed: bool,
2420 base_manager_name: Option<&str>,
2421 order_with_respect_to: Option<&str>,
2422 proxy: bool,
2423 required_db_features: &[String],
2424 required_db_vendor: Option<&str>,
2425 default_related_name: Option<&str>,
2426 db_table_comment: Option<&str>,
2427 get_latest_by: Option<(&str, bool)>,
2428 extra_permissions: &[(String, String)],
2429 default_permissions: &[String],
2430 global_scopes: &[GlobalScopeAttr],
2431 reverse_has_relations: &[ReverseHasAttr],
2432 generic_has_relations: &[GenericHasAttr],
2433) -> TokenStream2 {
2434 let root = rustango_root();
2435 let display_tokens = if let Some(name) = display {
2436 quote!(::core::option::Option::Some(#name))
2437 } else {
2438 quote!(::core::option::Option::None)
2439 };
2440 let app_label_tokens = if let Some(name) = app_label {
2441 quote!(::core::option::Option::Some(#name))
2442 } else {
2443 quote!(::core::option::Option::None)
2444 };
2445 let soft_delete_tokens = if let Some(col) = soft_delete_column {
2446 quote!(::core::option::Option::Some(#col))
2447 } else {
2448 quote!(::core::option::Option::None)
2449 };
2450 let audit_track_tokens = match audit_track {
2451 None => quote!(::core::option::Option::None),
2452 Some(names) => {
2453 let lits = names.iter().map(|n| n.as_str());
2454 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2455 }
2456 };
2457 let admin_tokens = admin_config_tokens(admin);
2458 // Default `tenant` so single-tenant projects (no `scope` attr
2459 // anywhere) keep the v0.24.x behavior. Container-attr parser
2460 // already validated the value is "registry" or "tenant".
2461 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2462 Some("registry") => quote!(#root::core::ModelScope::Registry),
2463 _ => quote!(#root::core::ModelScope::Tenant),
2464 };
2465 let verbose_name_tokens = optional_str(verbose_name);
2466 let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2467 let base_manager_name_tokens = optional_str(base_manager_name);
2468 let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2469 let required_db_features_lits: Vec<&str> =
2470 required_db_features.iter().map(String::as_str).collect();
2471 let required_db_vendor_tokens = optional_str(required_db_vendor);
2472 let default_related_name_tokens = optional_str(default_related_name);
2473 let db_table_comment_tokens = optional_str(db_table_comment);
2474 let get_latest_by_tokens = match get_latest_by {
2475 Some((col, desc)) => {
2476 quote!(::core::option::Option::Some((#col, #desc)))
2477 }
2478 None => quote!(::core::option::Option::None),
2479 };
2480 let extra_permission_tokens: Vec<_> = extra_permissions
2481 .iter()
2482 .map(|(c, l)| quote!((#c, #l)))
2483 .collect();
2484 let default_permission_tokens: Vec<_> = default_permissions
2485 .iter()
2486 .map(|action| quote!(#action))
2487 .collect();
2488 let indexes_tokens = indexes.iter().map(|idx| {
2489 // When no explicit `name = "..."` was given, derive a stable,
2490 // collision-free name from the table + columns
2491 // (`<table>_<col>_<col>_idx`) instead of a shared literal that
2492 // would clash the moment a model declares two unnamed indexes.
2493 // Capped at Postgres's 63-char identifier limit.
2494 let derived_name = idx.name.clone().unwrap_or_else(|| {
2495 let mut n = format!("{table}_{}_idx", idx.columns.join("_"));
2496 if n.len() > 63 {
2497 n.truncate(63);
2498 }
2499 n
2500 });
2501 let name = derived_name.as_str();
2502 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2503 let unique = idx.unique;
2504 // Map the parsed method string onto the IndexMethod enum
2505 // variant — kept at the codegen layer so the IR doesn't
2506 // carry the string form.
2507 let method_variant = match idx.method.as_str() {
2508 "gin" => quote!(#root::core::IndexMethod::Gin),
2509 "gist" => quote!(#root::core::IndexMethod::Gist),
2510 "brin" => quote!(#root::core::IndexMethod::Brin),
2511 "spgist" => quote!(#root::core::IndexMethod::SpGist),
2512 "hash" => quote!(#root::core::IndexMethod::Hash),
2513 "bloom" => quote!(#root::core::IndexMethod::Bloom),
2514 _ => quote!(#root::core::IndexMethod::BTree),
2515 };
2516 let where_clause = match &idx.where_clause {
2517 Some(s) => quote!(::core::option::Option::Some(#s)),
2518 None => quote!(::core::option::Option::None),
2519 };
2520 let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2521 quote! {{
2522 let mut i = #root::core::IndexSchema::new(#name, &[ #(#cols),* ]);
2523 i.unique = #unique;
2524 i.method = #method_variant;
2525 i.where_clause = #where_clause;
2526 i.include = &[ #(#include_lits),* ];
2527 i
2528 }}
2529 });
2530 let checks_tokens = checks.iter().map(|c| {
2531 let name = c.name.as_str();
2532 let expr = c.expr.as_str();
2533 quote!(#root::core::CheckConstraint::new(#name, #expr))
2534 });
2535 let excludes_tokens = excludes.iter().map(|e| {
2536 let name = e.name.as_str();
2537 let using = e.using.as_str();
2538 let element_tokens = e.elements.iter().map(|(col, op)| {
2539 let col_s = col.as_str();
2540 let op_s = op.as_str();
2541 quote!((#col_s, #op_s))
2542 });
2543 let where_tokens = match e.where_clause.as_deref() {
2544 Some(w) => quote!(::core::option::Option::Some(#w)),
2545 None => quote!(::core::option::Option::None),
2546 };
2547 quote! {{
2548 let mut e = #root::core::ExclusionConstraint::new(
2549 #name,
2550 #using,
2551 &[ #(#element_tokens),* ],
2552 );
2553 e.where_clause = #where_tokens;
2554 e
2555 }}
2556 });
2557 let composite_fk_tokens = composite_fks.iter().map(|rel| {
2558 let name = rel.name.as_str();
2559 let to = rel.to.as_str();
2560 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2561 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2562 quote! {
2563 #root::core::CompositeFkRelation::new(
2564 #name,
2565 #to,
2566 &[ #(#from_cols),* ],
2567 &[ #(#on_cols),* ],
2568 )
2569 }
2570 });
2571 let generic_fk_tokens = generic_fks.iter().map(|rel| {
2572 let name = rel.name.as_str();
2573 let ct_col = rel.ct_column.as_str();
2574 let pk_col = rel.pk_column.as_str();
2575 quote!(#root::core::GenericRelation::new(#name, #ct_col, #pk_col))
2576 });
2577 // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2578 // no `#[rustango(default_order = "...")]` attribute was supplied.
2579 let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2580 let col_lit = col.as_str();
2581 quote! { (#col_lit, #desc) }
2582 });
2583
2584 // Issue #820 — `global_scopes` slice literal. Empty when no
2585 // `#[rustango(global_scope(...))]` attribute was supplied. The
2586 // `apply` path is re-emitted verbatim so it resolves in the
2587 // consumer's scope; the name is stored as a string literal.
2588 let global_scope_tokens = global_scopes.iter().map(|s| {
2589 let name = s.name.as_str();
2590 // The slice sits in a nested `const`, where `Self` does not resolve.
2591 let mut apply = s.apply.clone();
2592 if let Some(first) = apply.segments.first_mut() {
2593 if first.ident == "Self" {
2594 first.ident = struct_name.clone();
2595 }
2596 }
2597 quote!(#root::core::GlobalScope::new(#name, #apply))
2598 });
2599
2600 let m2m_tokens = m2m_relations.iter().map(|rel| {
2601 let name = rel.name.as_str();
2602 let to = rel.to.as_str();
2603 let through = rel.through.as_str();
2604 let src = rel.src.as_str();
2605 let dst = rel.dst.as_str();
2606 let auto_create = rel.auto_create;
2607 quote! {{
2608 let mut m = #root::core::M2MRelation::new(#name, #to, #through, #src, #dst);
2609 m.auto_create = #auto_create;
2610 m
2611 }}
2612 });
2613 // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2614 // when the model declares `#[rustango(reverse_has(...))]`. Each
2615 // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2616 // a const expression. Models without reverse_has fall through to
2617 // the trait's empty default — no override emitted.
2618 let reverse_relations_override = if reverse_has_relations.is_empty() {
2619 quote!()
2620 } else {
2621 let entries = reverse_has_relations.iter().map(|rel| {
2622 let name = rel.name.as_str();
2623 let child = &rel.child;
2624 let child_fk_column = rel.child_fk_column.as_str();
2625 let self_pk_column = rel.self_pk_column.as_str();
2626 quote! {
2627 #root::core::ReverseRelation::new(
2628 #name,
2629 <#child as #root::core::Model>::SCHEMA,
2630 #child_fk_column,
2631 #self_pk_column,
2632 )
2633 }
2634 });
2635 quote! {
2636 fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2637 const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2638 RELS
2639 }
2640 }
2641 };
2642 // Issue #830 — `Model::generic_reverse_relations()` override for
2643 // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2644 let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2645 quote!()
2646 } else {
2647 let entries = generic_has_relations.iter().map(|rel| {
2648 let name = rel.name.as_str();
2649 let child = &rel.child;
2650 let ct_column = rel.ct_column.as_str();
2651 let pk_column = rel.pk_column.as_str();
2652 let self_pk_column = rel.self_pk_column.as_str();
2653 quote! {
2654 #root::core::GenericReverseRelation::new(
2655 #name,
2656 <#child as #root::core::Model>::SCHEMA,
2657 #ct_column,
2658 #pk_column,
2659 #self_pk_column,
2660 )
2661 }
2662 });
2663 quote! {
2664 fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2665 const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2666 RELS
2667 }
2668 }
2669 };
2670 let fields_slice = const_slice(
2671 "e!(#root::core::FieldSchema),
2672 field_schemas.iter().cloned(),
2673 );
2674 let m2m_slice = const_slice("e!(#root::core::M2MRelation), m2m_tokens);
2675 let indexes_slice = const_slice("e!(#root::core::IndexSchema), indexes_tokens);
2676 let checks_slice = const_slice("e!(#root::core::CheckConstraint), checks_tokens);
2677 let excludes_slice = const_slice("e!(#root::core::ExclusionConstraint), excludes_tokens);
2678 let composite_fk_slice = const_slice(
2679 "e!(#root::core::CompositeFkRelation),
2680 composite_fk_tokens,
2681 );
2682 let generic_fk_slice = const_slice("e!(#root::core::GenericRelation), generic_fk_tokens);
2683 let global_scope_slice = const_slice("e!(#root::core::GlobalScope), global_scope_tokens);
2684 quote! {
2685 impl #root::core::Model for #struct_name {
2686 const SCHEMA: &'static #root::core::ModelSchema = &{
2687 let mut s = #root::core::ModelSchema::new(#model_name, #table);
2688 s.fields = #fields_slice;
2689 s.display = #display_tokens;
2690 s.app_label = #app_label_tokens;
2691 s.admin = #admin_tokens;
2692 s.soft_delete_column = #soft_delete_tokens;
2693 s.permissions = #permissions;
2694 s.audit_track = #audit_track_tokens;
2695 s.m2m = #m2m_slice;
2696 s.indexes = #indexes_slice;
2697 s.check_constraints = #checks_slice;
2698 s.exclusion_constraints = #excludes_slice;
2699 s.composite_relations = #composite_fk_slice;
2700 s.generic_relations = #generic_fk_slice;
2701 s.scope = #scope_tokens;
2702 s.default_order = &[ #(#default_order_tokens),* ];
2703 s.is_view = #is_view;
2704 s.verbose_name = #verbose_name_tokens;
2705 s.verbose_name_plural = #verbose_name_plural_tokens;
2706 s.managed = #managed;
2707 s.base_manager_name = #base_manager_name_tokens;
2708 s.order_with_respect_to = #order_with_respect_to_tokens;
2709 s.proxy = #proxy;
2710 s.required_db_features = &[ #(#required_db_features_lits),* ];
2711 s.required_db_vendor = #required_db_vendor_tokens;
2712 s.default_related_name = #default_related_name_tokens;
2713 s.db_table_comment = #db_table_comment_tokens;
2714 s.get_latest_by = #get_latest_by_tokens;
2715 s.extra_permissions = &[ #(#extra_permission_tokens),* ];
2716 s.default_permissions = &[ #(#default_permission_tokens),* ];
2717 s.global_scopes = #global_scope_slice;
2718 s
2719 };
2720
2721 #reverse_relations_override
2722 #generic_reverse_relations_override
2723 }
2724 }
2725}
2726
2727/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2728/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2729/// otherwise a static reference to a populated `AdminConfig`.
2730fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2731 let root = rustango_root();
2732 let Some(admin) = admin else {
2733 return quote!(::core::option::Option::None);
2734 };
2735
2736 let list_display = admin
2737 .list_display
2738 .as_ref()
2739 .map(|(v, _)| v.as_slice())
2740 .unwrap_or(&[]);
2741 let list_display_lits = list_display.iter().map(|s| s.as_str());
2742
2743 let search_fields = admin
2744 .search_fields
2745 .as_ref()
2746 .map(|(v, _)| v.as_slice())
2747 .unwrap_or(&[]);
2748 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2749
2750 let readonly_fields = admin
2751 .readonly_fields
2752 .as_ref()
2753 .map(|(v, _)| v.as_slice())
2754 .unwrap_or(&[]);
2755 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2756
2757 let list_filter = admin
2758 .list_filter
2759 .as_ref()
2760 .map(|(v, _)| v.as_slice())
2761 .unwrap_or(&[]);
2762 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2763
2764 let actions = admin
2765 .actions
2766 .as_ref()
2767 .map(|(v, _)| v.as_slice())
2768 .unwrap_or(&[]);
2769 let actions_lits = actions.iter().map(|s| s.as_str());
2770
2771 let fieldsets = admin
2772 .fieldsets
2773 .as_ref()
2774 .map(|(v, _)| v.as_slice())
2775 .unwrap_or(&[]);
2776 let fieldset_tokens = const_slice(
2777 "e!(#root::core::Fieldset),
2778 fieldsets.iter().map(|(title, fields)| {
2779 let title = title.as_str();
2780 let field_lits = fields.iter().map(String::as_str);
2781 quote!(#root::core::Fieldset::new(#title, &[ #( #field_lits ),* ]))
2782 }),
2783 );
2784
2785 let list_display_links = admin
2786 .list_display_links
2787 .as_ref()
2788 .map(|(v, _)| v.as_slice())
2789 .unwrap_or(&[]);
2790 let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2791
2792 let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2793 let actions_on_top = admin.actions_on_top.unwrap_or(true);
2794 let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2795 let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2796
2797 let prepopulated = admin
2798 .prepopulated_fields
2799 .as_ref()
2800 .map(|(v, _)| v.as_slice())
2801 .unwrap_or(&[]);
2802 let prepopulated_tokens = const_slice(
2803 "e!(#root::core::PrepopulatedField),
2804 prepopulated.iter().map(|(target, sources)| {
2805 let target = target.as_str();
2806 let source_lits = sources.iter().map(String::as_str);
2807 quote!(#root::core::PrepopulatedField::new(#target, &[ #( #source_lits ),* ]))
2808 }),
2809 );
2810
2811 let raw_id_fields = admin
2812 .raw_id_fields
2813 .as_ref()
2814 .map(|(v, _)| v.as_slice())
2815 .unwrap_or(&[]);
2816 let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2817
2818 let autocomplete_fields = admin
2819 .autocomplete_fields
2820 .as_ref()
2821 .map(|(v, _)| v.as_slice())
2822 .unwrap_or(&[]);
2823 let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
2824
2825 // #352 — list_select_related accepts "all" | "none" | "field, field, …".
2826 let list_select_related_tokens = match admin.list_select_related.as_deref() {
2827 None | Some("all") => quote!(#root::core::ListSelectRelated::All),
2828 Some("none") => quote!(#root::core::ListSelectRelated::None),
2829 Some(raw) => {
2830 let names: Vec<&str> = raw
2831 .split(',')
2832 .map(str::trim)
2833 .filter(|s| !s.is_empty())
2834 .collect();
2835 quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
2836 }
2837 };
2838
2839 // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
2840 // a Vec<(String, String)>. Empty / unset → no overrides.
2841 let formfield_pairs: Vec<(&str, &str)> = admin
2842 .formfield_overrides
2843 .as_ref()
2844 .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
2845 .unwrap_or_default();
2846 let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
2847 let field = *field;
2848 let widget = *widget;
2849 quote!((#field, #widget))
2850 });
2851
2852 let list_per_page = admin.list_per_page.unwrap_or(0);
2853
2854 let ordering_pairs = admin
2855 .ordering
2856 .as_ref()
2857 .map(|(v, _)| v.as_slice())
2858 .unwrap_or(&[]);
2859 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
2860 let name = name.as_str();
2861 let desc = *desc;
2862 quote!((#name, #desc))
2863 });
2864
2865 quote! {
2866 ::core::option::Option::Some({
2867 const ADMIN: &#root::core::AdminConfig = &{
2868 let mut a = #root::core::AdminConfig::DEFAULT;
2869 a.list_display = &[ #( #list_display_lits ),* ];
2870 a.search_fields = &[ #( #search_fields_lits ),* ];
2871 a.list_per_page = #list_per_page;
2872 a.ordering = &[ #( #ordering_tokens ),* ];
2873 a.readonly_fields = &[ #( #readonly_fields_lits ),* ];
2874 a.list_filter = &[ #( #list_filter_lits ),* ];
2875 a.actions = &[ #( #actions_lits ),* ];
2876 a.fieldsets = #fieldset_tokens;
2877 a.list_display_links = &[ #( #list_display_links_lits ),* ];
2878 a.search_help_text = #search_help_text;
2879 a.actions_on_top = #actions_on_top;
2880 a.actions_on_bottom = #actions_on_bottom;
2881 a.date_hierarchy = #date_hierarchy;
2882 a.prepopulated_fields = #prepopulated_tokens;
2883 a.raw_id_fields = &[ #( #raw_id_fields_lits ),* ];
2884 a.autocomplete_fields = &[ #( #autocomplete_fields_lits ),* ];
2885 a.list_select_related = #list_select_related_tokens;
2886 a.formfield_overrides = &[ #( #formfield_tokens ),* ];
2887 a
2888 };
2889 ADMIN
2890 })
2891 }
2892}
2893
2894fn inherent_impl_tokens(
2895 struct_name: &syn::Ident,
2896 fields: &CollectedFields,
2897 primary_key: Option<&(syn::Ident, String)>,
2898 column_consts: &TokenStream2,
2899 audited_fields: Option<&[&ColumnEntry]>,
2900 indexes: &[IndexAttr],
2901 manager_fns: &[syn::Ident],
2902) -> TokenStream2 {
2903 let root = rustango_root();
2904 // Audit-emit fragments threaded into write paths. Non-empty only
2905 // when the model carries `#[rustango(audit(...))]`. They reborrow
2906 // `_executor` (a `&mut PgConnection` for audited models — the
2907 // macro switches the signature below) so the data write and the
2908 // audit INSERT both run on the same caller-supplied connection.
2909 let executor_passes_to_data_write = if audited_fields.is_some() {
2910 quote!(&mut *_executor)
2911 } else {
2912 quote!(_executor)
2913 };
2914 let executor_param = if audited_fields.is_some() {
2915 quote!(_executor: &mut #root::sql::sqlx::PgConnection)
2916 } else {
2917 quote!(_executor: _E)
2918 };
2919 let executor_generics = if audited_fields.is_some() {
2920 quote!()
2921 } else {
2922 quote!(<'_c, _E>)
2923 };
2924 let executor_where = if audited_fields.is_some() {
2925 quote!()
2926 } else {
2927 quote! {
2928 where
2929 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
2930 }
2931 };
2932 // For audited models the `_on` methods take `&mut PgConnection`, so
2933 // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
2934 // must acquire a connection first. Non-audited models keep the
2935 // direct delegation since `&PgPool` IS an Executor.
2936 let pool_to_save_on = if audited_fields.is_some() {
2937 quote! {
2938 let mut _conn = pool.acquire().await?;
2939 self.save_on(&mut *_conn).await
2940 }
2941 } else {
2942 quote!(self.save_on(pool).await)
2943 };
2944 let pool_to_insert_on = if audited_fields.is_some() {
2945 quote! {
2946 let mut _conn = pool.acquire().await?;
2947 self.insert_on(&mut *_conn).await
2948 }
2949 } else {
2950 quote!(self.insert_on(pool).await)
2951 };
2952 let pool_to_delete_on = if audited_fields.is_some() {
2953 quote! {
2954 let mut _conn = pool.acquire().await?;
2955 self.delete_on(&mut *_conn).await
2956 }
2957 } else {
2958 quote!(self.delete_on(pool).await)
2959 };
2960 let pool_to_bulk_insert_on = if audited_fields.is_some() {
2961 quote! {
2962 let mut _conn = pool.acquire().await?;
2963 Self::bulk_insert_on(rows, &mut *_conn).await
2964 }
2965 } else {
2966 quote!(Self::bulk_insert_on(rows, pool).await)
2967 };
2968 // Pre-existing bug surfaced by batch 22's first audited Auto<T>
2969 // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
2970 // directly, but `upsert_on` for audited models takes
2971 // `&mut PgConnection` (the audit emit needs a real connection).
2972 // Add the missing acquire shim to keep audited Auto-PK upsert
2973 // compiling.
2974 let pool_to_upsert_on = if audited_fields.is_some() {
2975 quote! {
2976 let mut _conn = pool.acquire().await?;
2977 self.upsert_on(&mut *_conn).await
2978 }
2979 } else {
2980 quote!(self.upsert_on(pool).await)
2981 };
2982
2983 // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
2984 // (audit-on-connection over &Pool needs a bi-dialect transaction
2985 // helper, deferred). Two body shapes:
2986 // - has_auto: build InsertQuery skipping Auto::Unset columns,
2987 // request Auto cols in `returning`, dispatch via
2988 // `insert_returning_pool`, then on the returned `PgRow` /
2989 // `MySqlAutoId(id)` enum — pull each Auto field from the PG
2990 // row OR drop the single i64 into the first Auto field on MySQL
2991 // (multi-Auto models on MySQL error at runtime since
2992 // `LAST_INSERT_ID()` only reports one)
2993 // - non-Auto: build InsertQuery with explicit columns/values and
2994 // call `insert_pool` (no returning needed)
2995 // pool_insert_method body for the audited Auto-PK case is moved
2996 // to after audit_pair_tokens / audit_pk_to_string (they live
2997 // ~150 lines below). This block keeps the non-audited and
2998 // non-Auto branches in place — the audited Auto-PK arm is
2999 // computed below and merged via the dispatch helper variable.
3000 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
3001 // Audited models with explicit (non-Auto) PKs go through
3002 // the non-Auto insert path below — the audit emit is one
3003 // round-trip after the INSERT inside the same tx via
3004 // audit::save_one_with_audit? No, INSERT semantics
3005 // differ. For non-Auto PK + audited, route through a
3006 // dedicated insert + audit emit on the same tx, but defer
3007 // the macro emission to the audit-bundle-aware block below
3008 // — this `quote!()` placeholder gets overwritten there.
3009 quote!()
3010 } else if audited_fields.is_some() && fields.has_auto {
3011 // Audited Auto-PK insert_pool — assembled after the audit
3012 // bundles. Placeholder; real emission below.
3013 quote!()
3014 } else if fields.has_auto {
3015 let pushes = &fields.insert_pushes;
3016 let returning_cols = &fields.returning_cols;
3017 // When every `Auto<T>` field is filled Rust-side
3018 // (`default_uuid_v7`, issue #823), there is no column to read
3019 // back from the database — `returning_cols` is empty. Route
3020 // through plain `insert_pool` instead of
3021 // `insert_returning_pool` to skip the redundant RETURNING /
3022 // LAST_INSERT_ID round-trip.
3023 if fields.returning_cols.is_empty() {
3024 quote! {
3025 /// Insert this row against either backend. Every
3026 /// `Auto<T>` PK on this model is filled Rust-side
3027 /// (e.g. `default_uuid_v7`) before binding, so no
3028 /// RETURNING round-trip is needed.
3029 ///
3030 /// # Errors
3031 /// As [`Self::insert`].
3032 pub async fn insert_pool(
3033 &mut self,
3034 pool: &#root::sql::Pool,
3035 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3036 let mut _columns: ::std::vec::Vec<&'static str> =
3037 ::std::vec::Vec::new();
3038 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3039 ::std::vec::Vec::new();
3040 #( #pushes )*
3041 let _query = #root::core::InsertQuery::new(
3042 <Self as #root::core::Model>::SCHEMA,
3043 _columns,
3044 _values,
3045 );
3046 #root::sql::insert_pool(pool, &_query).await
3047 }
3048
3049 /// Eloquent `Model::insertOrIgnore()` — same shape
3050 /// as the auto-PK branch above. Returns `Ok(true)`
3051 /// when inserted, `Ok(false)` when a conflict caused
3052 /// the INSERT to silently skip.
3053 ///
3054 /// # Errors
3055 /// As [`Self::insert`].
3056 pub async fn insert_or_ignore(
3057 &mut self,
3058 pool: &#root::sql::Pool,
3059 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3060 let mut _columns: ::std::vec::Vec<&'static str> =
3061 ::std::vec::Vec::new();
3062 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3063 ::std::vec::Vec::new();
3064 #( #pushes )*
3065 let _query = #root::core::InsertQuery::new(
3066 <Self as #root::core::Model>::SCHEMA,
3067 _columns,
3068 _values,
3069 )
3070 .on_conflict(#root::core::ConflictClause::DoNothing);
3071 let dialect = pool.dialect();
3072 let stmt = dialect.compile_insert(&_query)?;
3073 let rows = #root::sql::raw_execute_pool(
3074 pool, &stmt.sql, stmt.params,
3075 ).await?;
3076 ::core::result::Result::Ok(rows > 0)
3077 }
3078 }
3079 } else {
3080 quote! {
3081 /// Insert this row against either backend, populating any
3082 /// `Auto<T>` PK from the auto-assigned value.
3083 ///
3084 /// # Errors
3085 /// As [`Self::insert`].
3086 pub async fn insert_pool(
3087 &mut self,
3088 pool: &#root::sql::Pool,
3089 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3090 let mut _columns: ::std::vec::Vec<&'static str> =
3091 ::std::vec::Vec::new();
3092 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3093 ::std::vec::Vec::new();
3094 #( #pushes )*
3095 let _query = #root::core::InsertQuery::new(
3096 <Self as #root::core::Model>::SCHEMA,
3097 _columns,
3098 _values,
3099 )
3100 .returning(::std::vec![ #( #returning_cols ),* ]);
3101 let _result = #root::sql::insert_returning_pool(
3102 pool, &_query,
3103 ).await?;
3104 #root::sql::apply_auto_pk(_result, self)
3105 }
3106
3107 /// Eloquent `Model::insertOrIgnore()` — INSERT this
3108 /// row or silently skip on unique-constraint
3109 /// violation. Returns `Ok(true)` when a row was
3110 /// inserted, `Ok(false)` when a conflict caused the
3111 /// INSERT to silently skip.
3112 ///
3113 /// **Caveat on auto-PK models**: when the row is
3114 /// skipped (conflict), this instance's `Auto<T>`
3115 /// fields stay `Unset` — no PK is back-populated
3116 /// because the server didn't auto-assign one. For
3117 /// "insert then read back the PK or the existing
3118 /// row's PK", use the `upsert` family or
3119 /// `get_or_create`.
3120 ///
3121 /// # Errors
3122 /// As [`Self::insert`].
3123 pub async fn insert_or_ignore(
3124 &mut self,
3125 pool: &#root::sql::Pool,
3126 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3127 let mut _columns: ::std::vec::Vec<&'static str> =
3128 ::std::vec::Vec::new();
3129 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3130 ::std::vec::Vec::new();
3131 #( #pushes )*
3132 let _query = #root::core::InsertQuery::new(
3133 <Self as #root::core::Model>::SCHEMA,
3134 _columns,
3135 _values,
3136 )
3137 .on_conflict(#root::core::ConflictClause::DoNothing);
3138 let dialect = pool.dialect();
3139 let stmt = dialect.compile_insert(&_query)?;
3140 let rows = #root::sql::raw_execute_pool(
3141 pool, &stmt.sql, stmt.params,
3142 ).await?;
3143 ::core::result::Result::Ok(rows > 0)
3144 }
3145 }
3146 }
3147 } else {
3148 let insert_columns = &fields.insert_columns;
3149 let insert_values = &fields.insert_values;
3150 quote! {
3151 /// Insert this row into its table against either backend.
3152 /// Equivalent to [`Self::insert`] but takes
3153 /// [`#root::sql::Pool`].
3154 ///
3155 /// # Errors
3156 /// As [`Self::insert`].
3157 pub async fn insert_pool(
3158 &self,
3159 pool: &#root::sql::Pool,
3160 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3161 let _query = #root::core::InsertQuery::new(
3162 <Self as #root::core::Model>::SCHEMA,
3163 ::std::vec![ #( #insert_columns ),* ],
3164 ::std::vec![ #( #insert_values ),* ],
3165 );
3166 #root::sql::insert_pool(pool, &_query).await
3167 }
3168
3169 /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3170 /// or silently skip on unique-constraint violation. Maps
3171 /// to per-dialect "INSERT ... DO NOTHING on conflict":
3172 /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3173 /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL
3174 /// `INSERT IGNORE INTO …`.
3175 ///
3176 /// Returns `Ok(true)` when a row was inserted,
3177 /// `Ok(false)` when a conflict caused the INSERT to
3178 /// silently skip.
3179 ///
3180 /// Use for "create-if-absent" patterns where you don't
3181 /// need the row back. For "find-or-create with the row
3182 /// returned", use the queryset-level
3183 /// `crate::sql::get_or_create` free function.
3184 ///
3185 /// # Errors
3186 /// As [`Self::insert`], plus any dialect-specific
3187 /// translation error from the ConflictClause writer.
3188 pub async fn insert_or_ignore(
3189 &self,
3190 pool: &#root::sql::Pool,
3191 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3192 let _query = #root::core::InsertQuery::new(
3193 <Self as #root::core::Model>::SCHEMA,
3194 ::std::vec![ #( #insert_columns ),* ],
3195 ::std::vec![ #( #insert_values ),* ],
3196 )
3197 .on_conflict(#root::core::ConflictClause::DoNothing);
3198 let dialect = pool.dialect();
3199 let stmt = dialect.compile_insert(&_query)?;
3200 let rows = #root::sql::raw_execute_pool(pool, &stmt.sql, stmt.params).await?;
3201 ::core::result::Result::Ok(rows > 0)
3202 }
3203 }
3204 };
3205
3206 // pool_save_method moved to after audit_pair_tokens /
3207 // audit_pk_to_string (they live ~70 lines below) — needed for
3208 // the audited branch which builds an UpdateQuery + PendingEntry
3209 // and dispatches via audit::save_one_with_audit.
3210
3211 // pool_delete_method moved to after audit_pair_tokens / audit_pk_to_string
3212 // are computed (they live ~80 lines below).
3213
3214 // Build the (column, JSON value) pair list used by every
3215 // snapshot-style audit emission. Reused across delete_on,
3216 // soft_delete_on, restore_on, and (later) bulk paths. Empty
3217 // when the model isn't audited.
3218 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3219 .map(|tracked| {
3220 tracked
3221 .iter()
3222 .map(|c| {
3223 let column_lit = c.column.as_str();
3224 let ident = &c.ident;
3225 quote! {
3226 (
3227 #column_lit,
3228 #root::__serde_json::to_value(&self.#ident)
3229 .unwrap_or(#root::__serde_json::Value::Null),
3230 )
3231 }
3232 })
3233 .collect()
3234 })
3235 .unwrap_or_default();
3236 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3237 if fields.pk_is_auto {
3238 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3239 } else {
3240 quote!(::std::format!("{}", &self.#pk_ident))
3241 }
3242 } else {
3243 quote!(::std::string::String::new())
3244 };
3245 // `__rustango_audit_entry`: the one place a snapshot `PendingEntry`
3246 // is built, so every audited write path records the same shape.
3247 let audit_entry_method = if audited_fields.is_some() {
3248 let pairs = audit_pair_tokens.iter();
3249 let pk_str = audit_pk_to_string.clone();
3250 quote! {
3251 /// Snapshot audit entry for `operation` on this row.
3252 #[doc(hidden)]
3253 pub fn __rustango_audit_entry(
3254 &self,
3255 operation: #root::audit::AuditOp,
3256 ) -> #root::audit::PendingEntry {
3257 #root::audit::PendingEntry {
3258 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3259 entity_pk: #pk_str,
3260 operation,
3261 source: #root::audit::current_source(),
3262 changes: #root::audit::snapshot_changes(&[
3263 #( #pairs ),*
3264 ]),
3265 }
3266 }
3267 }
3268 } else {
3269 quote!()
3270 };
3271 // Builds `_audit_entry`; `written` is the soft-delete column's new
3272 // value, recorded in place of the stale `self` field.
3273 let make_entry = |op_path: TokenStream2, written: Option<TokenStream2>| -> TokenStream2 {
3274 if let (Some(v), Some(col)) = (written, fields.soft_delete_column.as_deref()) {
3275 quote! {
3276 let mut _audit_entry = self.__rustango_audit_entry(#op_path);
3277 _audit_entry.set_tracked(#col, #v);
3278 }
3279 } else {
3280 quote!(let _audit_entry = self.__rustango_audit_entry(#op_path);)
3281 }
3282 };
3283 let soft_deleted_json = quote!(#root::__serde_json::to_value(&_now)
3284 .unwrap_or(#root::__serde_json::Value::Null));
3285 let restored_json = quote!(#root::__serde_json::Value::Null);
3286 // `_on` emit; `affected` paths skip the row when nothing changed.
3287 let make_op_emit =
3288 |op_path: TokenStream2, written: Option<TokenStream2>, affected: bool| -> TokenStream2 {
3289 if audited_fields.is_none() {
3290 return quote!();
3291 }
3292 let entry = make_entry(op_path, written);
3293 let emit = quote! {
3294 #entry
3295 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3296 };
3297 if affected {
3298 quote!(if _affected > 0 { #emit })
3299 } else {
3300 emit
3301 }
3302 };
3303 let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create), None, false);
3304 let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete), None, true);
3305 let audit_softdelete_emit = make_op_emit(
3306 quote!(#root::audit::AuditOp::SoftDelete),
3307 Some(soft_deleted_json.clone()),
3308 true,
3309 );
3310 let audit_restore_emit = make_op_emit(
3311 quote!(#root::audit::AuditOp::Restore),
3312 Some(restored_json.clone()),
3313 true,
3314 );
3315 // `&Pool` update runner for soft_delete / restore: one tx with the
3316 // audit row when audited, a plain UPDATE otherwise.
3317 let make_pool_update = |op_path: TokenStream2, written: TokenStream2| -> TokenStream2 {
3318 if audited_fields.is_some() {
3319 let entry = make_entry(op_path, Some(written));
3320 quote! {
3321 #entry
3322 #root::audit::save_one_with_audit(pool, &_query, &_audit_entry).await
3323 }
3324 } else {
3325 quote!(#root::sql::update_pool(pool, &_query).await)
3326 }
3327 };
3328 let pool_softdelete_run =
3329 make_pool_update(quote!(#root::audit::AuditOp::SoftDelete), soft_deleted_json);
3330 let pool_restore_run = make_pool_update(quote!(#root::audit::AuditOp::Restore), restored_json);
3331
3332 // `save_pool(&Pool)` — emitted for every model with a PK.
3333 // Audited Auto-PK models are deferred (the Auto::Unset →
3334 // insert_pool path needs the audited-insert flow from a future
3335 // batch). Three body shapes:
3336 // - non-audited, plain PK: build UpdateQuery + dispatch through
3337 // sql::update_pool
3338 // - non-audited, Auto-PK: same, but Auto::Unset routes to
3339 // self.insert_pool which already handles RETURNING / LAST_INSERT_ID
3340 // - audited, plain PK: build UpdateQuery + PendingEntry, dispatch
3341 // through audit::save_one_with_audit (per-backend tx wraps
3342 // UPDATE + audit emit atomically). Snapshot-style audit (post-
3343 // write field values) — diff-style audit (with pre-UPDATE
3344 // SELECT for `before` values) needs per-tracked-column codegen
3345 // that doesn't fit the runtime-helper pattern; legacy &PgPool
3346 // `save` keeps the diff for now.
3347 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3348 let pk_column_lit = pk_col.as_str();
3349 let assignments = &fields.update_assignments;
3350 if audited_fields.is_some() {
3351 if fields.pk_is_auto {
3352 // Auto-PK + audited: defer. The Auto::Unset insert
3353 // path needs a transactional INSERT + LAST_INSERT_ID
3354 // + audit emit flow — that's a follow-up batch.
3355 quote!()
3356 } else {
3357 let pairs = audit_pair_tokens.iter();
3358 let pairs2 = audit_pair_tokens.iter();
3359 let pk_str = audit_pk_to_string.clone();
3360 let pk_str2 = audit_pk_to_string.clone();
3361 quote! {
3362 /// Save (UPDATE) this row against either backend
3363 /// with audit emission inside the same transaction.
3364 /// Bi-dialect counterpart of [`Self::save`] for
3365 /// audited models with non-`Auto<T>` PKs.
3366 ///
3367 /// Captures **post-write** field state (snapshot
3368 /// audit). The legacy &PgPool [`Self::save`]
3369 /// captures BEFORE+AFTER for true diff audit;
3370 /// porting that to the &Pool path needs runtime
3371 /// per-tracked-column decoding and is deferred.
3372 ///
3373 /// # Errors
3374 /// As [`Self::save`].
3375 pub async fn save_pool(
3376 &mut self,
3377 pool: &#root::sql::Pool,
3378 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3379 let _query = #root::core::UpdateQuery::new(
3380 <Self as #root::core::Model>::SCHEMA,
3381 ::std::vec![ #( #assignments ),* ],
3382 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3383 #pk_column_lit,
3384 #root::core::Op::Eq,
3385 ::core::convert::Into::<#root::core::SqlValue>::into(
3386 ::core::clone::Clone::clone(&self.#pk_ident)
3387 ),
3388 )),
3389 );
3390 let _audit_entry = #root::audit::PendingEntry {
3391 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3392 entity_pk: #pk_str,
3393 operation: #root::audit::AuditOp::Update,
3394 source: #root::audit::current_source(),
3395 changes: #root::audit::snapshot_changes(&[
3396 #( #pairs ),*
3397 ]),
3398 };
3399 let _affected = #root::audit::save_one_with_audit(
3400 pool, &_query, &_audit_entry,
3401 ).await?;
3402 ::core::result::Result::Ok(_affected)
3403 }
3404
3405 /// `save_pool` narrowed to a Rust-field allowlist — issue #66.
3406 /// Audit emission shrinks to the same column set so
3407 /// the audit log reflects exactly what was written.
3408 ///
3409 /// # Errors
3410 /// As [`Self::save_pool`], plus
3411 /// [`#root::core::QueryError::UnknownField`] wrapped
3412 /// in `ExecError::Query` for unknown field names.
3413 pub async fn save_partial(
3414 &mut self,
3415 fields: &[&str],
3416 pool: &#root::sql::Pool,
3417 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3418 if fields.is_empty() {
3419 #root::__tracing::warn!(
3420 target: "rustango::save_partial",
3421 model = <Self as #root::core::Model>::SCHEMA.name,
3422 "save_partial called with empty field list — no-op"
3423 );
3424 return ::core::result::Result::Ok(0);
3425 }
3426 let _schema = <Self as #root::core::Model>::SCHEMA;
3427 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3428 ::std::collections::HashSet::with_capacity(fields.len());
3429 for f in fields {
3430 match _schema.field(f) {
3431 ::core::option::Option::Some(fs) => {
3432 _wanted_cols.insert(fs.column);
3433 }
3434 ::core::option::Option::None => {
3435 return ::core::result::Result::Err(
3436 #root::sql::ExecError::Query(
3437 #root::core::QueryError::UnknownField {
3438 model: _schema.name,
3439 field: (*f).to_owned(),
3440 }
3441 )
3442 );
3443 }
3444 }
3445 }
3446 let _full: ::std::vec::Vec<#root::core::Assignment> =
3447 ::std::vec![ #( #assignments ),* ];
3448 let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3449 .into_iter()
3450 .filter(|a| _wanted_cols.contains(a.column))
3451 .collect();
3452 if _filtered.is_empty() {
3453 #root::__tracing::warn!(
3454 target: "rustango::save_partial",
3455 model = _schema.name,
3456 "save_partial: every named field maps to a non-assignable column — no-op"
3457 );
3458 return ::core::result::Result::Ok(0);
3459 }
3460 let _query = #root::core::UpdateQuery::new(
3461 _schema,
3462 _filtered,
3463 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3464 #pk_column_lit,
3465 #root::core::Op::Eq,
3466 ::core::convert::Into::<#root::core::SqlValue>::into(
3467 ::core::clone::Clone::clone(&self.#pk_ident)
3468 ),
3469 )),
3470 );
3471 // Narrow the audit snapshot to the same column set.
3472 let _all_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3473 ::std::vec![ #( #pairs2 ),* ];
3474 let _narrowed: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3475 _all_pairs
3476 .into_iter()
3477 .filter(|(col, _)| _wanted_cols.contains(col))
3478 .collect();
3479 let _audit_entry = #root::audit::PendingEntry {
3480 entity_table: _schema.table,
3481 entity_pk: #pk_str2,
3482 operation: #root::audit::AuditOp::Update,
3483 source: #root::audit::current_source(),
3484 changes: #root::audit::snapshot_changes(&_narrowed),
3485 };
3486 let _affected = #root::audit::save_one_with_audit(
3487 pool, &_query, &_audit_entry,
3488 ).await?;
3489 ::core::result::Result::Ok(_affected)
3490 }
3491
3492 /// Typed-column counterpart of [`Self::save_partial`] —
3493 /// issue #67. `fields` is a tuple of [`Column`]
3494 /// constants whose `Model` matches `Self`; typos and
3495 /// model mismatches surface at *compile time*
3496 /// (`Author::name` inside a `Post::save_partial_typed`
3497 /// call is a type error, no runtime check).
3498 ///
3499 /// ```ignore
3500 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3501 /// ```
3502 ///
3503 /// Lowers to [`Self::save_partial`] under the hood;
3504 /// audit narrowing + every other semantic is identical.
3505 ///
3506 /// [`Column`]: #root::core::Column
3507 ///
3508 /// # Errors
3509 /// As [`Self::save_partial`].
3510 pub async fn save_partial_typed<
3511 L: #root::core::TypedFieldList<Self>,
3512 >(
3513 &mut self,
3514 fields: L,
3515 pool: &#root::sql::Pool,
3516 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3517 let _names = fields.rust_field_names();
3518 let _refs: ::std::vec::Vec<&str> =
3519 _names.iter().copied().collect();
3520 self.save_partial(&_refs, pool).await
3521 }
3522 }
3523 }
3524 } else {
3525 let dispatch_unset = if fields.pk_is_auto {
3526 quote! {
3527 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3528 return self.insert_pool(pool).await.map(|()| 1u64);
3529 }
3530 }
3531 } else {
3532 quote!()
3533 };
3534 quote! {
3535 /// Save this row to its table against either backend.
3536 /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3537 /// `UPDATE` keyed on the PK.
3538 ///
3539 /// # Errors
3540 /// As [`Self::save`].
3541 pub async fn save_pool(
3542 &mut self,
3543 pool: &#root::sql::Pool,
3544 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3545 #dispatch_unset
3546 let _query = #root::core::UpdateQuery::new(
3547 <Self as #root::core::Model>::SCHEMA,
3548 ::std::vec![ #( #assignments ),* ],
3549 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3550 #pk_column_lit,
3551 #root::core::Op::Eq,
3552 ::core::convert::Into::<#root::core::SqlValue>::into(
3553 ::core::clone::Clone::clone(&self.#pk_ident)
3554 ),
3555 )),
3556 );
3557 let _affected = #root::sql::update_pool(pool, &_query).await?;
3558 ::core::result::Result::Ok(_affected)
3559 }
3560
3561 /// Save (UPDATE) only the listed Rust-side fields,
3562 /// leaving every other column untouched. Issue #66.
3563 ///
3564 /// `fields` are Rust-side struct field names; the macro
3565 /// resolves each to its SQL column. Unknown field
3566 /// names return [`#root::core::QueryError::UnknownField`]
3567 /// wrapped in `ExecError::Query`. An empty list is a
3568 /// no-op (returns `Ok(())` and logs a `tracing::warn!`).
3569 ///
3570 /// Use this when:
3571 /// * you only mutated a couple of fields on a wide row
3572 /// (avoid re-writing every column on every save), or
3573 /// * two writers diverged after their initial read and
3574 /// you want to preserve the other writer's changes to
3575 /// columns you didn't touch.
3576 ///
3577 /// Auto-PK models with an unset PK return
3578 /// [`#root::core::QueryError::UnknownField`] with
3579 /// field name `<pk>` — `save_partial` is an
3580 /// UPDATE-only path. Call [`Self::insert_pool`]
3581 /// (or [`Self::save_pool`] which dispatches based on
3582 /// PK state) for the INSERT case.
3583 ///
3584 /// # Errors
3585 /// As [`Self::save_pool`], plus `UnknownField` for
3586 /// unknown / empty / Auto-Unset cases.
3587 pub async fn save_partial(
3588 &mut self,
3589 fields: &[&str],
3590 pool: &#root::sql::Pool,
3591 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3592 if fields.is_empty() {
3593 #root::__tracing::warn!(
3594 target: "rustango::save_partial",
3595 model = <Self as #root::core::Model>::SCHEMA.name,
3596 "save_partial called with empty field list — no-op"
3597 );
3598 return ::core::result::Result::Ok(0);
3599 }
3600 let _schema = <Self as #root::core::Model>::SCHEMA;
3601 // Validate field names against the schema.
3602 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3603 ::std::collections::HashSet::with_capacity(fields.len());
3604 for f in fields {
3605 match _schema.field(f) {
3606 ::core::option::Option::Some(fs) => {
3607 _wanted_cols.insert(fs.column);
3608 }
3609 ::core::option::Option::None => {
3610 return ::core::result::Result::Err(
3611 #root::sql::ExecError::Query(
3612 #root::core::QueryError::UnknownField {
3613 model: _schema.name,
3614 field: (*f).to_owned(),
3615 }
3616 )
3617 );
3618 }
3619 }
3620 }
3621 // Build the full assignment vec, then keep only the
3622 // assignments whose column is in `_wanted_cols`.
3623 let _full: ::std::vec::Vec<#root::core::Assignment> =
3624 ::std::vec![ #( #assignments ),* ];
3625 let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3626 .into_iter()
3627 .filter(|a| _wanted_cols.contains(a.column))
3628 .collect();
3629 if _filtered.is_empty() {
3630 // All field names valid, but they all map to
3631 // non-assignable slots (PK column, computed/
3632 // virtual fields, relations without an
3633 // assignment). Same no-op as an empty list.
3634 #root::__tracing::warn!(
3635 target: "rustango::save_partial",
3636 model = _schema.name,
3637 "save_partial: every named field maps to a non-assignable column — no-op"
3638 );
3639 return ::core::result::Result::Ok(0);
3640 }
3641 let _query = #root::core::UpdateQuery::new(
3642 _schema,
3643 _filtered,
3644 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3645 #pk_column_lit,
3646 #root::core::Op::Eq,
3647 ::core::convert::Into::<#root::core::SqlValue>::into(
3648 ::core::clone::Clone::clone(&self.#pk_ident)
3649 ),
3650 )),
3651 );
3652 let _affected = #root::sql::update_pool(pool, &_query).await?;
3653 ::core::result::Result::Ok(_affected)
3654 }
3655
3656 /// Typed-column counterpart of [`Self::save_partial`] —
3657 /// issue #67. `fields` is a tuple of [`Column`]
3658 /// constants whose `Model` matches `Self`; typos and
3659 /// model mismatches surface at *compile time*
3660 /// (`Author::name` inside a `Post::save_partial_typed`
3661 /// call is a type error, no runtime check).
3662 ///
3663 /// ```ignore
3664 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3665 /// ```
3666 ///
3667 /// Lowers to [`Self::save_partial`] under the hood — the
3668 /// tuple is reduced to a `&[&str]` slice of Rust-side
3669 /// field names and forwarded.
3670 ///
3671 /// [`Column`]: #root::core::Column
3672 ///
3673 /// # Errors
3674 /// As [`Self::save_partial`].
3675 pub async fn save_partial_typed<
3676 L: #root::core::TypedFieldList<Self>,
3677 >(
3678 &mut self,
3679 fields: L,
3680 pool: &#root::sql::Pool,
3681 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3682 let _names = fields.rust_field_names();
3683 let _refs: ::std::vec::Vec<&str> =
3684 _names.iter().copied().collect();
3685 self.save_partial(&_refs, pool).await
3686 }
3687 }
3688 }
3689 } else {
3690 quote!()
3691 };
3692
3693 // Audited `insert_pool` (overrides the placeholder set higher up
3694 // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3695 // audited models get insert_pool routing through
3696 // audit::insert_one_with_audit (per-backend tx wraps INSERT
3697 // + auto-PK readback + audit emit). Snapshot-style audit (the
3698 // PendingEntry's `changes` carries post-write field values).
3699 let pool_insert_method = if audited_fields.is_some() {
3700 if let Some(_) = primary_key {
3701 let pushes = if fields.has_auto {
3702 fields.insert_pushes.clone()
3703 } else {
3704 // For non-Auto-PK models, the macro normally builds
3705 // {columns, values} from fields.insert_columns +
3706 // fields.insert_values rather than insert_pushes.
3707 // Map those into the pushes shape.
3708 fields
3709 .insert_columns
3710 .iter()
3711 .zip(&fields.insert_values)
3712 .map(|(col, val)| {
3713 quote! {
3714 _columns.push(#col);
3715 _values.push(#val);
3716 }
3717 })
3718 .collect()
3719 };
3720 let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
3721 fields.returning_cols.clone()
3722 } else {
3723 // Non-Auto-PK: still need RETURNING something for the
3724 // audit helper's contract (it errors on empty
3725 // returning). Return the PK column so the audit row
3726 // can carry the assigned PK back. Some non-Auto PKs
3727 // are server-side-default (e.g. UUIDv4 default), so
3728 // RETURNING is genuinely useful.
3729 primary_key
3730 .map(|(_, col)| {
3731 let lit = col.as_str();
3732 vec![quote!(#lit)]
3733 })
3734 .unwrap_or_default()
3735 };
3736 quote! {
3737 /// Insert this row against either backend with audit
3738 /// emission inside the same transaction. Bi-dialect
3739 /// counterpart of [`Self::insert`] for audited models.
3740 ///
3741 /// Snapshot-style audit (post-write field values).
3742 ///
3743 /// # Errors
3744 /// As [`Self::insert`].
3745 pub async fn insert_pool(
3746 &mut self,
3747 pool: &#root::sql::Pool,
3748 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3749 let mut _columns: ::std::vec::Vec<&'static str> =
3750 ::std::vec::Vec::new();
3751 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3752 ::std::vec::Vec::new();
3753 #( #pushes )*
3754 let _query = #root::core::InsertQuery::new(
3755 <Self as #root::core::Model>::SCHEMA,
3756 _columns,
3757 _values,
3758 )
3759 .returning(::std::vec![ #( #returning_cols ),* ]);
3760 #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self| {
3761 _m.__rustango_audit_entry(#root::audit::AuditOp::Create)
3762 })
3763 .await
3764 }
3765 }
3766 } else {
3767 quote!()
3768 }
3769 } else {
3770 // Keep the non-audited pool_insert_method we built earlier.
3771 pool_insert_method
3772 };
3773
3774 // Update audited save_pool: now that insert_pool is wired for
3775 // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3776 // insert_pool. Non-audited save_pool already does this.
3777 // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3778 // Replaces the snapshot-only emission with a per-backend transaction
3779 // body that:
3780 // 1. SELECTs the tracked columns by PK (typed Row::try_get per
3781 // column), capturing BEFORE values
3782 // 2. compiles the UPDATE via pool.dialect() and runs it on the tx
3783 // 3. builds AFTER pairs from &self
3784 // 4. diffs BEFORE/AFTER, emits one PendingEntry with
3785 // AuditOp::Update + diff_changes(...) on the same tx connection
3786 // 5. commits
3787 //
3788 // Per-backend arms inline the SQL string + placeholder shape, then
3789 // share the `audit_before_pair_tokens` decoder block (Row::try_get
3790 // is polymorphic over Row type — the same tokens work against
3791 // PgRow and MySqlRow as long as the field's Rust type implements
3792 // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3793 // primitives + chrono/uuid/serde_json::Value all do).
3794 let pool_save_method = if let Some(tracked) = audited_fields {
3795 if let Some((pk_ident, pk_col)) = primary_key {
3796 let pk_column_lit = pk_col.as_str();
3797 // Two iterators — quote!'s `#(#var)*` consumes the
3798 // iterator, and we need to splice the same after-pairs
3799 // sequence into both per-backend arms.
3800 let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
3801 let pk_str = audit_pk_to_string.clone();
3802 // Per-tracked-column BEFORE-pair token list. Each entry
3803 // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3804 // The Row alias resolves to PgRow / MySqlRow per call site,
3805 // so the same template generates both the PG and MySQL bodies.
3806 let mk_before_pairs =
3807 |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
3808 tracked
3809 .iter()
3810 .map(|c| {
3811 let column_lit = c.column.as_str();
3812 let value_ty = &c.value_ty;
3813 quote! {
3814 (
3815 #column_lit,
3816 match #getter::<#value_ty>(
3817 _audit_before_row, #column_lit,
3818 ) {
3819 ::core::result::Result::Ok(v) => {
3820 #root::__serde_json::to_value(&v)
3821 .unwrap_or(#root::__serde_json::Value::Null)
3822 }
3823 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3824 },
3825 )
3826 }
3827 })
3828 .collect()
3829 };
3830 let before_pairs_pg: Vec<proc_macro2::TokenStream> =
3831 mk_before_pairs(quote!(#root::sql::try_get_returning));
3832 let before_pairs_my: Vec<proc_macro2::TokenStream> =
3833 mk_before_pairs(quote!(#root::sql::try_get_returning_my));
3834 let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
3835 mk_before_pairs(quote!(#root::sql::try_get_returning_sqlite));
3836 let pg_select_cols: String = tracked
3837 .iter()
3838 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
3839 .collect::<Vec<_>>()
3840 .join(", ");
3841 let my_select_cols: String = tracked
3842 .iter()
3843 .map(|c| format!("`{}`", c.column.replace('`', "``")))
3844 .collect::<Vec<_>>()
3845 .join(", ");
3846 // SQLite uses double-quote identifier quoting (same as
3847 // Postgres in default config), so the column-list shape
3848 // matches PG.
3849 let sqlite_select_cols: String = pg_select_cols.clone();
3850 let pk_value_for_bind = if fields.pk_is_auto {
3851 quote!(self.#pk_ident.get().copied().unwrap_or_default())
3852 } else {
3853 quote!(::core::clone::Clone::clone(&self.#pk_ident))
3854 };
3855 let assignments = &fields.update_assignments;
3856 let unset_dispatch = if fields.has_auto {
3857 quote! {
3858 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3859 return self.insert_pool(pool).await.map(|()| 1u64);
3860 }
3861 }
3862 } else {
3863 quote!()
3864 };
3865 quote! {
3866 /// Save this row against either backend with audit
3867 /// emission (diff-style: BEFORE+AFTER) inside the
3868 /// same transaction. Auto::Unset PK routes to
3869 /// insert_pool. Bi-dialect counterpart of
3870 /// [`Self::save`] for audited models.
3871 ///
3872 /// The audit row's `changes` JSON contains one
3873 /// `{ "field": { "before": …, "after": … } }` entry
3874 /// per tracked column whose value actually changed
3875 /// — same shape as the existing &PgPool save() emits.
3876 ///
3877 /// # Errors
3878 /// As [`Self::save`].
3879 pub async fn save_pool(
3880 &mut self,
3881 pool: &#root::sql::Pool,
3882 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3883 #unset_dispatch
3884 let _query = #root::core::UpdateQuery::new(
3885 <Self as #root::core::Model>::SCHEMA,
3886 ::std::vec![ #( #assignments ),* ],
3887 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3888 #pk_column_lit,
3889 #root::core::Op::Eq,
3890 ::core::convert::Into::<#root::core::SqlValue>::into(
3891 ::core::clone::Clone::clone(&self.#pk_ident)
3892 ),
3893 )),
3894 );
3895 let _after_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3896 ::std::vec![ #( #after_pairs_pg ),* ];
3897 #root::audit::save_one_with_diff(
3898 pool,
3899 &_query,
3900 #pk_column_lit,
3901 ::core::convert::Into::<#root::core::SqlValue>::into(
3902 #pk_value_for_bind,
3903 ),
3904 <Self as #root::core::Model>::SCHEMA.table,
3905 #pk_str,
3906 _after_pairs,
3907 #pg_select_cols,
3908 #my_select_cols,
3909 #sqlite_select_cols,
3910 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
3911 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
3912 |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
3913 ).await
3914 }
3915 }
3916 } else {
3917 quote!()
3918 }
3919 } else {
3920 pool_save_method
3921 };
3922
3923 // `delete_pool(&Pool)` — emitted for every model with a PK. Two
3924 // body shapes:
3925 // - non-audited: simple dispatch through `sql::delete_pool`
3926 // - audited: routes through `audit::delete_one_with_audit`,
3927 // which opens a per-backend transaction wrapping DELETE +
3928 // audit emit so the data write and audit row commit atomically.
3929 let pool_delete_method = {
3930 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
3931 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
3932 if let Some(pk_ident) = pk_ident_for_pool {
3933 if audited_fields.is_some() {
3934 quote! {
3935 /// Delete this row against either backend with audit
3936 /// emission inside the same transaction. Bi-dialect
3937 /// counterpart of [`Self::delete`] for audited models.
3938 ///
3939 /// # Errors
3940 /// As [`Self::delete`].
3941 pub async fn delete_pool(
3942 &self,
3943 pool: &#root::sql::Pool,
3944 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3945 let _query = #root::core::DeleteQuery::by_pk(
3946 <Self as #root::core::Model>::SCHEMA,
3947 #pk_column_lit,
3948 ::core::convert::Into::<#root::core::SqlValue>::into(
3949 ::core::clone::Clone::clone(&self.#pk_ident)
3950 ),
3951 );
3952 let _audit_entry =
3953 self.__rustango_audit_entry(#root::audit::AuditOp::Delete);
3954 #root::audit::delete_one_with_audit(
3955 pool, &_query, &_audit_entry,
3956 ).await
3957 }
3958 }
3959 } else {
3960 quote! {
3961 /// Delete the row identified by this instance's primary key
3962 /// against either backend. Equivalent to [`Self::delete`] but
3963 /// takes [`#root::sql::Pool`] and dispatches per backend.
3964 ///
3965 /// # Errors
3966 /// As [`Self::delete`].
3967 pub async fn delete_pool(
3968 &self,
3969 pool: &#root::sql::Pool,
3970 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3971 let _query = #root::core::DeleteQuery::by_pk(
3972 <Self as #root::core::Model>::SCHEMA,
3973 #pk_column_lit,
3974 ::core::convert::Into::<#root::core::SqlValue>::into(
3975 ::core::clone::Clone::clone(&self.#pk_ident)
3976 ),
3977 );
3978 #root::sql::delete_pool(pool, &_query).await
3979 }
3980 }
3981 }
3982 } else {
3983 quote!()
3984 }
3985 };
3986
3987 // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
3988 // matching this instance's PK and overwrite the in-memory state
3989 // with the freshly-fetched columns.
3990 // Issue #825. Only emitted when the model declares a PK; non-PK
3991 // models can't address a specific row.
3992 //
3993 // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
3994 // every field from `self`; resets the PK to `Auto::Unset` when
3995 // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
3996 // a fresh autoincrement value. Non-Auto PKs preserve the source
3997 // PK — the caller must overwrite before insert. Pure-Rust, no
3998 // I/O, no dialect surface.
3999 let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
4000 let other_field_clones: Vec<TokenStream2> = fields
4001 .column_entries
4002 .iter()
4003 .filter(|c| &c.ident != pk_ident)
4004 .map(|c| {
4005 let ident = &c.ident;
4006 quote! {
4007 #ident: ::core::clone::Clone::clone(&self.#ident)
4008 }
4009 })
4010 .collect();
4011 let pk_clone_token = if fields.pk_is_auto {
4012 quote! { #pk_ident: #root::sql::Auto::Unset }
4013 } else {
4014 quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
4015 };
4016 let replicate_doc = if fields.pk_is_auto {
4017 quote! {
4018 /// Eloquent-style `replicate()` — return a clone of this
4019 /// row with the primary key reset to [`Auto::Unset`] so
4020 /// the copy is ready for `insert_pool` / `save_pool` to
4021 /// allocate a fresh autoincrement value. Every other
4022 /// field is `Clone`d verbatim. Issue #825.
4023 ///
4024 /// `auto_now_add` / `auto_now` timestamp fields are
4025 /// **not** reset (Eloquent's `replicate` doesn't reset
4026 /// them either) — pass them through the normal insert
4027 /// path if you want fresh values, or assign them
4028 /// explicitly after the call.
4029 }
4030 } else {
4031 quote! {
4032 /// Eloquent-style `replicate()` — clone this row
4033 /// verbatim. Because the primary key is **not** an
4034 /// `Auto<T>`, the clone keeps the source PK; the
4035 /// caller must overwrite `copy.<pk>` before inserting
4036 /// to avoid a unique-key violation. Issue #825.
4037 }
4038 };
4039 // 2026-06-07 — field-name / shortcut collision guard.
4040 //
4041 // The macro emits both `pub const <field>: <field>_col = ...`
4042 // (per-field typed-column const, used by the typed-builder
4043 // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
4044 // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
4045 // model has a field named e.g. `count`, both items would land
4046 // in the same inherent impl with the same name, and the
4047 // compiler rejects the derive with "duplicate definitions".
4048 //
4049 // Drop the conflicting shortcut for that model. Callers can
4050 // still reach the same behavior via
4051 // `QuerySet::<Model>::default().count(&pool)`.
4052 let column_names: ::std::collections::HashSet<String> = fields
4053 .column_entries
4054 .iter()
4055 .map(|c| c.ident.to_string())
4056 .collect();
4057 let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
4058 if column_names.contains(name) {
4059 quote! {}
4060 } else {
4061 tokens
4062 }
4063 };
4064 let count_method = emit_if_no_field_collision(
4065 "count",
4066 quote! {
4067 /// Count rows of this model — `SELECT COUNT(*) FROM
4068 /// <table>`. Eloquent `Model::count()` parity.
4069 ///
4070 /// Skipped on models that already declare a field named
4071 /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
4072 /// in that case.
4073 ///
4074 /// # Errors
4075 /// As [`CounterPool::count`].
4076 ///
4077 /// [`CounterPool::count`]: rustango::sql::CounterPool::count
4078 pub async fn count(
4079 pool: &#root::sql::Pool,
4080 ) -> ::core::result::Result<i64, #root::sql::ExecError> {
4081 use #root::sql::CounterPool as _;
4082 #root::query::QuerySet::<Self>::default()
4083 .count(pool)
4084 .await
4085 }
4086 },
4087 );
4088 let value_method = emit_if_no_field_collision(
4089 "value",
4090 quote! {
4091 /// Pluck a single scalar from the first row.
4092 /// Eloquent `Model::query()->value($col)` parity.
4093 ///
4094 /// Skipped on models that already declare a field named
4095 /// `value`. Drop into
4096 /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4097 ///
4098 /// # Errors
4099 /// As `ValuesFlatQuerySet::first`.
4100 pub async fn value<U>(
4101 col: &str,
4102 pool: &#root::sql::Pool,
4103 ) -> ::core::result::Result<
4104 ::core::option::Option<U>,
4105 #root::sql::ExecError,
4106 >
4107 where
4108 U: #root::sql::MaybePgScalar
4109 + #root::sql::MaybeMyScalar
4110 + #root::sql::MaybeSqliteScalar
4111 + ::core::marker::Send
4112 + ::core::marker::Unpin,
4113 {
4114 let _col_static: &'static str = Self::__resolve_col(col)?;
4115 #root::query::QuerySet::<Self>::default()
4116 .values_list_flat(_col_static)
4117 .first::<U>(pool)
4118 .await
4119 }
4120 },
4121 );
4122 let sum_method = emit_if_no_field_collision(
4123 "sum",
4124 quote! {
4125 /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4126 /// Skipped on models that already declare a field named
4127 /// `sum`.
4128 ///
4129 /// # Errors
4130 /// As [`#root::sql::fetch_aggregate_pool`].
4131 pub async fn sum<U>(
4132 col: &str,
4133 pool: &#root::sql::Pool,
4134 ) -> ::core::result::Result<
4135 ::core::option::Option<U>,
4136 #root::sql::ExecError,
4137 >
4138 where
4139 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4140 + #root::sql::MaybeMyFromRow
4141 + #root::sql::MaybeSqliteFromRow
4142 + ::core::marker::Send
4143 + ::core::marker::Unpin,
4144 {
4145 #root::query::QuerySet::<Self>::default()
4146 .sum::<U>(col, pool)
4147 .await
4148 }
4149 },
4150 );
4151 let avg_method = emit_if_no_field_collision(
4152 "avg",
4153 quote! {
4154 /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4155 /// models that already declare a field named `avg`.
4156 ///
4157 /// # Errors
4158 /// As [`#root::sql::fetch_aggregate_pool`].
4159 pub async fn avg<U>(
4160 col: &str,
4161 pool: &#root::sql::Pool,
4162 ) -> ::core::result::Result<
4163 ::core::option::Option<U>,
4164 #root::sql::ExecError,
4165 >
4166 where
4167 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4168 + #root::sql::MaybeMyFromRow
4169 + #root::sql::MaybeSqliteFromRow
4170 + ::core::marker::Send
4171 + ::core::marker::Unpin,
4172 {
4173 #root::query::QuerySet::<Self>::default()
4174 .avg::<U>(col, pool)
4175 .await
4176 }
4177 },
4178 );
4179 let min_method = emit_if_no_field_collision(
4180 "min",
4181 quote! {
4182 /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4183 /// models that already declare a field named `min`.
4184 ///
4185 /// # Errors
4186 /// As [`#root::sql::fetch_aggregate_pool`].
4187 pub async fn min<U>(
4188 col: &str,
4189 pool: &#root::sql::Pool,
4190 ) -> ::core::result::Result<
4191 ::core::option::Option<U>,
4192 #root::sql::ExecError,
4193 >
4194 where
4195 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4196 + #root::sql::MaybeMyFromRow
4197 + #root::sql::MaybeSqliteFromRow
4198 + ::core::marker::Send
4199 + ::core::marker::Unpin,
4200 {
4201 #root::query::QuerySet::<Self>::default()
4202 .min::<U>(col, pool)
4203 .await
4204 }
4205 },
4206 );
4207 let max_method = emit_if_no_field_collision(
4208 "max",
4209 quote! {
4210 /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4211 /// models that already declare a field named `max`.
4212 ///
4213 /// # Errors
4214 /// As [`#root::sql::fetch_aggregate_pool`].
4215 pub async fn max<U>(
4216 col: &str,
4217 pool: &#root::sql::Pool,
4218 ) -> ::core::result::Result<
4219 ::core::option::Option<U>,
4220 #root::sql::ExecError,
4221 >
4222 where
4223 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4224 + #root::sql::MaybeMyFromRow
4225 + #root::sql::MaybeSqliteFromRow
4226 + ::core::marker::Send
4227 + ::core::marker::Unpin,
4228 {
4229 #root::query::QuerySet::<Self>::default()
4230 .max::<U>(col, pool)
4231 .await
4232 }
4233 },
4234 );
4235 let first_method = emit_if_no_field_collision(
4236 "first",
4237 quote! {
4238 /// First row of this model. Eloquent `Model::first()`.
4239 /// Skipped on models that already declare a field named
4240 /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4241 ///
4242 /// # Errors
4243 /// As `QuerySet::first`.
4244 pub async fn first(
4245 pool: &#root::sql::Pool,
4246 ) -> ::core::result::Result<
4247 ::core::option::Option<Self>,
4248 #root::sql::ExecError,
4249 > {
4250 #root::query::QuerySet::<Self>::default()
4251 .first(pool)
4252 .await
4253 }
4254 },
4255 );
4256 let last_method = emit_if_no_field_collision(
4257 "last",
4258 quote! {
4259 /// Last row of this model by primary-key DESC.
4260 /// Eloquent `Model::query()->latest('id')->first()`
4261 /// parity — fetches the highest-PK row without
4262 /// requiring the caller to spell the PK column.
4263 /// Returns `None` on an empty table.
4264 ///
4265 /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4266 /// Skipped on models that already declare a field
4267 /// named `last`.
4268 ///
4269 /// # Errors
4270 /// As `QuerySet::last`.
4271 pub async fn last(
4272 pool: &#root::sql::Pool,
4273 ) -> ::core::result::Result<
4274 ::core::option::Option<Self>,
4275 #root::sql::ExecError,
4276 > {
4277 #root::query::QuerySet::<Self>::default()
4278 .last(pool)
4279 .await
4280 }
4281 },
4282 );
4283 quote! {
4284 /// Re-SELECT this row by its primary key and overwrite
4285 /// every in-memory field with the freshly-fetched value.
4286 /// Issue #825.
4287 ///
4288 /// Use this when the row may have been modified by another
4289 /// process / connection / job since you read it — e.g. after
4290 /// a queued task callback, or to re-sync stale UI state
4291 /// before re-saving.
4292 ///
4293 /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4294 /// when the primary key no longer matches any row (e.g.
4295 /// the row was deleted concurrently).
4296 ///
4297 /// # Errors
4298 /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4299 /// the PK no longer exists.
4300 ///
4301 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4302 pub async fn refresh_from_db(
4303 &mut self,
4304 pool: &#root::sql::Pool,
4305 ) -> ::core::result::Result<(), #root::sql::ExecError> {
4306 use #root::sql::FetcherPool as _;
4307 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4308 ::core::clone::Clone::clone(&self.#pk_ident),
4309 );
4310 let mut _rows: ::std::vec::Vec<Self> =
4311 #root::query::QuerySet::<Self>::default()
4312 .filter(::core::stringify!(#pk_ident), _pk_val)
4313 .limit(1)
4314 .fetch(pool)
4315 .await?;
4316 match _rows.into_iter().next() {
4317 ::core::option::Option::Some(_fresh) => {
4318 *self = _fresh;
4319 ::core::result::Result::Ok(())
4320 }
4321 ::core::option::Option::None => ::core::result::Result::Err(
4322 #root::sql::ExecError::Driver(
4323 #root::sql::sqlx::Error::RowNotFound,
4324 ),
4325 ),
4326 }
4327 }
4328
4329 /// Atomically increment the integer column `col` by
4330 /// `by` for this row. Equivalent to
4331 /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4332 ///
4333 /// **Doesn't mutate `self`** — the in-memory copy is now
4334 /// stale; call [`Self::refresh_from_db`] /
4335 /// [`Self::fresh`] to re-sync. Returns the rows-
4336 /// affected count (0 when the PK doesn't match any row,
4337 /// 1 on success).
4338 ///
4339 /// `col` is the Rust field name as a string; unknown
4340 /// fields surface as `UnknownField` at runtime. Negative
4341 /// `by` values atomically decrement (see also
4342 /// [`Self::decrement`]).
4343 ///
4344 /// # Errors
4345 /// As [`UpdaterPool::execute_pool`].
4346 ///
4347 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4348 pub async fn increment(
4349 &self,
4350 col: &str,
4351 by: i64,
4352 pool: &#root::sql::Pool,
4353 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4354 Self::__increment_one(self, col, by, pool).await
4355 }
4356
4357 /// Sibling of [`Self::increment`] — atomically
4358 /// decrement this row's `col` by `by`. Eloquent
4359 /// `$model->decrement($col, $by)` parity. Equivalent to
4360 /// `self.increment(col, -by, &pool)`; the separate name
4361 /// keeps call sites readable.
4362 ///
4363 /// # Errors
4364 /// As [`Self::increment`].
4365 pub async fn decrement(
4366 &self,
4367 col: &str,
4368 by: i64,
4369 pool: &#root::sql::Pool,
4370 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4371 Self::__increment_one(self, col, -by, pool).await
4372 }
4373
4374 /// Bulk-increment: add `by` to `col` on every row of the
4375 /// table. Eloquent `Model::query()->increment($col, $by)`
4376 /// parity. Use for counters, score adjustments, view
4377 /// rollups.
4378 ///
4379 /// # Errors
4380 /// As [`UpdaterPool::execute_pool`].
4381 ///
4382 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4383 pub async fn increment_each(
4384 col: &str,
4385 by: i64,
4386 pool: &#root::sql::Pool,
4387 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4388 Self::__increment_all(col, by, pool).await
4389 }
4390
4391 /// Sibling of [`Self::increment_each`] — bulk-decrement.
4392 ///
4393 /// # Errors
4394 /// As [`Self::increment_each`].
4395 pub async fn decrement_each(
4396 col: &str,
4397 by: i64,
4398 pool: &#root::sql::Pool,
4399 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4400 Self::__increment_all(col, -by, pool).await
4401 }
4402
4403 /// Internal: forward to
4404 /// [`#root::sql::model_shortcuts::increment_one_pool`].
4405 /// One-line wrapper kept as a per-Model method so the
4406 /// macro's emitted `increment` / `decrement` instance
4407 /// calls don't have to thread `Self` through manually.
4408 #[doc(hidden)]
4409 pub async fn __increment_one(
4410 this: &Self,
4411 col: &str,
4412 by: i64,
4413 pool: &#root::sql::Pool,
4414 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4415 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4416 ::core::clone::Clone::clone(&this.#pk_ident),
4417 );
4418 #root::sql::model_shortcuts::increment_one_pool::<Self>(
4419 ::core::stringify!(#pk_ident),
4420 _pk_val,
4421 col,
4422 by,
4423 pool,
4424 )
4425 .await
4426 }
4427
4428 /// Internal: forward to
4429 /// [`#root::sql::model_shortcuts::increment_all_pool`].
4430 #[doc(hidden)]
4431 pub async fn __increment_all(
4432 col: &str,
4433 by: i64,
4434 pool: &#root::sql::Pool,
4435 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4436 #root::sql::model_shortcuts::increment_all_pool::<Self>(col, by, pool).await
4437 }
4438
4439 /// Internal: forward to
4440 /// [`#root::sql::model_shortcuts::resolve_col`].
4441 #[doc(hidden)]
4442 pub fn __resolve_col(
4443 col: &str,
4444 ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4445 #root::sql::model_shortcuts::resolve_col::<Self>(col)
4446 }
4447
4448 /// Internal: forward to
4449 /// [`#root::sql::model_shortcuts::add_signed_expr`].
4450 #[doc(hidden)]
4451 #[must_use]
4452 pub fn __add_signed_expr(
4453 col_static: &'static str,
4454 signed_by: i64,
4455 ) -> #root::core::Expr {
4456 #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4457 }
4458
4459 /// Re-SELECT this row by its primary key and return a
4460 /// **new** instance with the freshly-fetched fields.
4461 /// Eloquent `Model::fresh()` parity — non-mutating
4462 /// counterpart of [`Self::refresh_from_db`].
4463 ///
4464 /// Returns `Ok(None)` when the row was deleted
4465 /// concurrently — vs [`Self::refresh_from_db`]
4466 /// which surfaces that as `RowNotFound` because
4467 /// in-place mutation has nothing to write to.
4468 ///
4469 /// Useful when you want to compare the in-memory
4470 /// instance against the persisted state (audit-style
4471 /// diffs, conflict detection) without mutating the
4472 /// reference you already hold.
4473 ///
4474 /// # Errors
4475 /// As [`FetcherPool::fetch`].
4476 ///
4477 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4478 pub async fn fresh(
4479 &self,
4480 pool: &#root::sql::Pool,
4481 ) -> ::core::result::Result<
4482 ::core::option::Option<Self>,
4483 #root::sql::ExecError,
4484 > {
4485 use #root::sql::FetcherPool as _;
4486 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4487 ::core::clone::Clone::clone(&self.#pk_ident),
4488 );
4489 let _rows: ::std::vec::Vec<Self> =
4490 #root::query::QuerySet::<Self>::default()
4491 .filter(::core::stringify!(#pk_ident), _pk_val)
4492 .limit(1)
4493 .fetch(pool)
4494 .await?;
4495 ::core::result::Result::Ok(_rows.into_iter().next())
4496 }
4497
4498 #replicate_doc
4499 #[must_use]
4500 pub fn replicate(&self) -> Self {
4501 Self {
4502 #pk_clone_token,
4503 #( #other_field_clones, )*
4504 }
4505 }
4506
4507 #first_method
4508
4509 #last_method
4510
4511 /// Throwing counterpart of [`Self::first`] —
4512 /// errors with `RowNotFound` when the table is empty.
4513 /// Eloquent `Model::firstOrFail()` parity.
4514 ///
4515 /// # Errors
4516 /// As [`Self::first`]; additionally
4517 /// [`sqlx::Error::RowNotFound`] on empty tables.
4518 ///
4519 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4520 pub async fn first_or_fail(
4521 pool: &#root::sql::Pool,
4522 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4523 // Route through the queryset rather than `Self::first`,
4524 // which is suppressed on models with a field named
4525 // `first` (field/shortcut collision guard).
4526 match #root::query::QuerySet::<Self>::default().first(pool).await? {
4527 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4528 ::core::option::Option::None => ::core::result::Result::Err(
4529 #root::sql::ExecError::Driver(
4530 #root::sql::sqlx::Error::RowNotFound,
4531 ),
4532 ),
4533 }
4534 }
4535
4536 /// Single-column projection — `SELECT <col> FROM
4537 /// <table>`. Returns `Vec<U>` where each element is the
4538 /// decoded value of the column. Eloquent
4539 /// `Model::pluck($column)` parity.
4540 ///
4541 /// Thin wrapper over `QuerySet::<Self>::default()
4542 /// .values_list_flat(col).fetch::<U>(pool)`. `U` must
4543 /// be decodable from the column's SQL type on every
4544 /// dialect the binary targets (common picks: `i64` /
4545 /// `i32` / `String` / `bool` / `f64`).
4546 ///
4547 /// # Errors
4548 /// As `ValuesFlatQuerySet::fetch`.
4549 pub async fn pluck<U>(
4550 col: &'static str,
4551 pool: &#root::sql::Pool,
4552 ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4553 where
4554 U: #root::sql::MaybePgScalar
4555 + #root::sql::MaybeMyScalar
4556 + #root::sql::MaybeSqliteScalar
4557 + ::core::marker::Send
4558 + ::core::marker::Unpin,
4559 {
4560 #root::query::QuerySet::<Self>::default()
4561 .values_list_flat(col)
4562 .fetch::<U>(pool)
4563 .await
4564 }
4565
4566 /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4567 /// stream every row of this model in batches of `n`,
4568 /// invoking the callback once per batch. Stable PK-ASC
4569 /// ordering so the LIMIT/OFFSET pagination is
4570 /// deterministic across drivers.
4571 ///
4572 /// The callback is async — it can do further DB work
4573 /// (writes, related-row lookups, queue dispatch) per
4574 /// batch. Return `Err(...)` from the callback to abort
4575 /// the iteration early; the error bubbles up.
4576 ///
4577 /// **When to use**: bulk processing flows that can't
4578 /// fit the whole table in memory — sending newsletters,
4579 /// running data migrations, computing summary
4580 /// statistics. For small / known-bounded tables, plain
4581 /// `Self::all(&pool)` is simpler.
4582 ///
4583 /// **Caveat**: ascending OFFSET pagination is O(N²) on
4584 /// large tables. For multi-million-row scans prefer
4585 /// keyset-by-PK (the standard "WHERE id > last_seen"
4586 /// shape) over `chunk(...)`.
4587 ///
4588 /// Skipped on models without a primary key — chunking
4589 /// needs a stable order to avoid skipping / repeating
4590 /// rows across batches.
4591 pub async fn chunk<F, Fut>(
4592 n: i64,
4593 pool: &#root::sql::Pool,
4594 mut cb: F,
4595 ) -> ::core::result::Result<(), #root::sql::ExecError>
4596 where
4597 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4598 Fut: ::core::future::Future<
4599 Output = ::core::result::Result<(), #root::sql::ExecError>,
4600 >,
4601 {
4602 use #root::sql::FetcherPool as _;
4603 let pk_col = match Self::primary_key_column() {
4604 ::core::option::Option::Some(c) => c,
4605 ::core::option::Option::None => {
4606 return ::core::result::Result::Ok(());
4607 }
4608 };
4609 let mut offset: i64 = 0;
4610 loop {
4611 let rows: ::std::vec::Vec<Self> =
4612 #root::query::QuerySet::<Self>::default()
4613 .order_by(&[(pk_col, false)])
4614 .limit(n)
4615 .offset(offset)
4616 .fetch(pool)
4617 .await?;
4618 if rows.is_empty() {
4619 return ::core::result::Result::Ok(());
4620 }
4621 let len = rows.len() as i64;
4622 cb(rows).await?;
4623 if len < n {
4624 return ::core::result::Result::Ok(());
4625 }
4626 offset += n;
4627 }
4628 }
4629
4630 /// Eloquent `Model::chunkById($n, fn (...))` — same
4631 /// per-batch callback shape as [`Self::chunk`], but uses
4632 /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4633 /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4634 /// — the right choice for multi-million-row sweeps.
4635 ///
4636 /// Requires the primary key to be a signed integer type
4637 /// (`i64` / `i32`); the keyset comparison rides on
4638 /// `__rustango_pk_value()` lowering through
4639 /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4640 /// non-integer PKs (UUID, String) — those should use
4641 /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4642 /// keyset loop.
4643 ///
4644 /// Callback errors abort iteration; the error bubbles up
4645 /// unchanged. Empty table → callback invoked zero times.
4646 pub async fn chunk_by_id<F, Fut>(
4647 n: i64,
4648 pool: &#root::sql::Pool,
4649 mut cb: F,
4650 ) -> ::core::result::Result<(), #root::sql::ExecError>
4651 where
4652 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4653 Fut: ::core::future::Future<
4654 Output = ::core::result::Result<(), #root::sql::ExecError>,
4655 >,
4656 {
4657 use #root::sql::FetcherPool as _;
4658 let pk_col = match Self::primary_key_column() {
4659 ::core::option::Option::Some(c) => c,
4660 ::core::option::Option::None => {
4661 return ::core::result::Result::Ok(());
4662 }
4663 };
4664 // Track the largest PK seen so the next batch picks
4665 // up from there. `i64::MIN` as the sentinel — the
4666 // very first iteration's `> MIN` matches every row,
4667 // so the loop entry is uniform with subsequent
4668 // iterations.
4669 let mut last_seen: i64 = i64::MIN;
4670 loop {
4671 let key = ::std::format!("{}__gt", pk_col);
4672 let rows: ::std::vec::Vec<Self> =
4673 #root::query::QuerySet::<Self>::default()
4674 .filter(key.as_str(), last_seen)
4675 .order_by(&[(pk_col, false)])
4676 .limit(n)
4677 .fetch(pool)
4678 .await?;
4679 if rows.is_empty() {
4680 return ::core::result::Result::Ok(());
4681 }
4682 let len = rows.len() as i64;
4683 // Capture the last row's PK BEFORE moving rows
4684 // into the callback.
4685 let max_pk = match rows
4686 .last()
4687 .map(|r| r.__rustango_pk_value())
4688 {
4689 ::core::option::Option::Some(
4690 #root::core::SqlValue::I64(v),
4691 ) => v,
4692 ::core::option::Option::Some(
4693 #root::core::SqlValue::I32(v),
4694 ) => i64::from(v),
4695 _ => return ::core::result::Result::Ok(()),
4696 };
4697 cb(rows).await?;
4698 if len < n {
4699 return ::core::result::Result::Ok(());
4700 }
4701 last_seen = max_pk;
4702 }
4703 }
4704
4705 /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4706 /// per-row callback companion to [`Self::chunk`].
4707 /// Streams every row in keyset-paginated batches of
4708 /// `batch` size, calling `cb` once per row.
4709 ///
4710 /// Inherits the keyset-paginated scan of
4711 /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4712 /// non-integer PKs are silently a no-op).
4713 ///
4714 /// ```ignore
4715 /// Post::each(500, &pool, |p| async move {
4716 /// reindex(p).await?;
4717 /// Ok(())
4718 /// }).await?;
4719 /// ```
4720 ///
4721 /// Return `Err(...)` from the callback to abort
4722 /// iteration; the error bubbles up unchanged.
4723 pub async fn each<F, Fut>(
4724 batch: i64,
4725 pool: &#root::sql::Pool,
4726 mut cb: F,
4727 ) -> ::core::result::Result<(), #root::sql::ExecError>
4728 where
4729 F: ::core::ops::FnMut(Self) -> Fut,
4730 Fut: ::core::future::Future<
4731 Output = ::core::result::Result<(), #root::sql::ExecError>,
4732 >,
4733 {
4734 use #root::sql::FetcherPool as _;
4735 let pk_col = match Self::primary_key_column() {
4736 ::core::option::Option::Some(c) => c,
4737 ::core::option::Option::None => {
4738 return ::core::result::Result::Ok(());
4739 }
4740 };
4741 let mut last_seen: i64 = i64::MIN;
4742 loop {
4743 let key = ::std::format!("{}__gt", pk_col);
4744 let rows: ::std::vec::Vec<Self> =
4745 #root::query::QuerySet::<Self>::default()
4746 .filter(key.as_str(), last_seen)
4747 .order_by(&[(pk_col, false)])
4748 .limit(batch)
4749 .fetch(pool)
4750 .await?;
4751 if rows.is_empty() {
4752 return ::core::result::Result::Ok(());
4753 }
4754 let len = rows.len() as i64;
4755 let max_pk = match rows
4756 .last()
4757 .map(|r| r.__rustango_pk_value())
4758 {
4759 ::core::option::Option::Some(
4760 #root::core::SqlValue::I64(v),
4761 ) => v,
4762 ::core::option::Option::Some(
4763 #root::core::SqlValue::I32(v),
4764 ) => i64::from(v),
4765 _ => return ::core::result::Result::Ok(()),
4766 };
4767 for row in rows {
4768 cb(row).await?;
4769 }
4770 if len < batch {
4771 return ::core::result::Result::Ok(());
4772 }
4773 last_seen = max_pk;
4774 }
4775 }
4776
4777 /// Delete every row of this model — `TRUNCATE TABLE
4778 /// <table> RESTART IDENTITY CASCADE` on Postgres,
4779 /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4780 /// support `TRUNCATE` inside foreign-key constraints
4781 /// or — for SQLite — at all). Eloquent `Model::truncate()`
4782 /// parity.
4783 ///
4784 /// **Use only in tests / fixture-reset flows.** Production
4785 /// writes through this would silently bypass the
4786 /// `pre_delete` / `post_delete` signals (no per-row hooks
4787 /// fire on a TRUNCATE / bulk DELETE FROM) and lose every
4788 /// row's audit-log entry.
4789 ///
4790 /// # Errors
4791 /// As [`raw_execute_pool`].
4792 ///
4793 /// [`raw_execute_pool`]: rustango::sql::raw_execute_pool
4794 pub async fn truncate(
4795 pool: &#root::sql::Pool,
4796 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4797 let _table = <Self as #root::core::Model>::SCHEMA.table;
4798 let _dialect = pool.dialect();
4799 let _quoted = _dialect.quote_ident(_table);
4800 let _sql = if _dialect.name() == "postgres" {
4801 ::std::format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", _quoted)
4802 } else {
4803 ::std::format!("DELETE FROM {}", _quoted)
4804 };
4805 #root::sql::raw_execute_pool(pool, &_sql, ::std::vec::Vec::new()).await
4806 }
4807
4808 /// Bulk-delete every row whose primary key is in
4809 /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4810 /// Returns the affected row count.
4811 ///
4812 /// Eloquent `Model::destroy([1, 2, 3])` parity.
4813 /// Empty `pks` is a no-op (returns 0).
4814 ///
4815 /// Accepts any iterable whose elements are
4816 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4817 /// `[i64; N]`, etc.
4818 ///
4819 /// # Errors
4820 /// As `delete_pool`.
4821 pub async fn destroy<V>(
4822 pks: impl ::core::iter::IntoIterator<Item = V>,
4823 pool: &#root::sql::Pool,
4824 ) -> ::core::result::Result<u64, #root::sql::ExecError>
4825 where
4826 V: ::core::convert::Into<#root::core::SqlValue>,
4827 {
4828 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4829 pks.into_iter().map(::core::convert::Into::into).collect();
4830 if _values.is_empty() {
4831 return ::core::result::Result::Ok(0);
4832 }
4833 let _pk = <Self as #root::core::Model>::SCHEMA
4834 .primary_key()
4835 .ok_or_else(|| {
4836 #root::sql::ExecError::Sql(#root::sql::SqlError::MissingPrimaryKey)
4837 })?;
4838 let _query = #root::query::QuerySet::<Self>::default()
4839 .filter_op(
4840 _pk.name,
4841 #root::core::Op::In,
4842 #root::core::SqlValue::List(_values),
4843 )
4844 .compile_delete()?;
4845 #root::sql::delete_pool(pool, &_query).await
4846 }
4847
4848 /// Fetch every row where `<col> = <val>`. Eloquent
4849 /// `Model::where($col, $val)->get()` parity.
4850 ///
4851 /// Thin wrapper over `QuerySet::<Self>::default()
4852 /// .filter(col, val).fetch(pool)`. For one row,
4853 /// use [`Self::first_where`]; for a chain that
4854 /// needs further `.filter()` / `.order_by()` /
4855 /// `.limit()`, drop down to `Self::query().filter(...)`
4856 /// directly.
4857 ///
4858 /// `val` accepts any value `Into<SqlValue>` so plain
4859 /// strings, ints, UUIDs, etc. all work.
4860 ///
4861 /// # Errors
4862 /// As [`FetcherPool::fetch`].
4863 ///
4864 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4865 pub async fn where_(
4866 col: &str,
4867 val: impl ::core::convert::Into<#root::core::SqlValue>,
4868 pool: &#root::sql::Pool,
4869 ) -> ::core::result::Result<
4870 ::std::vec::Vec<Self>,
4871 #root::sql::ExecError,
4872 > {
4873 use #root::sql::FetcherPool as _;
4874 #root::query::QuerySet::<Self>::default()
4875 .filter(col, val)
4876 .fetch(pool)
4877 .await
4878 }
4879
4880 /// Fetch every row where `<col> IN (vals)`. Eloquent
4881 /// `Model::whereIn($col, $vals)->get()` parity. Empty
4882 /// `vals` returns no rows (matches SQL's empty-IN
4883 /// semantics).
4884 ///
4885 /// `vals` accepts any iterable whose items are
4886 /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
4887 /// etc.
4888 ///
4889 /// # Errors
4890 /// As [`FetcherPool::fetch`].
4891 ///
4892 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4893 pub async fn where_in<V>(
4894 col: &str,
4895 vals: impl ::core::iter::IntoIterator<Item = V>,
4896 pool: &#root::sql::Pool,
4897 ) -> ::core::result::Result<
4898 ::std::vec::Vec<Self>,
4899 #root::sql::ExecError,
4900 >
4901 where
4902 V: ::core::convert::Into<#root::core::SqlValue>,
4903 {
4904 use #root::sql::FetcherPool as _;
4905 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4906 vals.into_iter().map(::core::convert::Into::into).collect();
4907 if _values.is_empty() {
4908 return ::core::result::Result::Ok(::std::vec::Vec::new());
4909 }
4910 let _key = ::std::format!("{}__in", col);
4911 #root::query::QuerySet::<Self>::default()
4912 .filter(&_key, #root::core::SqlValue::List(_values))
4913 .fetch(pool)
4914 .await
4915 }
4916
4917 /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
4918 /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
4919 /// `vals` returns every row (matches SQL's empty-NOT-IN
4920 /// semantics — vacuously true for every row).
4921 ///
4922 /// # Errors
4923 /// As [`FetcherPool::fetch`].
4924 ///
4925 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4926 pub async fn where_not_in<V>(
4927 col: &str,
4928 vals: impl ::core::iter::IntoIterator<Item = V>,
4929 pool: &#root::sql::Pool,
4930 ) -> ::core::result::Result<
4931 ::std::vec::Vec<Self>,
4932 #root::sql::ExecError,
4933 >
4934 where
4935 V: ::core::convert::Into<#root::core::SqlValue>,
4936 {
4937 use #root::sql::FetcherPool as _;
4938 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4939 vals.into_iter().map(::core::convert::Into::into).collect();
4940 if _values.is_empty() {
4941 return #root::query::QuerySet::<Self>::default()
4942 .fetch(pool)
4943 .await;
4944 }
4945 let _key = ::std::format!("{}__not_in", col);
4946 #root::query::QuerySet::<Self>::default()
4947 .filter(&_key, #root::core::SqlValue::List(_values))
4948 .fetch(pool)
4949 .await
4950 }
4951
4952 /// Fetch every row where `<col> IS NULL`. Eloquent
4953 /// `Model::whereNull($col)->get()` parity.
4954 ///
4955 /// # Errors
4956 /// As [`FetcherPool::fetch`].
4957 ///
4958 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4959 pub async fn where_null(
4960 col: &str,
4961 pool: &#root::sql::Pool,
4962 ) -> ::core::result::Result<
4963 ::std::vec::Vec<Self>,
4964 #root::sql::ExecError,
4965 > {
4966 use #root::sql::FetcherPool as _;
4967 let _key = ::std::format!("{}__isnull", col);
4968 #root::query::QuerySet::<Self>::default()
4969 .filter(&_key, #root::core::SqlValue::Bool(true))
4970 .fetch(pool)
4971 .await
4972 }
4973
4974 /// Fetch every row where `<col> IS NOT NULL`. Eloquent
4975 /// `Model::whereNotNull($col)->get()` parity.
4976 ///
4977 /// # Errors
4978 /// As [`FetcherPool::fetch`].
4979 ///
4980 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4981 pub async fn where_not_null(
4982 col: &str,
4983 pool: &#root::sql::Pool,
4984 ) -> ::core::result::Result<
4985 ::std::vec::Vec<Self>,
4986 #root::sql::ExecError,
4987 > {
4988 use #root::sql::FetcherPool as _;
4989 let _key = ::std::format!("{}__isnull", col);
4990 #root::query::QuerySet::<Self>::default()
4991 .filter(&_key, #root::core::SqlValue::Bool(false))
4992 .fetch(pool)
4993 .await
4994 }
4995
4996 /// Fetch up to `n` rows in random order. Eloquent
4997 /// `Model::inRandomOrder()->limit($n)->get()` /
4998 /// `Model::query()->inRandomOrder()->get()->take($n)`
4999 /// parity. **Performance caveat**: random ordering
5000 /// forces a full table scan + per-row random key sort;
5001 /// the optimizer cannot use an index. Prefer a
5002 /// `pk >= rand_offset LIMIT N` walk for huge tables.
5003 ///
5004 /// # Errors
5005 /// As [`FetcherPool::fetch`].
5006 ///
5007 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5008 pub async fn random_n(
5009 n: i64,
5010 pool: &#root::sql::Pool,
5011 ) -> ::core::result::Result<
5012 ::std::vec::Vec<Self>,
5013 #root::sql::ExecError,
5014 > {
5015 use #root::sql::FetcherPool as _;
5016 #root::query::QuerySet::<Self>::default()
5017 .order_random()
5018 .limit(n)
5019 .fetch(pool)
5020 .await
5021 }
5022
5023 /// Fetch one row in random order. Eloquent
5024 /// `Model::inRandomOrder()->first()` parity. Same
5025 /// performance caveat as [`Self::random_n`].
5026 ///
5027 /// # Errors
5028 /// As [`FetcherPool::fetch`].
5029 ///
5030 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5031 pub async fn random(
5032 pool: &#root::sql::Pool,
5033 ) -> ::core::result::Result<
5034 ::core::option::Option<Self>,
5035 #root::sql::ExecError,
5036 > {
5037 ::core::result::Result::Ok(
5038 Self::random_n(1, pool).await?.into_iter().next(),
5039 )
5040 }
5041
5042 /// Fetch every row ordered ASC by `field`. Eloquent
5043 /// `Model::oldest($field)->get()` parity — the multi-row
5044 /// counterpart of [`Self::earliest`].
5045 ///
5046 /// # Errors
5047 /// As [`FetcherPool::fetch`].
5048 ///
5049 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5050 pub async fn oldest(
5051 field: &str,
5052 pool: &#root::sql::Pool,
5053 ) -> ::core::result::Result<
5054 ::std::vec::Vec<Self>,
5055 #root::sql::ExecError,
5056 > {
5057 use #root::sql::FetcherPool as _;
5058 #root::query::QuerySet::<Self>::default()
5059 .order_by(&[(field, false)])
5060 .fetch(pool)
5061 .await
5062 }
5063
5064 /// Fetch every row ordered DESC by `field`. Eloquent
5065 /// `Model::latest($field)->get()` parity — the multi-row
5066 /// counterpart of [`Self::latest`].
5067 ///
5068 /// # Errors
5069 /// As [`FetcherPool::fetch`].
5070 ///
5071 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5072 pub async fn newest(
5073 field: &str,
5074 pool: &#root::sql::Pool,
5075 ) -> ::core::result::Result<
5076 ::std::vec::Vec<Self>,
5077 #root::sql::ExecError,
5078 > {
5079 use #root::sql::FetcherPool as _;
5080 #root::query::QuerySet::<Self>::default()
5081 .order_by(&[(field, true)])
5082 .fetch(pool)
5083 .await
5084 }
5085
5086 /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5087 /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5088 /// Routes through the existing `__year` lookup suffix
5089 /// (issue #829).
5090 ///
5091 /// # Errors
5092 /// As [`FetcherPool::fetch`].
5093 ///
5094 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5095 pub async fn where_year(
5096 col: &str,
5097 year: i64,
5098 pool: &#root::sql::Pool,
5099 ) -> ::core::result::Result<
5100 ::std::vec::Vec<Self>,
5101 #root::sql::ExecError,
5102 > {
5103 use #root::sql::FetcherPool as _;
5104 let _key = ::std::format!("{}__year", col);
5105 #root::query::QuerySet::<Self>::default()
5106 .filter(&_key, #root::core::SqlValue::I64(year))
5107 .fetch(pool)
5108 .await
5109 }
5110
5111 /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5112 /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5113 /// `month` is 1–12.
5114 ///
5115 /// # Errors
5116 /// As [`FetcherPool::fetch`].
5117 ///
5118 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5119 pub async fn where_month(
5120 col: &str,
5121 month: i64,
5122 pool: &#root::sql::Pool,
5123 ) -> ::core::result::Result<
5124 ::std::vec::Vec<Self>,
5125 #root::sql::ExecError,
5126 > {
5127 use #root::sql::FetcherPool as _;
5128 let _key = ::std::format!("{}__month", col);
5129 #root::query::QuerySet::<Self>::default()
5130 .filter(&_key, #root::core::SqlValue::I64(month))
5131 .fetch(pool)
5132 .await
5133 }
5134
5135 /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5136 /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5137 /// `day` is 1–31.
5138 ///
5139 /// # Errors
5140 /// As [`FetcherPool::fetch`].
5141 ///
5142 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5143 pub async fn where_day(
5144 col: &str,
5145 day: i64,
5146 pool: &#root::sql::Pool,
5147 ) -> ::core::result::Result<
5148 ::std::vec::Vec<Self>,
5149 #root::sql::ExecError,
5150 > {
5151 use #root::sql::FetcherPool as _;
5152 let _key = ::std::format!("{}__day", col);
5153 #root::query::QuerySet::<Self>::default()
5154 .filter(&_key, #root::core::SqlValue::I64(day))
5155 .fetch(pool)
5156 .await
5157 }
5158
5159 /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5160 /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5161 /// `hour` is 0–23.
5162 ///
5163 /// # Errors
5164 /// As [`FetcherPool::fetch`].
5165 ///
5166 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5167 pub async fn where_hour(
5168 col: &str,
5169 hour: i64,
5170 pool: &#root::sql::Pool,
5171 ) -> ::core::result::Result<
5172 ::std::vec::Vec<Self>,
5173 #root::sql::ExecError,
5174 > {
5175 use #root::sql::FetcherPool as _;
5176 let _key = ::std::format!("{}__hour", col);
5177 #root::query::QuerySet::<Self>::default()
5178 .filter(&_key, #root::core::SqlValue::I64(hour))
5179 .fetch(pool)
5180 .await
5181 }
5182
5183 /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5184 /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5185 /// `minute` is 0–59.
5186 ///
5187 /// # Errors
5188 /// As [`FetcherPool::fetch`].
5189 ///
5190 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5191 pub async fn where_minute(
5192 col: &str,
5193 minute: i64,
5194 pool: &#root::sql::Pool,
5195 ) -> ::core::result::Result<
5196 ::std::vec::Vec<Self>,
5197 #root::sql::ExecError,
5198 > {
5199 use #root::sql::FetcherPool as _;
5200 let _key = ::std::format!("{}__minute", col);
5201 #root::query::QuerySet::<Self>::default()
5202 .filter(&_key, #root::core::SqlValue::I64(minute))
5203 .fetch(pool)
5204 .await
5205 }
5206
5207 /// Fetch every row where `<col> LIKE <pattern>` —
5208 /// caller-supplied pattern (must include `%` / `_`
5209 /// wildcards manually). Eloquent `Model::whereLike`
5210 /// parity.
5211 ///
5212 /// # Errors
5213 /// As [`FetcherPool::fetch`].
5214 ///
5215 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5216 pub async fn where_like(
5217 col: &str,
5218 pattern: impl ::core::convert::Into<::std::string::String>,
5219 pool: &#root::sql::Pool,
5220 ) -> ::core::result::Result<
5221 ::std::vec::Vec<Self>,
5222 #root::sql::ExecError,
5223 > {
5224 use #root::sql::FetcherPool as _;
5225 let _key = ::std::format!("{}__like", col);
5226 #root::query::QuerySet::<Self>::default()
5227 .filter(
5228 &_key,
5229 #root::core::SqlValue::String(pattern.into()),
5230 )
5231 .fetch(pool)
5232 .await
5233 }
5234
5235 /// Fetch every row where `<col> ILIKE <pattern>` —
5236 /// case-insensitive LIKE (PG native, MySQL/SQLite
5237 /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5238 ///
5239 /// # Errors
5240 /// As [`FetcherPool::fetch`].
5241 ///
5242 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5243 pub async fn where_ilike(
5244 col: &str,
5245 pattern: impl ::core::convert::Into<::std::string::String>,
5246 pool: &#root::sql::Pool,
5247 ) -> ::core::result::Result<
5248 ::std::vec::Vec<Self>,
5249 #root::sql::ExecError,
5250 > {
5251 use #root::sql::FetcherPool as _;
5252 let _key = ::std::format!("{}__ilike", col);
5253 #root::query::QuerySet::<Self>::default()
5254 .filter(
5255 &_key,
5256 #root::core::SqlValue::String(pattern.into()),
5257 )
5258 .fetch(pool)
5259 .await
5260 }
5261
5262 /// Fetch every row where `<col>` starts with `prefix`
5263 /// (auto-appends `%`). Eloquent
5264 /// `whereLike("col", "$prefix%")` parity.
5265 ///
5266 /// # Errors
5267 /// As [`FetcherPool::fetch`].
5268 ///
5269 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5270 pub async fn where_starts_with(
5271 col: &str,
5272 prefix: impl ::core::convert::Into<::std::string::String>,
5273 pool: &#root::sql::Pool,
5274 ) -> ::core::result::Result<
5275 ::std::vec::Vec<Self>,
5276 #root::sql::ExecError,
5277 > {
5278 use #root::sql::FetcherPool as _;
5279 let _key = ::std::format!("{}__startswith", col);
5280 #root::query::QuerySet::<Self>::default()
5281 .filter(
5282 &_key,
5283 #root::core::SqlValue::String(prefix.into()),
5284 )
5285 .fetch(pool)
5286 .await
5287 }
5288
5289 /// Fetch every row where `<col>` ends with `suffix`
5290 /// (auto-prepends `%`). Eloquent
5291 /// `whereLike("col", "%$suffix")` parity.
5292 ///
5293 /// # Errors
5294 /// As [`FetcherPool::fetch`].
5295 ///
5296 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5297 pub async fn where_ends_with(
5298 col: &str,
5299 suffix: impl ::core::convert::Into<::std::string::String>,
5300 pool: &#root::sql::Pool,
5301 ) -> ::core::result::Result<
5302 ::std::vec::Vec<Self>,
5303 #root::sql::ExecError,
5304 > {
5305 use #root::sql::FetcherPool as _;
5306 let _key = ::std::format!("{}__endswith", col);
5307 #root::query::QuerySet::<Self>::default()
5308 .filter(
5309 &_key,
5310 #root::core::SqlValue::String(suffix.into()),
5311 )
5312 .fetch(pool)
5313 .await
5314 }
5315
5316 /// Fetch every row where `<col>` contains `substr`
5317 /// (auto-wraps with `%`).
5318 /// Eloquent `whereLike("col", "%$substr%")` parity.
5319 ///
5320 /// # Errors
5321 /// As [`FetcherPool::fetch`].
5322 ///
5323 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5324 pub async fn where_contains(
5325 col: &str,
5326 substr: impl ::core::convert::Into<::std::string::String>,
5327 pool: &#root::sql::Pool,
5328 ) -> ::core::result::Result<
5329 ::std::vec::Vec<Self>,
5330 #root::sql::ExecError,
5331 > {
5332 use #root::sql::FetcherPool as _;
5333 let _key = ::std::format!("{}__contains", col);
5334 #root::query::QuerySet::<Self>::default()
5335 .filter(
5336 &_key,
5337 #root::core::SqlValue::String(substr.into()),
5338 )
5339 .fetch(pool)
5340 .await
5341 }
5342
5343 /// Fetch every row where `<col> > val`. Eloquent
5344 /// `Model::where($col, ">", $val)->get()` parity.
5345 ///
5346 /// # Errors
5347 /// As [`FetcherPool::fetch`].
5348 ///
5349 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5350 pub async fn where_gt(
5351 col: &str,
5352 val: impl ::core::convert::Into<#root::core::SqlValue>,
5353 pool: &#root::sql::Pool,
5354 ) -> ::core::result::Result<
5355 ::std::vec::Vec<Self>,
5356 #root::sql::ExecError,
5357 > {
5358 use #root::sql::FetcherPool as _;
5359 let _key = ::std::format!("{}__gt", col);
5360 #root::query::QuerySet::<Self>::default()
5361 .filter(&_key, val)
5362 .fetch(pool)
5363 .await
5364 }
5365
5366 /// Fetch every row where `<col> >= val`. Eloquent
5367 /// `Model::where($col, ">=", $val)->get()` parity.
5368 ///
5369 /// # Errors
5370 /// As [`FetcherPool::fetch`].
5371 ///
5372 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5373 pub async fn where_gte(
5374 col: &str,
5375 val: impl ::core::convert::Into<#root::core::SqlValue>,
5376 pool: &#root::sql::Pool,
5377 ) -> ::core::result::Result<
5378 ::std::vec::Vec<Self>,
5379 #root::sql::ExecError,
5380 > {
5381 use #root::sql::FetcherPool as _;
5382 let _key = ::std::format!("{}__gte", col);
5383 #root::query::QuerySet::<Self>::default()
5384 .filter(&_key, val)
5385 .fetch(pool)
5386 .await
5387 }
5388
5389 /// Fetch every row where `<col> < val`. Eloquent
5390 /// `Model::where($col, "<", $val)->get()` parity.
5391 ///
5392 /// # Errors
5393 /// As [`FetcherPool::fetch`].
5394 ///
5395 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5396 pub async fn where_lt(
5397 col: &str,
5398 val: impl ::core::convert::Into<#root::core::SqlValue>,
5399 pool: &#root::sql::Pool,
5400 ) -> ::core::result::Result<
5401 ::std::vec::Vec<Self>,
5402 #root::sql::ExecError,
5403 > {
5404 use #root::sql::FetcherPool as _;
5405 let _key = ::std::format!("{}__lt", col);
5406 #root::query::QuerySet::<Self>::default()
5407 .filter(&_key, val)
5408 .fetch(pool)
5409 .await
5410 }
5411
5412 /// Fetch every row where `<col> <= val`. Eloquent
5413 /// `Model::where($col, "<=", $val)->get()` parity.
5414 ///
5415 /// # Errors
5416 /// As [`FetcherPool::fetch`].
5417 ///
5418 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5419 pub async fn where_lte(
5420 col: &str,
5421 val: impl ::core::convert::Into<#root::core::SqlValue>,
5422 pool: &#root::sql::Pool,
5423 ) -> ::core::result::Result<
5424 ::std::vec::Vec<Self>,
5425 #root::sql::ExecError,
5426 > {
5427 use #root::sql::FetcherPool as _;
5428 let _key = ::std::format!("{}__lte", col);
5429 #root::query::QuerySet::<Self>::default()
5430 .filter(&_key, val)
5431 .fetch(pool)
5432 .await
5433 }
5434
5435 /// Fetch every row where `<col> <> val`. Eloquent
5436 /// `Model::where($col, "!=", $val)->get()` parity.
5437 ///
5438 /// # Errors
5439 /// As [`FetcherPool::fetch`].
5440 ///
5441 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5442 pub async fn where_ne(
5443 col: &str,
5444 val: impl ::core::convert::Into<#root::core::SqlValue>,
5445 pool: &#root::sql::Pool,
5446 ) -> ::core::result::Result<
5447 ::std::vec::Vec<Self>,
5448 #root::sql::ExecError,
5449 > {
5450 use #root::sql::FetcherPool as _;
5451 let _key = ::std::format!("{}__ne", col);
5452 #root::query::QuerySet::<Self>::default()
5453 .filter(&_key, val)
5454 .fetch(pool)
5455 .await
5456 }
5457
5458 /// Fetch every row matching `<col> = val` for ANY of the
5459 /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5460 /// parity. Empty `cols` returns no rows.
5461 ///
5462 /// Resolves each `&str` column to its SCHEMA-registered
5463 /// `&'static str` once and builds a single OR-composed
5464 /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5465 ///
5466 /// # Errors
5467 /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5468 /// when any column is not declared on the model.
5469 ///
5470 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5471 pub async fn where_any(
5472 cols: &[&str],
5473 val: impl ::core::convert::Into<#root::core::SqlValue>,
5474 pool: &#root::sql::Pool,
5475 ) -> ::core::result::Result<
5476 ::std::vec::Vec<Self>,
5477 #root::sql::ExecError,
5478 > {
5479 Self::__where_multi(cols, val, false, pool).await
5480 }
5481
5482 /// Fetch every row matching `<col> = val` for ALL listed
5483 /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5484 /// Empty `cols` returns every row (vacuous AND).
5485 ///
5486 /// # Errors
5487 /// As [`Self::where_any`].
5488 pub async fn where_all(
5489 cols: &[&str],
5490 val: impl ::core::convert::Into<#root::core::SqlValue>,
5491 pool: &#root::sql::Pool,
5492 ) -> ::core::result::Result<
5493 ::std::vec::Vec<Self>,
5494 #root::sql::ExecError,
5495 > {
5496 Self::__where_multi(cols, val, true, pool).await
5497 }
5498
5499 /// Internal: build a Q expression composing `cols` via
5500 /// AND (`all`) or OR (`!all`), then fetch. Backs
5501 /// `where_any` / `where_all`.
5502 #[doc(hidden)]
5503 pub async fn __where_multi(
5504 cols: &[&str],
5505 val: impl ::core::convert::Into<#root::core::SqlValue>,
5506 all: bool,
5507 pool: &#root::sql::Pool,
5508 ) -> ::core::result::Result<
5509 ::std::vec::Vec<Self>,
5510 #root::sql::ExecError,
5511 > {
5512 #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5513 .await
5514 }
5515
5516 /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5517 /// parity. PK-ordered
5518 /// is NOT guaranteed without an explicit `order_by` —
5519 /// drop into `Self::query()` for that.
5520 ///
5521 /// # Errors
5522 /// As [`FetcherPool::fetch`].
5523 ///
5524 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5525 pub async fn take(
5526 n: i64,
5527 pool: &#root::sql::Pool,
5528 ) -> ::core::result::Result<
5529 ::std::vec::Vec<Self>,
5530 #root::sql::ExecError,
5531 > {
5532 use #root::sql::FetcherPool as _;
5533 #root::query::QuerySet::<Self>::default()
5534 .limit(n)
5535 .fetch(pool)
5536 .await
5537 }
5538
5539 /// Fetch the page-th window of `per_page` rows
5540 /// (1-indexed). Eloquent
5541 /// `Model::query()->forPage($page, $perPage)->get()`
5542 /// parity. The DB scans an `OFFSET (page - 1) * per_page
5543 /// LIMIT per_page`; for large offsets this is O(N) —
5544 /// prefer keyset pagination via PK on hot paths.
5545 ///
5546 /// # Errors
5547 /// As [`FetcherPool::fetch`].
5548 ///
5549 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5550 pub async fn for_page(
5551 page: i64,
5552 per_page: i64,
5553 pool: &#root::sql::Pool,
5554 ) -> ::core::result::Result<
5555 ::std::vec::Vec<Self>,
5556 #root::sql::ExecError,
5557 > {
5558 use #root::sql::FetcherPool as _;
5559 let _offset = if page > 1 { (page - 1) * per_page } else { 0 };
5560 #root::query::QuerySet::<Self>::default()
5561 .limit(per_page)
5562 .offset(_offset)
5563 .fetch(pool)
5564 .await
5565 }
5566
5567 /// Eloquent `Model::paginate($per_page, $page)` — fetch
5568 /// one page of rows AND the total row count in one
5569 /// call. Returns `(rows, total)`. Two queries: a
5570 /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5571 /// for the total.
5572 ///
5573 /// Useful for paginated UIs that need both the visible
5574 /// rows AND a "Page X of Y" / total-count widget. For
5575 /// large tables prefer keyset pagination + cached count;
5576 /// every call to `paginate` re-counts the full table.
5577 ///
5578 /// Same 1-indexed `page` convention as [`Self::for_page`].
5579 ///
5580 /// # Errors
5581 /// As [`Self::for_page`] and [`Self::count`].
5582 pub async fn paginate(
5583 page: i64,
5584 per_page: i64,
5585 pool: &#root::sql::Pool,
5586 ) -> ::core::result::Result<
5587 (::std::vec::Vec<Self>, i64),
5588 #root::sql::ExecError,
5589 > {
5590 // Route through the queryset rather than `Self::count`:
5591 // the `count` inherent method is suppressed on models
5592 // that declare a field named `count` (field/shortcut
5593 // collision guard), and `paginate` must still compile
5594 // for those models.
5595 let total = {
5596 use #root::sql::CounterPool as _;
5597 #root::query::QuerySet::<Self>::default()
5598 .count(pool)
5599 .await?
5600 };
5601 let rows = Self::for_page(page, per_page, pool).await?;
5602 ::core::result::Result::Ok((rows, total))
5603 }
5604
5605 /// Bulk-update — set `set_col = set_val` on every row
5606 /// matching `where_col = where_val`. Returns affected row
5607 /// count. Eloquent
5608 /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5609 /// parity.
5610 ///
5611 /// For multi-column updates drop into the queryset
5612 /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5613 ///
5614 /// # Errors
5615 /// As [`UpdaterPool::execute_pool`].
5616 ///
5617 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5618 pub async fn update_where(
5619 where_col: &str,
5620 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5621 set_col: &str,
5622 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5623 pool: &#root::sql::Pool,
5624 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5625 use #root::sql::UpdaterPool as _;
5626 #root::query::QuerySet::<Self>::default()
5627 .filter(where_col, where_val)
5628 .update()
5629 .set(set_col, set_val)
5630 .execute_pool(pool)
5631 .await
5632 }
5633
5634 /// Bulk-delete — remove every row matching
5635 /// `where_col = where_val`. Returns affected row count.
5636 /// Eloquent
5637 /// `Model::where($where_col, $where_val)->delete()` parity.
5638 ///
5639 /// # Errors
5640 /// As [`#root::sql::delete_pool`].
5641 pub async fn delete_where(
5642 where_col: &str,
5643 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5644 pool: &#root::sql::Pool,
5645 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5646 let _query = #root::query::QuerySet::<Self>::default()
5647 .filter_op(where_col, #root::core::Op::Eq, where_val)
5648 .compile_delete()?;
5649 #root::sql::delete_pool(pool, &_query).await
5650 }
5651
5652 /// Bulk-update — set `set_col = set_val` on EVERY row of
5653 /// this model's table. **No WHERE clause** — use with
5654 /// care. Eloquent `Model::query()->update([$col => $val])`
5655 /// parity.
5656 ///
5657 /// Use for backfills, one-shot reset flows, etc. The
5658 /// returned count is rows affected.
5659 ///
5660 /// # Errors
5661 /// As [`UpdaterPool::execute_pool`].
5662 ///
5663 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5664 pub async fn update_all(
5665 set_col: &str,
5666 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5667 pool: &#root::sql::Pool,
5668 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5669 use #root::sql::UpdaterPool as _;
5670 #root::query::QuerySet::<Self>::default()
5671 .update()
5672 .set(set_col, set_val)
5673 .execute_pool(pool)
5674 .await
5675 }
5676
5677 /// Fetch every row where `<col> NOT LIKE <pattern>`.
5678 /// Eloquent `Model::whereNotLike` parity. Pattern is
5679 /// passed verbatim — caller controls `%` / `_`.
5680 ///
5681 /// # Errors
5682 /// As [`FetcherPool::fetch`].
5683 ///
5684 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5685 pub async fn where_not_like(
5686 col: &str,
5687 pattern: impl ::core::convert::Into<::std::string::String>,
5688 pool: &#root::sql::Pool,
5689 ) -> ::core::result::Result<
5690 ::std::vec::Vec<Self>,
5691 #root::sql::ExecError,
5692 > {
5693 use #root::sql::FetcherPool as _;
5694 let _key = ::std::format!("{}__not_like", col);
5695 #root::query::QuerySet::<Self>::default()
5696 .filter(
5697 &_key,
5698 #root::core::SqlValue::String(pattern.into()),
5699 )
5700 .fetch(pool)
5701 .await
5702 }
5703
5704 /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5705 /// case-insensitive `NOT LIKE` (PG native, MySQL /
5706 /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5707 ///
5708 /// # Errors
5709 /// As [`FetcherPool::fetch`].
5710 ///
5711 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5712 pub async fn where_not_ilike(
5713 col: &str,
5714 pattern: impl ::core::convert::Into<::std::string::String>,
5715 pool: &#root::sql::Pool,
5716 ) -> ::core::result::Result<
5717 ::std::vec::Vec<Self>,
5718 #root::sql::ExecError,
5719 > {
5720 use #root::sql::FetcherPool as _;
5721 let _key = ::std::format!("{}__not_ilike", col);
5722 #root::query::QuerySet::<Self>::default()
5723 .filter(
5724 &_key,
5725 #root::core::SqlValue::String(pattern.into()),
5726 )
5727 .fetch(pool)
5728 .await
5729 }
5730
5731 /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5732 /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5733 /// parity.
5734 ///
5735 /// # Errors
5736 /// As [`FetcherPool::fetch`].
5737 ///
5738 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5739 pub async fn where_not_between(
5740 col: &str,
5741 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5742 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5743 pool: &#root::sql::Pool,
5744 ) -> ::core::result::Result<
5745 ::std::vec::Vec<Self>,
5746 #root::sql::ExecError,
5747 > {
5748 use #root::sql::FetcherPool as _;
5749 let _key = ::std::format!("{}__not_between", col);
5750 let _vals = #root::core::SqlValue::List(::std::vec![
5751 ::core::convert::Into::into(lo),
5752 ::core::convert::Into::into(hi),
5753 ]);
5754 #root::query::QuerySet::<Self>::default()
5755 .filter(&_key, _vals)
5756 .fetch(pool)
5757 .await
5758 }
5759
5760 /// Returns the SQL table name for this model. Eloquent
5761 /// `$model->getTable()` parity.
5762 #[must_use]
5763 pub fn table_name() -> &'static str {
5764 <Self as #root::core::Model>::SCHEMA.table
5765 }
5766
5767 /// Returns the SQL column name of this model's primary
5768 /// key, or `None` when the model has no
5769 /// `#[rustango(primary_key)]`. Eloquent
5770 /// `$model->getKeyName()` parity.
5771 #[must_use]
5772 pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5773 <Self as #root::core::Model>::SCHEMA
5774 .primary_key()
5775 .map(|f| f.column)
5776 }
5777
5778 /// Fetch every row where `<col> BETWEEN lo AND hi`
5779 /// (inclusive on both ends — same as SQL). Eloquent
5780 /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5781 ///
5782 /// # Errors
5783 /// As [`FetcherPool::fetch`].
5784 ///
5785 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5786 pub async fn where_between(
5787 col: &str,
5788 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5789 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5790 pool: &#root::sql::Pool,
5791 ) -> ::core::result::Result<
5792 ::std::vec::Vec<Self>,
5793 #root::sql::ExecError,
5794 > {
5795 use #root::sql::FetcherPool as _;
5796 let _key = ::std::format!("{}__between", col);
5797 let _vals = #root::core::SqlValue::List(::std::vec![
5798 ::core::convert::Into::into(lo),
5799 ::core::convert::Into::into(hi),
5800 ]);
5801 #root::query::QuerySet::<Self>::default()
5802 .filter(&_key, _vals)
5803 .fetch(pool)
5804 .await
5805 }
5806
5807 /// Fetch the first row where `<col> = <val>`. Returns
5808 /// `Ok(None)` when no row matches. Eloquent
5809 /// `Model::firstWhere($col, $val)` parity.
5810 ///
5811 /// Thin wrapper over `QuerySet::<Self>::default()
5812 /// .filter(col, val).first(pool)`. Use this when you
5813 /// want one row identified by a non-PK column (e.g.
5814 /// `User::first_where_pool("email", "x@y.com", &pool)`).
5815 ///
5816 /// `val` accepts any value `Into<SqlValue>` so plain
5817 /// strings, ints, UUIDs, etc. all work.
5818 ///
5819 /// # Errors
5820 /// As `QuerySet::first`.
5821 pub async fn first_where(
5822 col: &str,
5823 val: impl ::core::convert::Into<#root::core::SqlValue>,
5824 pool: &#root::sql::Pool,
5825 ) -> ::core::result::Result<
5826 ::core::option::Option<Self>,
5827 #root::sql::ExecError,
5828 > {
5829 #root::query::QuerySet::<Self>::default()
5830 .filter(col, val)
5831 .first(pool)
5832 .await
5833 }
5834
5835 #value_method
5836
5837 /// Fetch the row with the largest `field` value —
5838 /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
5839 /// `Ok(None)` for an empty table. Eloquent
5840 /// `Model::latest($field)->first()` parity.
5841 /// Thin wrapper over `QuerySet::<Self>::default()
5842 /// .latest(field, pool)`.
5843 ///
5844 /// **Field name** is the Rust field ident as a string
5845 /// (not the SQL column). It is a runtime `&str`, so an
5846 /// unknown one compiles and surfaces as
5847 /// `ExecError::Query(QueryError::UnknownField)` when the
5848 /// query runs.
5849 ///
5850 /// # Errors
5851 /// As `QuerySet::latest`.
5852 pub async fn latest(
5853 field: &str,
5854 pool: &#root::sql::Pool,
5855 ) -> ::core::result::Result<
5856 ::core::option::Option<Self>,
5857 #root::sql::ExecError,
5858 > {
5859 #root::query::QuerySet::<Self>::default()
5860 .latest(field, pool)
5861 .await
5862 }
5863
5864 /// Sibling of [`Self::latest`] — fetches the row
5865 /// with the smallest `field` value (`ORDER BY <field>
5866 /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
5867 /// ->first()` parity.
5868 ///
5869 /// # Errors
5870 /// As [`Self::latest`].
5871 pub async fn earliest(
5872 field: &str,
5873 pool: &#root::sql::Pool,
5874 ) -> ::core::result::Result<
5875 ::core::option::Option<Self>,
5876 #root::sql::ExecError,
5877 > {
5878 #root::query::QuerySet::<Self>::default()
5879 .earliest(field, pool)
5880 .await
5881 }
5882
5883 #count_method
5884
5885 /// `true` when the table contains at least one row.
5886 /// Eloquent `Model::query()->exists()` parity. Thin wrapper
5887 /// over `QuerySet::<Self>::default().exists(pool)`.
5888 ///
5889 /// # Errors
5890 /// As [`ExistsPool::exists`].
5891 ///
5892 /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
5893 pub async fn exists(
5894 pool: &#root::sql::Pool,
5895 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5896 use #root::sql::ExistsPool as _;
5897 #root::query::QuerySet::<Self>::default()
5898 .exists(pool)
5899 .await
5900 }
5901
5902 /// Inverse of [`Self::exists`] — returns `true` when
5903 /// the table has zero rows. Eloquent
5904 /// `Model::doesntExist()` parity.
5905 ///
5906 /// # Errors
5907 /// As [`Self::exists`].
5908 pub async fn doesnt_exist(
5909 pool: &#root::sql::Pool,
5910 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5911 Self::exists(pool).await.map(|e| !e)
5912 }
5913
5914 /// Eloquent `Model::query()->whereKey($pk)->exists()` —
5915 /// `true` when a row with primary key `pk` exists in
5916 /// the table. Sugar over
5917 /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
5918 ///
5919 /// Differs from [`Self::exists`] (which checks "any row
5920 /// in the table") by checking a specific PK existence.
5921 /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
5922 /// because the row is never materialized — the SQL is
5923 /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
5924 ///
5925 /// # Errors
5926 /// As [`#root::sql::ExistsPool::contains_pk`].
5927 pub async fn contains_pk(
5928 pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
5929 pool: &#root::sql::Pool,
5930 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5931 use #root::sql::ExistsPool as _;
5932 #root::query::QuerySet::<Self>::default()
5933 .contains_pk(pool, pk)
5934 .await
5935 }
5936
5937 #sum_method
5938 #avg_method
5939 #min_method
5940 #max_method
5941
5942 /// Fetch every row of this model from `pool`. Eloquent
5943 /// `Model::all()` parity — a thin wrapper over
5944 /// `QuerySet::<Self>::default().fetch(pool)`.
5945 ///
5946 /// **Use with care on large tables** — there's no
5947 /// pagination or limit; the entire table is materialized
5948 /// into memory. For anything beyond fixture / lookup
5949 /// tables, page through `QuerySet::<Self>::default()
5950 /// .order_by(...).limit(N).offset(M).fetch(pool)`
5951 /// or stream via `.iterator(chunk_size)`.
5952 ///
5953 /// # Errors
5954 /// As [`FetcherPool::fetch`].
5955 ///
5956 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5957 pub async fn all(
5958 pool: &#root::sql::Pool,
5959 ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
5960 {
5961 use #root::sql::FetcherPool as _;
5962 #root::query::QuerySet::<Self>::default()
5963 .fetch(pool)
5964 .await
5965 }
5966
5967 /// Look up every row whose primary key is in `pks`.
5968 /// Returns the matching rows in **inventory** order — NOT
5969 /// the order of `pks`. Empty `pks` returns an empty
5970 /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
5971 /// with a list) parity.
5972 ///
5973 /// Thin wrapper over `QuerySet::<Self>::default()
5974 /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
5975 /// Caller-supplied PKs that don't match a row are
5976 /// silently skipped (the returned vec is shorter than
5977 /// the input list). For an order-preserving / "fail
5978 /// when any missing" variant, build the queryset
5979 /// explicitly with `in_bulk` instead.
5980 ///
5981 /// Accepts any iterable whose elements are
5982 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
5983 /// `Vec<Uuid>`, etc.
5984 ///
5985 /// # Errors
5986 /// As [`FetcherPool::fetch`].
5987 ///
5988 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5989 pub async fn find_many<V>(
5990 pks: impl ::core::iter::IntoIterator<Item = V>,
5991 pool: &#root::sql::Pool,
5992 ) -> ::core::result::Result<
5993 ::std::vec::Vec<Self>,
5994 #root::sql::ExecError,
5995 >
5996 where
5997 V: ::core::convert::Into<#root::core::SqlValue>,
5998 {
5999 use #root::sql::FetcherPool as _;
6000 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6001 pks.into_iter().map(::core::convert::Into::into).collect();
6002 if _values.is_empty() {
6003 return ::core::result::Result::Ok(::std::vec::Vec::new());
6004 }
6005 let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
6006 #root::query::QuerySet::<Self>::default()
6007 .filter(&_key, #root::core::SqlValue::List(_values))
6008 .fetch(pool)
6009 .await
6010 }
6011
6012 /// Look up the row whose primary key equals `pk`. Returns
6013 /// `Ok(None)` when no row matches, rather than erroring.
6014 /// Eloquent `Model::find`
6015 /// shape — accepts any value `Into<SqlValue>`.
6016 ///
6017 /// One-liner shortcut for the common
6018 /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6019 /// .limit(1).fetch(pool).await?.into_iter().next()`
6020 /// dance.
6021 ///
6022 /// # Errors
6023 /// As [`FetcherPool::fetch`].
6024 ///
6025 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6026 pub async fn find(
6027 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6028 pool: &#root::sql::Pool,
6029 ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6030 {
6031 use #root::sql::FetcherPool as _;
6032 let _pk_val: #root::core::SqlValue = pk.into();
6033 let mut _rows: ::std::vec::Vec<Self> =
6034 #root::query::QuerySet::<Self>::default()
6035 .filter(::core::stringify!(#pk_ident), _pk_val)
6036 .limit(1)
6037 .fetch(pool)
6038 .await?;
6039 ::core::result::Result::Ok(_rows.into_iter().next())
6040 }
6041
6042 /// Look up the row whose primary key equals `pk`. Errors
6043 /// when no row matches — the throwing counterpart of
6044 /// [`Self::find`]. Eloquent `Model::findOrFail`
6045 /// parity.
6046 ///
6047 /// Translates the miss into
6048 /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6049 /// so callers can `?`-bubble straight through the typical
6050 /// `ExecError` error chain.
6051 ///
6052 /// # Errors
6053 /// As [`Self::find`]; additionally
6054 /// [`sqlx::Error::RowNotFound`] when no row matches.
6055 ///
6056 /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6057 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6058 pub async fn find_or_fail(
6059 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6060 pool: &#root::sql::Pool,
6061 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6062 match Self::find(pk, pool).await? {
6063 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6064 ::core::option::Option::None => ::core::result::Result::Err(
6065 #root::sql::ExecError::Driver(
6066 #root::sql::sqlx::Error::RowNotFound,
6067 ),
6068 ),
6069 }
6070 }
6071
6072 /// Look up multiple rows by primary key, **failing** if
6073 /// any requested PK is missing. Eloquent
6074 /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6075 /// inventory order (NOT request order — same as
6076 /// [`Self::find_many`]).
6077 ///
6078 /// Empty `pks` returns an empty `Vec` (no rows requested
6079 /// → nothing to fail on).
6080 ///
6081 /// Differs from [`Self::find_many`] in that this method
6082 /// requires every PK to resolve. If even one is missing,
6083 /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6084 /// in `ExecError::Driver`). Rows are deduped at the SQL
6085 /// layer, so passing the same PK twice in `pks` counts as
6086 /// one expected row.
6087 ///
6088 /// # Errors
6089 /// As [`Self::find_many`], plus `RowNotFound` when the
6090 /// returned row count is less than the count of distinct
6091 /// requested PKs.
6092 pub async fn find_many_or_fail<V>(
6093 pks: impl ::core::iter::IntoIterator<Item = V>,
6094 pool: &#root::sql::Pool,
6095 ) -> ::core::result::Result<
6096 ::std::vec::Vec<Self>,
6097 #root::sql::ExecError,
6098 >
6099 where
6100 V: ::core::convert::Into<#root::core::SqlValue>,
6101 {
6102 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6103 pks.into_iter().map(::core::convert::Into::into).collect();
6104 if _values.is_empty() {
6105 return ::core::result::Result::Ok(::std::vec::Vec::new());
6106 }
6107 // Dedup the requested PK list before counting so
6108 // duplicate-PK requests don't false-fail (the SQL
6109 // `IN (...)` clause naturally dedups too).
6110 let mut _seen: ::std::collections::HashSet<
6111 ::std::string::String,
6112 > = ::std::collections::HashSet::new();
6113 for v in &_values {
6114 _seen.insert(v.to_display_string());
6115 }
6116 let _expected = _seen.len();
6117 let _rows = Self::find_many(_values, pool).await?;
6118 if _rows.len() < _expected {
6119 return ::core::result::Result::Err(
6120 #root::sql::ExecError::Driver(
6121 #root::sql::sqlx::Error::RowNotFound,
6122 ),
6123 );
6124 }
6125 ::core::result::Result::Ok(_rows)
6126 }
6127
6128 /// Find by primary key or run `fallback` to produce a
6129 /// default row to return. Eloquent
6130 /// `Model::findOr($pk, fn() => …)` parity.
6131 ///
6132 /// Unlike [`Self::find_or_fail`] (which raises on
6133 /// miss), this is the "give me something sensible"
6134 /// branch: typical use is "fetch the user's row, else
6135 /// fall back to an anonymous/guest stub".
6136 ///
6137 /// `fallback` runs only when no row matches — the DB
6138 /// round-trip happens unconditionally.
6139 ///
6140 /// # Errors
6141 /// As [`Self::find`].
6142 pub async fn find_or<F>(
6143 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6144 pool: &#root::sql::Pool,
6145 fallback: F,
6146 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6147 where
6148 F: ::core::ops::FnOnce() -> Self,
6149 {
6150 ::core::result::Result::Ok(
6151 Self::find(pk, pool).await?.unwrap_or_else(fallback),
6152 )
6153 }
6154
6155 /// Same as [`Self::find_or`] but also returns a `bool`
6156 /// indicating whether the row was found in the DB
6157 /// (`true`) or freshly built from `fallback` (`false`).
6158 /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6159 /// in PHP the returned model also exposes `->exists`;
6160 /// here that's surfaced as the second tuple element.
6161 ///
6162 /// Useful for edit-or-create form handlers where the
6163 /// caller needs to know whether to PATCH or POST when
6164 /// the user submits the form.
6165 ///
6166 /// # Errors
6167 /// As [`Self::find`].
6168 pub async fn find_or_new<F>(
6169 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6170 pool: &#root::sql::Pool,
6171 fallback: F,
6172 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6173 where
6174 F: ::core::ops::FnOnce() -> Self,
6175 {
6176 match Self::find(pk, pool).await? {
6177 ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6178 ::core::option::Option::None => {
6179 ::core::result::Result::Ok((fallback(), false))
6180 }
6181 }
6182 }
6183
6184 /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6185 /// [`Self::find_or_new`] but **persists** the new row
6186 /// when the PK isn't found. Returns `(row, exists: bool)`
6187 /// — `exists=true` when the PK was found,
6188 /// `false` when a fresh row was inserted.
6189 ///
6190 /// ```ignore
6191 /// let (post, found) = Post::find_or_insert(
6192 /// pk,
6193 /// &pool,
6194 /// || Post { id: Auto::default(), title: "new".into() },
6195 /// ).await?;
6196 /// // `found` true → returned existing row;
6197 /// // `found` false → fallback was inserted, `post.id` populated.
6198 /// ```
6199 ///
6200 /// **Caveat**: two concurrent calls that both miss the
6201 /// find can both INSERT, violating uniqueness. Wrap in a
6202 /// transaction or rely on a UNIQUE constraint + handle
6203 /// the conflict error if you need race-free semantics.
6204 ///
6205 /// # Errors
6206 /// As [`Self::find`] and [`Self::save_pool`].
6207 pub async fn find_or_insert<F>(
6208 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6209 pool: &#root::sql::Pool,
6210 fallback: F,
6211 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6212 where
6213 F: ::core::ops::FnOnce() -> Self,
6214 {
6215 if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6216 return ::core::result::Result::Ok((_row, true));
6217 }
6218 let mut _new = fallback();
6219 _new.save_pool(pool).await?;
6220 ::core::result::Result::Ok((_new, false))
6221 }
6222
6223 /// Fetch the first row of the table, or run `fallback`
6224 /// when the table is empty. Eloquent
6225 /// `Model::firstOr(fn() => …)` parity.
6226 ///
6227 /// # Errors
6228 /// As [`Self::first`].
6229 pub async fn first_or<F>(
6230 pool: &#root::sql::Pool,
6231 fallback: F,
6232 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6233 where
6234 F: ::core::ops::FnOnce() -> Self,
6235 {
6236 // Route through the queryset rather than `Self::first`,
6237 // which is suppressed on models with a field named
6238 // `first` (field/shortcut collision guard).
6239 ::core::result::Result::Ok(
6240 #root::query::QuerySet::<Self>::default()
6241 .first(pool)
6242 .await?
6243 .unwrap_or_else(fallback),
6244 )
6245 }
6246
6247 /// Fetch exactly one row matching `<col> = <val>`. Errors
6248 /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6249 /// or more than one matches
6250 /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6251 /// Eloquent `Model::sole($col, $val)` parity.
6252 ///
6253 /// # Errors
6254 /// As [`Self::where_`] plus the explicit
6255 /// `RowNotFound` / `MultipleRowsReturned` cases above.
6256 pub async fn sole(
6257 col: &str,
6258 val: impl ::core::convert::Into<#root::core::SqlValue>,
6259 pool: &#root::sql::Pool,
6260 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6261 let mut _rows = Self::where_(col, val, pool).await?;
6262 match _rows.len() {
6263 0 => ::core::result::Result::Err(
6264 #root::sql::ExecError::Driver(
6265 #root::sql::sqlx::Error::RowNotFound,
6266 ),
6267 ),
6268 1 => ::core::result::Result::Ok(_rows.remove(0)),
6269 n => ::core::result::Result::Err(
6270 #root::sql::ExecError::MultipleRowsReturned {
6271 op: "sole",
6272 table: <Self as #root::core::Model>::SCHEMA.name,
6273 count: n,
6274 },
6275 ),
6276 }
6277 }
6278 }
6279 } else {
6280 quote!()
6281 };
6282
6283 // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6284 // the non-audited `_pool` methods but execute against an open
6285 // `PoolTx` so the writes participate in the caller's transaction.
6286 // Auditing inside TX is deferred; these always use the plain
6287 // executor primitives regardless of whether the model is audited.
6288 let tx_insert_method = if fields.has_auto {
6289 let pushes = &fields.insert_pushes;
6290 let returning_cols = &fields.returning_cols;
6291 quote! {
6292 /// Insert this row inside an open transaction, populating
6293 /// any `Auto<T>` PK from the auto-assigned value. Works
6294 /// against any backend that `tx` wraps.
6295 ///
6296 /// # Errors
6297 /// As [`Self::insert_pool`].
6298 pub async fn insert_tx(
6299 &mut self,
6300 tx: &mut #root::sql::PoolTx<'_>,
6301 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6302 let mut _columns: ::std::vec::Vec<&'static str> =
6303 ::std::vec::Vec::new();
6304 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6305 ::std::vec::Vec::new();
6306 #( #pushes )*
6307 let _query = #root::core::InsertQuery::new(
6308 <Self as #root::core::Model>::SCHEMA,
6309 _columns,
6310 _values,
6311 )
6312 .returning(::std::vec![ #( #returning_cols ),* ]);
6313 let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6314 #root::sql::apply_auto_pk(_result, self)
6315 }
6316 }
6317 } else {
6318 let insert_columns = &fields.insert_columns;
6319 let insert_values = &fields.insert_values;
6320 quote! {
6321 /// Insert this row inside an open transaction.
6322 ///
6323 /// # Errors
6324 /// As [`Self::insert_pool`].
6325 pub async fn insert_tx(
6326 &self,
6327 tx: &mut #root::sql::PoolTx<'_>,
6328 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6329 let _query = #root::core::InsertQuery::new(
6330 <Self as #root::core::Model>::SCHEMA,
6331 ::std::vec![ #( #insert_columns ),* ],
6332 ::std::vec![ #( #insert_values ),* ],
6333 );
6334 #root::sql::insert_tx(tx, &_query).await
6335 }
6336 }
6337 };
6338
6339 let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6340 let pk_column_lit = pk_col.as_str();
6341 let assignments = &fields.update_assignments;
6342 let dispatch_unset = if fields.pk_is_auto {
6343 quote! {
6344 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6345 return self.insert_tx(tx).await.map(|()| 1u64);
6346 }
6347 }
6348 } else {
6349 quote!()
6350 };
6351 quote! {
6352 /// Save this row inside an open transaction. `INSERT` when
6353 /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6354 /// PK. Works against any backend that `tx` wraps.
6355 ///
6356 /// # Errors
6357 /// As [`Self::save_pool`].
6358 pub async fn save_tx(
6359 &mut self,
6360 tx: &mut #root::sql::PoolTx<'_>,
6361 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6362 #dispatch_unset
6363 let _query = #root::core::UpdateQuery::new(
6364 <Self as #root::core::Model>::SCHEMA,
6365 ::std::vec![ #( #assignments ),* ],
6366 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6367 #pk_column_lit,
6368 #root::core::Op::Eq,
6369 ::core::convert::Into::<#root::core::SqlValue>::into(
6370 ::core::clone::Clone::clone(&self.#pk_ident)
6371 ),
6372 )),
6373 );
6374 let _affected = #root::sql::update_tx(tx, &_query).await?;
6375 ::core::result::Result::Ok(_affected)
6376 }
6377 }
6378 } else {
6379 quote!()
6380 };
6381
6382 let tx_delete_method = {
6383 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6384 let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6385 if let Some(pk_ident) = pk_ident_for_tx {
6386 quote! {
6387 /// Delete the row identified by this instance's PK
6388 /// inside an open transaction. Works against any backend
6389 /// that `tx` wraps.
6390 ///
6391 /// # Errors
6392 /// As [`Self::delete_pool`].
6393 pub async fn delete_tx(
6394 &self,
6395 tx: &mut #root::sql::PoolTx<'_>,
6396 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6397 let _query = #root::core::DeleteQuery::by_pk(
6398 <Self as #root::core::Model>::SCHEMA,
6399 #pk_column_lit,
6400 ::core::convert::Into::<#root::core::SqlValue>::into(
6401 ::core::clone::Clone::clone(&self.#pk_ident)
6402 ),
6403 );
6404 #root::sql::delete_tx(tx, &_query).await
6405 }
6406 }
6407 } else {
6408 quote!()
6409 }
6410 };
6411
6412 // Update emission captures both BEFORE and AFTER state — runs an
6413 // extra SELECT against `_executor` BEFORE the UPDATE, captures
6414 // each tracked field's prior value, then after the UPDATE diffs
6415 // against the in-memory `&self`. `diff_changes` drops unchanged
6416 // columns so the JSON only contains the actual delta.
6417 //
6418 // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6419 // and binds `_audit_before_pairs`; `audit_update_post` runs
6420 // after the UPDATE and emits the PendingEntry.
6421 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6422 audited_fields
6423 {
6424 if tracked.is_empty() {
6425 (quote!(), quote!())
6426 } else {
6427 let select_cols: String = tracked
6428 .iter()
6429 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
6430 .collect::<Vec<_>>()
6431 .join(", ");
6432 let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6433 let select_cols_lit = select_cols;
6434 let pk_column_lit_for_select = pk_column_for_select;
6435 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6436 if fields.pk_is_auto {
6437 quote!(self.#pk_ident.get().copied().unwrap_or_default())
6438 } else {
6439 quote!(::core::clone::Clone::clone(&self.#pk_ident))
6440 }
6441 } else {
6442 quote!(0_i64)
6443 };
6444 let before_pairs = tracked.iter().map(|c| {
6445 let column_lit = c.column.as_str();
6446 let value_ty = &c.value_ty;
6447 quote! {
6448 (
6449 #column_lit,
6450 match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6451 &_audit_before_row, #column_lit,
6452 ) {
6453 ::core::result::Result::Ok(v) => {
6454 #root::__serde_json::to_value(&v)
6455 .unwrap_or(#root::__serde_json::Value::Null)
6456 }
6457 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6458 },
6459 )
6460 }
6461 });
6462 let after_pairs = tracked.iter().map(|c| {
6463 let column_lit = c.column.as_str();
6464 let ident = &c.ident;
6465 quote! {
6466 (
6467 #column_lit,
6468 #root::__serde_json::to_value(&self.#ident)
6469 .unwrap_or(#root::__serde_json::Value::Null),
6470 )
6471 }
6472 });
6473 let pk_str = audit_pk_to_string.clone();
6474 let pre = quote! {
6475 let _audit_select_sql = ::std::format!(
6476 r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
6477 #select_cols_lit,
6478 <Self as #root::core::Model>::SCHEMA.table,
6479 #pk_column_lit_for_select,
6480 );
6481 let _audit_before_pairs:
6482 ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6483 match #root::sql::sqlx::query(&_audit_select_sql)
6484 .bind(#pk_value_for_bind)
6485 .fetch_optional(&mut *_executor)
6486 .await
6487 {
6488 ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
6489 ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
6490 }
6491 _ => ::core::option::Option::None,
6492 };
6493 };
6494 let post = quote! {
6495 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6496 let _audit_after:
6497 ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
6498 ::std::vec![ #( #after_pairs ),* ];
6499 let _audit_entry = #root::audit::PendingEntry {
6500 entity_table: <Self as #root::core::Model>::SCHEMA.table,
6501 entity_pk: #pk_str,
6502 operation: #root::audit::AuditOp::Update,
6503 source: #root::audit::current_source(),
6504 changes: #root::audit::diff_changes(
6505 &_audit_before,
6506 &_audit_after,
6507 ),
6508 };
6509 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6510 }
6511 };
6512 (pre, post)
6513 }
6514 } else {
6515 (quote!(), quote!())
6516 };
6517
6518 // Bulk-insert audit: capture every row's tracked fields after the
6519 // RETURNING populates each PK, then push one batched INSERT INTO
6520 // audit_log via `emit_many`. One round-trip regardless of N rows.
6521 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6522 let row_pk_str = if let Some((pk_ident, _)) = primary_key {
6523 if fields.pk_is_auto {
6524 quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
6525 } else {
6526 quote!(::std::format!("{}", &_row.#pk_ident))
6527 }
6528 } else {
6529 quote!(::std::string::String::new())
6530 };
6531 let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
6532 let column_lit = c.column.as_str();
6533 let ident = &c.ident;
6534 quote! {
6535 (
6536 #column_lit,
6537 #root::__serde_json::to_value(&_row.#ident)
6538 .unwrap_or(#root::__serde_json::Value::Null),
6539 )
6540 }
6541 });
6542 quote! {
6543 let _audit_source = #root::audit::current_source();
6544 let mut _audit_entries:
6545 ::std::vec::Vec<#root::audit::PendingEntry> =
6546 ::std::vec::Vec::with_capacity(rows.len());
6547 for _row in rows.iter() {
6548 _audit_entries.push(#root::audit::PendingEntry {
6549 entity_table: <Self as #root::core::Model>::SCHEMA.table,
6550 entity_pk: #row_pk_str,
6551 operation: #root::audit::AuditOp::Create,
6552 source: _audit_source.clone(),
6553 changes: #root::audit::snapshot_changes(&[
6554 #( #row_pairs ),*
6555 ]),
6556 });
6557 }
6558 #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6559 }
6560 } else {
6561 quote!()
6562 };
6563
6564 let save_method = if fields.pk_is_auto {
6565 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6566 let pk_column_lit = pk_column.as_str();
6567 let assignments = &fields.update_assignments;
6568 let upsert_cols = &fields.upsert_update_columns;
6569 let upsert_pushes = &fields.insert_pushes;
6570 let upsert_returning = &fields.returning_cols;
6571 let upsert_auto_assigns = &fields.auto_assigns;
6572 // Conflict target: prefer the first declared `unique_together`
6573 // when it exists. Plain `Auto<T>` PKs are server-assigned via
6574 // `BIGSERIAL` and never collide on insert, so a PK-only target
6575 // would silently turn `upsert()` into "always-insert" for
6576 // surrogate-PK models with composite UNIQUE constraints — see
6577 // `RolePermission` / `UserRole` / `UserPermission` in the
6578 // tenancy permission engine. When no `unique_together` is
6579 // declared we keep the PK target (the original behaviour).
6580 let upsert_target_columns: Vec<String> = indexes
6581 .iter()
6582 .find(|i| i.unique && !i.columns.is_empty())
6583 .map(|i| i.columns.clone())
6584 .unwrap_or_else(|| vec![pk_column.clone()]);
6585 let upsert_target_lits = upsert_target_columns
6586 .iter()
6587 .map(String::as_str)
6588 .collect::<Vec<_>>();
6589 let conflict_clause = if fields.upsert_update_columns.is_empty() {
6590 quote!(#root::core::ConflictClause::DoNothing)
6591 } else {
6592 quote!(#root::core::ConflictClause::DoUpdate {
6593 target: ::std::vec![ #( #upsert_target_lits ),* ],
6594 update_columns: ::std::vec![ #( #upsert_cols ),* ],
6595 })
6596 };
6597 Some(quote! {
6598 /// Insert this row if its `Auto<T>` primary key is
6599 /// `Unset`, otherwise update the existing row matching the
6600 /// PK. The caller doesn't need to pick `insert` vs the
6601 /// bulk-update path manually.
6602 ///
6603 /// On the insert branch, populates the PK from `RETURNING`
6604 /// (same behavior as `insert`). On the update branch,
6605 /// writes every non-PK column back; if no row matches the
6606 /// PK, returns `Ok(())` silently.
6607 ///
6608 /// Only generated when the primary key is declared as
6609 /// `Auto<T>`. Models with a manually-managed PK must use
6610 /// `insert` or the QuerySet update builder.
6611 ///
6612 /// **Postgres only — prefer [`Self::save_pool`].** This takes a
6613 /// driver-specific `PgPool`, so it does not exist at all on a
6614 /// build without the `postgres` feature: selecting `sqlite`
6615 /// makes the method vanish rather than fail with anything that
6616 /// names the cause.
6617 ///
6618 /// [`Self::save_pool`] takes `rustango::sql::Pool` and works on
6619 /// all three backends. The naming is inverted — the short name
6620 /// is the narrow one — and that is tracked in #1293.
6621 ///
6622 /// # Errors
6623 /// Returns [`#root::sql::ExecError`] for SQL-writing
6624 /// or driver failures.
6625 #[cfg(feature = "postgres")]
6626 pub async fn save(
6627 &mut self,
6628 pool: &#root::sql::sqlx::PgPool,
6629 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6630 #pool_to_save_on
6631 }
6632
6633 /// Like [`Self::save`] but accepts any sqlx executor —
6634 /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6635 /// escape hatch for tenant-scoped writes: schema-mode
6636 /// tenants share the registry pool but rely on a per-
6637 /// checkout `SET search_path`, so passing `&PgPool` would
6638 /// silently hit the wrong schema. Acquire a connection
6639 /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6640 ///
6641 /// # Errors
6642 /// As [`Self::save`].
6643 #[cfg(feature = "postgres")]
6644 pub async fn save_on #executor_generics (
6645 &mut self,
6646 #executor_param,
6647 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6648 #executor_where
6649 {
6650 // #1029 — INSERT writes exactly one row → 1; UPDATE returns
6651 // the rows-affected count (0 when the PK no longer exists).
6652 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6653 return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
6654 }
6655 #audit_update_pre
6656 let _query = #root::core::UpdateQuery::new(
6657 <Self as #root::core::Model>::SCHEMA,
6658 ::std::vec![ #( #assignments ),* ],
6659 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6660 #pk_column_lit,
6661 #root::core::Op::Eq,
6662 ::core::convert::Into::<#root::core::SqlValue>::into(
6663 ::core::clone::Clone::clone(&self.#pk_ident)
6664 ),
6665 )),
6666 );
6667 let _affected = #root::sql::__macro_internals::update_on(
6668 #executor_passes_to_data_write,
6669 &_query,
6670 ).await?;
6671 #audit_update_post
6672 ::core::result::Result::Ok(_affected)
6673 }
6674
6675 /// Per-call override for the audit source. Runs
6676 /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6677 /// scope so the resulting audit entry records `source`
6678 /// instead of the task-local default. Useful for seed
6679 /// scripts and one-off CLI tools that don't sit inside an
6680 /// admin handler. The override applies only to this call;
6681 /// no global state changes.
6682 ///
6683 /// # Errors
6684 /// As [`Self::save_on`].
6685 #[cfg(feature = "postgres")]
6686 pub async fn save_on_with #executor_generics (
6687 &mut self,
6688 #executor_param,
6689 source: #root::audit::AuditSource,
6690 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6691 #executor_where
6692 {
6693 #root::audit::with_source(source, self.save_on(_executor)).await
6694 }
6695
6696 /// Insert this row or update it in-place if the primary key already
6697 /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6698 ///
6699 /// With `Auto::Unset` PK the server assigns a new key and no conflict
6700 /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6701 /// inserted if absent or all non-PK columns are overwritten if present.
6702 ///
6703 /// # Errors
6704 /// As [`Self::insert_on`].
6705 #[cfg(feature = "postgres")]
6706 pub async fn upsert(
6707 &mut self,
6708 pool: &#root::sql::sqlx::PgPool,
6709 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6710 #pool_to_upsert_on
6711 }
6712
6713 /// Like [`Self::upsert`] but accepts any sqlx executor.
6714 /// See [`Self::save_on`] for tenancy-scoped rationale.
6715 ///
6716 /// # Errors
6717 /// As [`Self::upsert`].
6718 #[cfg(feature = "postgres")]
6719 pub async fn upsert_on #executor_generics (
6720 &mut self,
6721 #executor_param,
6722 ) -> ::core::result::Result<(), #root::sql::ExecError>
6723 #executor_where
6724 {
6725 let mut _columns: ::std::vec::Vec<&'static str> =
6726 ::std::vec::Vec::new();
6727 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6728 ::std::vec::Vec::new();
6729 #( #upsert_pushes )*
6730 let query = #root::core::InsertQuery::new(
6731 <Self as #root::core::Model>::SCHEMA,
6732 _columns,
6733 _values,
6734 )
6735 .returning(::std::vec![ #( #upsert_returning ),* ])
6736 .on_conflict(#conflict_clause);
6737 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
6738 #executor_passes_to_data_write,
6739 &query,
6740 ).await?;
6741 let _returning_row = &_returning_row_v;
6742 #( #upsert_auto_assigns )*
6743 ::core::result::Result::Ok(())
6744 }
6745 })
6746 } else {
6747 None
6748 };
6749
6750 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6751 let pk_column_lit = pk_column.as_str();
6752 // Optional `soft_delete_on` / `restore_on` companions when the
6753 // model has a `#[rustango(soft_delete)]` column. They land
6754 // alongside the regular `delete_on` so callers have both
6755 // options — a hard delete (audit-tracked as a real DELETE) and
6756 // a logical delete (audit-tracked as an UPDATE setting the
6757 // deleted_at column to NOW()).
6758 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6759 let col_lit = col;
6760 let sd_field_ident = fields
6761 .soft_delete_field_ident
6762 .clone()
6763 .expect("soft_delete_column without ident");
6764 quote! {
6765 /// Soft-delete this row by setting its
6766 /// `#[rustango(soft_delete)]` column to `NOW()`.
6767 /// The row stays in the table; query helpers can
6768 /// filter it out by checking the column for `IS NOT
6769 /// NULL`.
6770 ///
6771 /// # Errors
6772 /// As [`Self::delete`].
6773 #[cfg(feature = "postgres")]
6774 pub async fn soft_delete_on #executor_generics (
6775 &self,
6776 #executor_param,
6777 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6778 #executor_where
6779 {
6780 let _now = #root::__chrono::Utc::now();
6781 let _query = #root::core::UpdateQuery::new(
6782 <Self as #root::core::Model>::SCHEMA,
6783 ::std::vec![#root::core::Assignment::new(
6784 #col_lit,
6785 ::core::convert::Into::<#root::core::SqlValue>::into(_now),
6786 )],
6787 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6788 #pk_column_lit,
6789 #root::core::Op::Eq,
6790 ::core::convert::Into::<#root::core::SqlValue>::into(
6791 ::core::clone::Clone::clone(&self.#pk_ident)
6792 ),
6793 )),
6794 );
6795 let _affected = #root::sql::__macro_internals::update_on(
6796 #executor_passes_to_data_write,
6797 &_query,
6798 ).await?;
6799 #audit_softdelete_emit
6800 ::core::result::Result::Ok(_affected)
6801 }
6802
6803 /// Inverse of [`Self::soft_delete_on`] — clears the
6804 /// soft-delete column back to NULL so the row is
6805 /// considered live again.
6806 ///
6807 /// # Errors
6808 /// As [`Self::delete`].
6809 #[cfg(feature = "postgres")]
6810 pub async fn restore_on #executor_generics (
6811 &self,
6812 #executor_param,
6813 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6814 #executor_where
6815 {
6816 let _query = #root::core::UpdateQuery::new(
6817 <Self as #root::core::Model>::SCHEMA,
6818 ::std::vec![#root::core::Assignment::new(
6819 #col_lit,
6820 #root::core::SqlValue::Null,
6821 )],
6822 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6823 #pk_column_lit,
6824 #root::core::Op::Eq,
6825 ::core::convert::Into::<#root::core::SqlValue>::into(
6826 ::core::clone::Clone::clone(&self.#pk_ident)
6827 ),
6828 )),
6829 );
6830 let _affected = #root::sql::__macro_internals::update_on(
6831 #executor_passes_to_data_write,
6832 &_query,
6833 ).await?;
6834 #audit_restore_emit
6835 ::core::result::Result::Ok(_affected)
6836 }
6837
6838 /// Tri-dialect counterpart of [`Self::soft_delete_on`]
6839 /// — takes [`#root::sql::Pool`] and dispatches per
6840 /// backend. Eloquent `Model::delete()` semantics on
6841 /// soft-delete-enabled models (closes #821 partial).
6842 ///
6843 /// Sets the `#[rustango(soft_delete)]` column to
6844 /// `NOW()` on every backend. Query helpers
6845 /// (`QuerySet::active()` / `only_trashed()`,
6846 /// `soft_delete::active_filter` /
6847 /// `compose_with_active`) filter trashed rows out by
6848 /// reading `IS NULL` on the same column.
6849 ///
6850 /// # Errors
6851 /// As [`#root::sql::update_pool`].
6852 pub async fn soft_delete(
6853 &self,
6854 pool: &#root::sql::Pool,
6855 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6856 let _now = #root::__chrono::Utc::now();
6857 let _query = #root::core::UpdateQuery::new(
6858 <Self as #root::core::Model>::SCHEMA,
6859 ::std::vec![#root::core::Assignment::new(
6860 #col_lit,
6861 ::core::convert::Into::<#root::core::SqlValue>::into(_now),
6862 )],
6863 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6864 #pk_column_lit,
6865 #root::core::Op::Eq,
6866 ::core::convert::Into::<#root::core::SqlValue>::into(
6867 ::core::clone::Clone::clone(&self.#pk_ident)
6868 ),
6869 )),
6870 );
6871 #pool_softdelete_run
6872 }
6873
6874 /// Tri-dialect counterpart of [`Self::restore_on`].
6875 /// Clears the `#[rustango(soft_delete)]` column back
6876 /// to `NULL`, marking the row live again. Eloquent
6877 /// `Model::restore()` parity.
6878 ///
6879 /// # Errors
6880 /// As [`#root::sql::update_pool`].
6881 pub async fn restore(
6882 &self,
6883 pool: &#root::sql::Pool,
6884 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6885 let _query = #root::core::UpdateQuery::new(
6886 <Self as #root::core::Model>::SCHEMA,
6887 ::std::vec![#root::core::Assignment::new(
6888 #col_lit,
6889 #root::core::SqlValue::Null,
6890 )],
6891 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6892 #pk_column_lit,
6893 #root::core::Op::Eq,
6894 ::core::convert::Into::<#root::core::SqlValue>::into(
6895 ::core::clone::Clone::clone(&self.#pk_ident)
6896 ),
6897 )),
6898 );
6899 #pool_restore_run
6900 }
6901
6902 /// Hard-delete this row, ignoring the soft-delete
6903 /// column. Eloquent `Model::forceDelete()` parity —
6904 /// the escape hatch when you need to actually purge
6905 /// data (GDPR, fixture cleanup, etc.).
6906 ///
6907 /// Equivalent to [`Self::delete_pool`] (the framework's
6908 /// non-soft delete) — exposed under the Eloquent name
6909 /// for muscle-memory + so soft-delete-enabled models
6910 /// have all three operations (soft / restore / force)
6911 /// in one place.
6912 ///
6913 /// # Errors
6914 /// As [`Self::delete_pool`].
6915 pub async fn force_delete(
6916 &self,
6917 pool: &#root::sql::Pool,
6918 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6919 Self::delete_pool(self, pool).await
6920 }
6921
6922 /// Returns `true` when this row is soft-deleted (its
6923 /// `#[rustango(soft_delete)]` column is currently
6924 /// set — Eloquent `$model->trashed()` parity).
6925 ///
6926 /// Pure in-memory predicate over `&self`; does not
6927 /// hit the database. Useful in admin/template code
6928 /// like `{% if post.trashed() %}…{% endif %}` and in
6929 /// guard clauses on restore/force-delete flows.
6930 pub fn trashed(&self) -> bool {
6931 ::core::option::Option::is_some(&self.#sd_field_ident)
6932 }
6933
6934 /// Fetch every row whose `#[rustango(soft_delete)]`
6935 /// column is `NULL` (a.k.a. the live, non-trashed
6936 /// rows). Eloquent's default `Model::all()` behavior on
6937 /// a soft-delete model (Eloquent auto-scopes trashed
6938 /// rows out; rustango doesn't have auto-scoping yet —
6939 /// see issue #820 — so this is the explicit shortcut).
6940 ///
6941 /// One-liner over `QuerySet::<Self>::default()
6942 /// .active().fetch(pool)`. Closes #821 partial.
6943 ///
6944 /// # Errors
6945 /// As [`#root::sql::FetcherPool::fetch`].
6946 pub async fn active(
6947 pool: &#root::sql::Pool,
6948 ) -> ::core::result::Result<
6949 ::std::vec::Vec<Self>,
6950 #root::sql::ExecError,
6951 > {
6952 use #root::sql::FetcherPool as _;
6953 #root::query::QuerySet::<Self>::default()
6954 .active()
6955 .fetch(pool)
6956 .await
6957 }
6958
6959 /// Fetch ONLY soft-deleted rows. Eloquent
6960 /// `Model::onlyTrashed()->get()` parity — drives the
6961 /// admin "Trash" page, restore flows, GDPR purge
6962 /// scans, etc.
6963 ///
6964 /// One-liner over `QuerySet::<Self>::default()
6965 /// .only_trashed().fetch(pool)`. Closes #821
6966 /// partial.
6967 ///
6968 /// # Errors
6969 /// As [`#root::sql::FetcherPool::fetch`].
6970 pub async fn only_trashed(
6971 pool: &#root::sql::Pool,
6972 ) -> ::core::result::Result<
6973 ::std::vec::Vec<Self>,
6974 #root::sql::ExecError,
6975 > {
6976 use #root::sql::FetcherPool as _;
6977 #root::query::QuerySet::<Self>::default()
6978 .only_trashed()
6979 .fetch(pool)
6980 .await
6981 }
6982
6983 /// Fetch every row, both live and soft-deleted.
6984 /// Eloquent `Model::withTrashed()->get()` parity.
6985 ///
6986 /// Today every queryset already includes trashed rows
6987 /// (rustango has no global-scope tracking yet — issue
6988 /// #820), so this is functionally equivalent to
6989 /// [`Self::all`]. Exposed as a named shortcut so
6990 /// soft-delete-aware code reads `Model::with_trashed_pool`
6991 /// rather than `Model::all_pool` — keeps intent visible
6992 /// in callers and stays correct when auto-scoping lands.
6993 ///
6994 /// Closes #821 partial.
6995 ///
6996 /// # Errors
6997 /// As [`#root::sql::FetcherPool::fetch`].
6998 pub async fn with_trashed(
6999 pool: &#root::sql::Pool,
7000 ) -> ::core::result::Result<
7001 ::std::vec::Vec<Self>,
7002 #root::sql::ExecError,
7003 > {
7004 use #root::sql::FetcherPool as _;
7005 #root::query::QuerySet::<Self>::default()
7006 .with_trashed()
7007 .fetch(pool)
7008 .await
7009 }
7010
7011 }
7012 } else {
7013 quote!()
7014 };
7015 quote! {
7016 /// Delete the row identified by this instance's primary key.
7017 ///
7018 /// Returns the number of rows affected (0 or 1).
7019 ///
7020 /// **Postgres only — prefer [`Self::delete_pool`].** This takes
7021 /// a driver-specific `PgPool`, so it does not exist on a build
7022 /// without the `postgres` feature. [`Self::delete_pool`] takes
7023 /// `rustango::sql::Pool` and works on all three. The naming is
7024 /// inverted; tracked in #1293.
7025 ///
7026 /// # Errors
7027 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7028 /// driver failures.
7029 #[cfg(feature = "postgres")]
7030 pub async fn delete(
7031 &self,
7032 pool: &#root::sql::sqlx::PgPool,
7033 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7034 #pool_to_delete_on
7035 }
7036
7037 /// Like [`Self::delete`] but accepts any sqlx executor —
7038 /// for tenant-scoped deletes against an explicitly-acquired
7039 /// connection. See [`Self::save_on`] for the rationale.
7040 ///
7041 /// # Errors
7042 /// As [`Self::delete`].
7043 #[cfg(feature = "postgres")]
7044 pub async fn delete_on #executor_generics (
7045 &self,
7046 #executor_param,
7047 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7048 #executor_where
7049 {
7050 let query = #root::core::DeleteQuery::by_pk(
7051 <Self as #root::core::Model>::SCHEMA,
7052 #pk_column_lit,
7053 ::core::convert::Into::<#root::core::SqlValue>::into(
7054 ::core::clone::Clone::clone(&self.#pk_ident)
7055 ),
7056 );
7057 let _affected = #root::sql::__macro_internals::delete_on(
7058 #executor_passes_to_data_write,
7059 &query,
7060 ).await?;
7061 #audit_delete_emit
7062 ::core::result::Result::Ok(_affected)
7063 }
7064
7065 /// Per-call audit-source override for [`Self::delete_on`].
7066 /// See [`Self::save_on_with`] for shape rationale.
7067 ///
7068 /// # Errors
7069 /// As [`Self::delete_on`].
7070 #[cfg(feature = "postgres")]
7071 pub async fn delete_on_with #executor_generics (
7072 &self,
7073 #executor_param,
7074 source: #root::audit::AuditSource,
7075 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7076 #executor_where
7077 {
7078 #root::audit::with_source(source, self.delete_on(_executor)).await
7079 }
7080 #pool_delete_method
7081 #pool_insert_method
7082 #audit_entry_method
7083 #pool_save_method
7084 #refresh_replicate_methods
7085 #tx_delete_method
7086 #tx_insert_method
7087 #tx_save_method
7088 #soft_delete_methods
7089
7090 /// Returns `true` when `other` represents the same DB
7091 /// row as `self` — i.e. their primary keys compare
7092 /// equal. Eloquent `$model->is($other)` parity.
7093 ///
7094 /// Because both arguments are typed `&Self`, the
7095 /// model/table check is automatic — `Post::is` cannot
7096 /// be invoked against a `Comment` at compile time. Only
7097 /// the PK has to be compared at runtime.
7098 pub fn is(&self, other: &Self) -> bool {
7099 self.#pk_ident == other.#pk_ident
7100 }
7101
7102 /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7103 /// parity.
7104 pub fn is_not(&self, other: &Self) -> bool {
7105 self.#pk_ident != other.#pk_ident
7106 }
7107
7108 /// Returns this row's primary-key value as an
7109 /// [`#root::core::SqlValue`]. Eloquent
7110 /// `$model->getKey()` parity.
7111 ///
7112 /// Useful when you need to thread the PK through a
7113 /// generic `Into<SqlValue>`-bound API without knowing the
7114 /// concrete PK type (`i64` vs `Uuid` vs `String`).
7115 #[must_use]
7116 pub fn get_key(&self) -> #root::core::SqlValue {
7117 ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7118 }
7119
7120 }
7121 });
7122
7123 let insert_method = if fields.has_auto {
7124 let pushes = &fields.insert_pushes;
7125 let returning_cols = &fields.returning_cols;
7126 let auto_assigns = &fields.auto_assigns;
7127 quote! {
7128 /// Insert this row into its table. Skips columns whose
7129 /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7130 /// sequence fills them in, then reads each `Auto` column
7131 /// back via `RETURNING` and stores it on `self`.
7132 ///
7133 /// **Postgres only — prefer [`Self::insert_pool`].** This takes
7134 /// a driver-specific `PgPool`, so it does not exist on a build
7135 /// without the `postgres` feature. [`Self::insert_pool`] takes
7136 /// `rustango::sql::Pool` and works on all three. The naming is
7137 /// inverted; tracked in #1293.
7138 ///
7139 /// # Errors
7140 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7141 /// driver failures.
7142 #[cfg(feature = "postgres")]
7143 pub async fn insert(
7144 &mut self,
7145 pool: &#root::sql::sqlx::PgPool,
7146 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7147 #pool_to_insert_on
7148 }
7149
7150 /// Like [`Self::insert`] but accepts any sqlx executor.
7151 /// See [`Self::save_on`] for tenancy-scoped rationale.
7152 ///
7153 /// # Errors
7154 /// As [`Self::insert`].
7155 #[cfg(feature = "postgres")]
7156 pub async fn insert_on #executor_generics (
7157 &mut self,
7158 #executor_param,
7159 ) -> ::core::result::Result<(), #root::sql::ExecError>
7160 #executor_where
7161 {
7162 let mut _columns: ::std::vec::Vec<&'static str> =
7163 ::std::vec::Vec::new();
7164 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7165 ::std::vec::Vec::new();
7166 #( #pushes )*
7167 let query = #root::core::InsertQuery::new(
7168 <Self as #root::core::Model>::SCHEMA,
7169 _columns,
7170 _values,
7171 )
7172 .returning(::std::vec![ #( #returning_cols ),* ]);
7173 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7174 #executor_passes_to_data_write,
7175 &query,
7176 ).await?;
7177 let _returning_row = &_returning_row_v;
7178 #( #auto_assigns )*
7179 #audit_insert_emit
7180 ::core::result::Result::Ok(())
7181 }
7182
7183 /// Per-call audit-source override for [`Self::insert_on`].
7184 /// See [`Self::save_on_with`] for shape rationale.
7185 ///
7186 /// # Errors
7187 /// As [`Self::insert_on`].
7188 #[cfg(feature = "postgres")]
7189 pub async fn insert_on_with #executor_generics (
7190 &mut self,
7191 #executor_param,
7192 source: #root::audit::AuditSource,
7193 ) -> ::core::result::Result<(), #root::sql::ExecError>
7194 #executor_where
7195 {
7196 #root::audit::with_source(source, self.insert_on(_executor)).await
7197 }
7198 }
7199 } else {
7200 let insert_columns = &fields.insert_columns;
7201 let insert_values = &fields.insert_values;
7202 quote! {
7203 /// Insert this row into its table.
7204 ///
7205 /// # Errors
7206 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7207 /// driver failures.
7208 #[cfg(feature = "postgres")]
7209 pub async fn insert(
7210 &self,
7211 pool: &#root::sql::sqlx::PgPool,
7212 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7213 self.insert_on(pool).await
7214 }
7215
7216 /// Like [`Self::insert`] but accepts any sqlx executor.
7217 /// See [`Self::save_on`] for tenancy-scoped rationale.
7218 ///
7219 /// # Errors
7220 /// As [`Self::insert`].
7221 #[cfg(feature = "postgres")]
7222 pub async fn insert_on<'_c, _E>(
7223 &self,
7224 _executor: _E,
7225 ) -> ::core::result::Result<(), #root::sql::ExecError>
7226 where
7227 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7228 {
7229 let query = #root::core::InsertQuery::new(
7230 <Self as #root::core::Model>::SCHEMA,
7231 ::std::vec![ #( #insert_columns ),* ],
7232 ::std::vec![ #( #insert_values ),* ],
7233 );
7234 #root::sql::__macro_internals::insert_on(_executor, &query).await
7235 }
7236 }
7237 };
7238
7239 let bulk_insert_method = if fields.has_auto {
7240 let cols_no_auto = &fields.bulk_columns_no_auto;
7241 let cols_all = &fields.bulk_columns_all;
7242 let pushes_no_auto = &fields.bulk_pushes_no_auto;
7243 let pushes_all = &fields.bulk_pushes_all;
7244 let returning_cols = &fields.returning_cols;
7245 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7246 let uniformity = &fields.bulk_auto_uniformity;
7247 let first_auto_ident = fields
7248 .first_auto_ident
7249 .as_ref()
7250 .expect("has_auto implies first_auto_ident is Some");
7251 quote! {
7252 /// Bulk-insert `rows` in a single round-trip. Every row's
7253 /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7254 /// (sequence fills them in) or uniformly `Auto::Set(_)`
7255 /// (caller-supplied values). Mixed Set/Unset is rejected
7256 /// — call `insert` per row for that case.
7257 ///
7258 /// Empty slice is a no-op. Each row's `Auto` fields are
7259 /// populated from the `RETURNING` clause in input order
7260 /// before this returns.
7261 ///
7262 /// # Errors
7263 /// Returns [`#root::sql::ExecError`] for validation,
7264 /// SQL-writing, mixed-Auto rejection, or driver failures.
7265 #[cfg(feature = "postgres")]
7266 pub async fn bulk_insert(
7267 rows: &mut [Self],
7268 pool: &#root::sql::sqlx::PgPool,
7269 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7270 #pool_to_bulk_insert_on
7271 }
7272
7273 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7274 /// See [`Self::save_on`] for tenancy-scoped rationale.
7275 ///
7276 /// # Errors
7277 /// As [`Self::bulk_insert`].
7278 #[cfg(feature = "postgres")]
7279 pub async fn bulk_insert_on #executor_generics (
7280 rows: &mut [Self],
7281 #executor_param,
7282 ) -> ::core::result::Result<(), #root::sql::ExecError>
7283 #executor_where
7284 {
7285 if rows.is_empty() {
7286 return ::core::result::Result::Ok(());
7287 }
7288 let _first_unset = matches!(
7289 rows[0].#first_auto_ident,
7290 #root::sql::Auto::Unset
7291 );
7292 #( #uniformity )*
7293
7294 let mut _all_rows: ::std::vec::Vec<
7295 ::std::vec::Vec<#root::core::SqlValue>,
7296 > = ::std::vec::Vec::with_capacity(rows.len());
7297 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7298 for _row in rows.iter() {
7299 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7300 ::std::vec::Vec::new();
7301 #( #pushes_no_auto )*
7302 _all_rows.push(_row_vals);
7303 }
7304 ::std::vec![ #( #cols_no_auto ),* ]
7305 } else {
7306 for _row in rows.iter() {
7307 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7308 ::std::vec::Vec::new();
7309 #( #pushes_all )*
7310 _all_rows.push(_row_vals);
7311 }
7312 ::std::vec![ #( #cols_all ),* ]
7313 };
7314
7315 let _query = #root::core::BulkInsertQuery::new(
7316 <Self as #root::core::Model>::SCHEMA,
7317 _columns,
7318 _all_rows,
7319 )
7320 .returning(::std::vec![ #( #returning_cols ),* ]);
7321 let _returned = #root::sql::__macro_internals::bulk_insert_on(
7322 #executor_passes_to_data_write,
7323 &_query,
7324 ).await?;
7325 if _returned.len() != rows.len() {
7326 return ::core::result::Result::Err(
7327 #root::sql::ExecError::Sql(
7328 #root::sql::SqlError::BulkInsertReturningMismatch {
7329 expected: rows.len(),
7330 actual: _returned.len(),
7331 }
7332 )
7333 );
7334 }
7335 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7336 #auto_assigns_for_row
7337 }
7338 #audit_bulk_insert_emit
7339 ::core::result::Result::Ok(())
7340 }
7341 }
7342 } else {
7343 let cols_all = &fields.bulk_columns_all;
7344 let pushes_all = &fields.bulk_pushes_all;
7345 quote! {
7346 /// Bulk-insert `rows` in a single round-trip. Every row's
7347 /// fields are written verbatim — there are no `Auto<T>`
7348 /// fields on this model.
7349 ///
7350 /// Empty slice is a no-op.
7351 ///
7352 /// # Errors
7353 /// Returns [`#root::sql::ExecError`] for validation,
7354 /// SQL-writing, or driver failures.
7355 #[cfg(feature = "postgres")]
7356 pub async fn bulk_insert(
7357 rows: &[Self],
7358 pool: &#root::sql::sqlx::PgPool,
7359 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7360 Self::bulk_insert_on(rows, pool).await
7361 }
7362
7363 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7364 /// See [`Self::save_on`] for tenancy-scoped rationale.
7365 ///
7366 /// # Errors
7367 /// As [`Self::bulk_insert`].
7368 #[cfg(feature = "postgres")]
7369 pub async fn bulk_insert_on<'_c, _E>(
7370 rows: &[Self],
7371 _executor: _E,
7372 ) -> ::core::result::Result<(), #root::sql::ExecError>
7373 where
7374 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7375 {
7376 if rows.is_empty() {
7377 return ::core::result::Result::Ok(());
7378 }
7379 let mut _all_rows: ::std::vec::Vec<
7380 ::std::vec::Vec<#root::core::SqlValue>,
7381 > = ::std::vec::Vec::with_capacity(rows.len());
7382 for _row in rows.iter() {
7383 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7384 ::std::vec::Vec::new();
7385 #( #pushes_all )*
7386 _all_rows.push(_row_vals);
7387 }
7388 let _query = #root::core::BulkInsertQuery::new(
7389 <Self as #root::core::Model>::SCHEMA,
7390 ::std::vec![ #( #cols_all ),* ],
7391 _all_rows,
7392 );
7393 let _ = #root::sql::__macro_internals::bulk_insert_on(_executor, &_query).await?;
7394 ::core::result::Result::Ok(())
7395 }
7396 }
7397 };
7398
7399 // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7400 // (no postgres-feature gate); routes through the existing
7401 // `bulk_insert_pool` + per-dialect conflict writer.
7402 //
7403 // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7404 // sequence picks the PK for fresh inserts; the UPDATE branch never
7405 // touches the Auto column.
7406 let bulk_upsert_pool_method = {
7407 // Pick the "no Auto" columns when the model has Auto fields,
7408 // else every column.
7409 let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7410 (
7411 fields.bulk_columns_no_auto.clone(),
7412 fields.bulk_pushes_no_auto.clone(),
7413 )
7414 } else {
7415 (
7416 fields.bulk_columns_all.clone(),
7417 fields.bulk_pushes_all.clone(),
7418 )
7419 };
7420 quote! {
7421 /// Tri-dialect bulk insert that updates on conflict — the
7422 /// "import a batch idempotently" shape. Issue #267 / T1.5.
7423 ///
7424 /// Per-row values are extracted and lowered into a
7425 /// [`#root::core::BulkInsertQuery`] with
7426 /// `on_conflict = DoUpdate { target, update_columns }`. The
7427 /// writer dispatches per-dialect:
7428 /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7429 /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)` (target ignored — MySQL matches every UNIQUE index)
7430 ///
7431 /// `target` names the column(s) whose unique constraint
7432 /// defines the conflict (typically a `unique` or
7433 /// `unique_together` natural-key column, NOT the `Auto<T>`
7434 /// PK). `update_cols` names the columns to overwrite on
7435 /// conflict — every other column is left untouched on the
7436 /// existing row.
7437 ///
7438 /// Auto-PK rows must all have `Auto::Unset` (the sequence
7439 /// picks the PK on insert; the update path never touches
7440 /// the Auto column). Auto-set rows trigger a hard error.
7441 /// Empty slice is a no-op.
7442 ///
7443 /// # Errors
7444 /// Returns [`#root::sql::ExecError`] for validation,
7445 /// SQL-writing, or driver failures.
7446 pub async fn bulk_upsert_pool(
7447 rows: &[Self],
7448 target: &[&'static str],
7449 update_cols: &[&'static str],
7450 pool: &#root::sql::Pool,
7451 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7452 if rows.is_empty() {
7453 return ::core::result::Result::Ok(());
7454 }
7455 let mut _all_rows: ::std::vec::Vec<
7456 ::std::vec::Vec<#root::core::SqlValue>,
7457 > = ::std::vec::Vec::with_capacity(rows.len());
7458 for _row in rows.iter() {
7459 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7460 ::std::vec::Vec::new();
7461 #( #upsert_pushes )*
7462 _all_rows.push(_row_vals);
7463 }
7464 let _query = #root::core::BulkInsertQuery::new(
7465 <Self as #root::core::Model>::SCHEMA,
7466 ::std::vec![ #( #upsert_cols ),* ],
7467 _all_rows,
7468 )
7469 .on_conflict_do_update(target, update_cols);
7470 #root::sql::bulk_insert_pool(pool, &_query).await
7471 }
7472
7473 /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7474 /// skip rows that would violate a unique constraint. Issue
7475 /// #267 / T1.5. Same per-dialect dispatch as
7476 /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7477 /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7478 /// <pivot> = <pivot>` (MySQL no-op write).
7479 ///
7480 /// # Errors
7481 /// As [`Self::bulk_upsert_pool`].
7482 pub async fn bulk_insert_or_ignore_pool(
7483 rows: &[Self],
7484 pool: &#root::sql::Pool,
7485 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7486 if rows.is_empty() {
7487 return ::core::result::Result::Ok(());
7488 }
7489 let mut _all_rows: ::std::vec::Vec<
7490 ::std::vec::Vec<#root::core::SqlValue>,
7491 > = ::std::vec::Vec::with_capacity(rows.len());
7492 for _row in rows.iter() {
7493 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7494 ::std::vec::Vec::new();
7495 #( #upsert_pushes )*
7496 _all_rows.push(_row_vals);
7497 }
7498 let _query = #root::core::BulkInsertQuery::new(
7499 <Self as #root::core::Model>::SCHEMA,
7500 ::std::vec![ #( #upsert_cols ),* ],
7501 _all_rows,
7502 )
7503 .on_conflict_do_nothing();
7504 #root::sql::bulk_insert_pool(pool, &_query).await
7505 }
7506 }
7507 };
7508
7509 // Ergonomic `Model::bulk_update(objs, fields)`.
7510 // The SQL/IR/executor stack
7511 // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7512 // `write_bulk_update_*` writers) already existed; what was missing
7513 // was the per-model constructor that maps `&[Self]` + a runtime
7514 // column list into rows of `[pk, col_vals…]` so callers don't
7515 // hand-build the IR. Emitted only when the model has a primary key
7516 // (the PK is the join key and can't itself be updated).
7517 let bulk_update_method = match &fields.primary_key {
7518 None => quote! {},
7519 Some((pk_ident, pk_col)) => {
7520 // One pair of match arms per non-PK column: a validation arm
7521 // resolving the runtime name to its `&'static str` column,
7522 // and a value arm pushing that field off the row.
7523 let mut col_arms: Vec<TokenStream2> = Vec::new();
7524 let mut val_arms: Vec<TokenStream2> = Vec::new();
7525 for entry in &fields.column_entries {
7526 if &entry.column == pk_col {
7527 continue;
7528 }
7529 let col = &entry.column;
7530 let ident = &entry.ident;
7531 col_arms.push(quote! { #col => #col, });
7532 val_arms.push(quote! {
7533 #col => _row_vals.push(
7534 ::core::convert::Into::<#root::core::SqlValue>::into(
7535 ::core::clone::Clone::clone(&_o.#ident)
7536 )
7537 ),
7538 });
7539 }
7540 quote! {
7541 /// Write per-row-different values for the named `fields`
7542 /// across every object in `objs` in a single statement,
7543 /// matched by primary key.
7544 ///
7545 /// `fields` names the **columns** to update. The primary
7546 /// key identifies each row and cannot itself be updated
7547 /// (pass it and you get
7548 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7549 /// Empty `objs` or `fields` is a no-op returning `0`.
7550 /// Objects whose PK matches no row are simply not updated.
7551 /// Returns the number of rows affected.
7552 ///
7553 /// Tri-dialect: lowers to one
7554 /// [`#root::core::BulkUpdateQuery`] and dispatches
7555 /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7556 /// a CTE + correlated subquery on SQLite, an inner
7557 /// `JOIN (VALUES …)` on MySQL.
7558 ///
7559 /// # Errors
7560 /// [`#root::core::QueryError::UnknownField`] for a name
7561 /// that isn't a column on this model,
7562 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7563 /// `fields` names the PK, or [`#root::sql::ExecError`] for
7564 /// SQL-writing / driver failures.
7565 pub async fn bulk_update(
7566 objs: &[Self],
7567 fields: &[&str],
7568 pool: &#root::sql::Pool,
7569 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7570 if objs.is_empty() || fields.is_empty() {
7571 return ::core::result::Result::Ok(0);
7572 }
7573 let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7574 let mut _update_columns: ::std::vec::Vec<&'static str> =
7575 ::std::vec::Vec::with_capacity(fields.len());
7576 for &_f in fields {
7577 let _col: &'static str = match _f {
7578 #pk_col => {
7579 return ::core::result::Result::Err(
7580 ::core::convert::Into::into(
7581 #root::core::QueryError::BulkUpdatePrimaryKey {
7582 model: _model_name,
7583 field: ::std::string::ToString::to_string(_f),
7584 }
7585 )
7586 );
7587 }
7588 #( #col_arms )*
7589 _ => {
7590 return ::core::result::Result::Err(
7591 ::core::convert::Into::into(
7592 #root::core::QueryError::UnknownField {
7593 model: _model_name,
7594 field: ::std::string::ToString::to_string(_f),
7595 }
7596 )
7597 );
7598 }
7599 };
7600 // A model whose only field is the primary key emits no
7601 // non-PK arms, so every arm above diverges and this push
7602 // is genuinely unreachable — correct, since such a model
7603 // has nothing to bulk-update. Scoped to the statement
7604 // rather than the fn so a real unreachability elsewhere
7605 // in this body still warns. Without it every PK-only
7606 // model warns in the *user's* crate, which they cannot
7607 // act on (#1290).
7608 #[allow(unreachable_code)]
7609 _update_columns.push(_col);
7610 }
7611 let mut _rows: ::std::vec::Vec<
7612 ::std::vec::Vec<#root::core::SqlValue>,
7613 > = ::std::vec::Vec::with_capacity(objs.len());
7614 for _o in objs.iter() {
7615 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7616 ::std::vec::Vec::with_capacity(fields.len() + 1);
7617 // PK first — the writers expect `[pk, …update cols]`.
7618 _row_vals.push(
7619 ::core::convert::Into::<#root::core::SqlValue>::into(
7620 ::core::clone::Clone::clone(&_o.#pk_ident)
7621 )
7622 );
7623 for &_f in fields {
7624 match _f {
7625 #( #val_arms )*
7626 // Unreachable: every name was validated
7627 // against the same arm set above.
7628 _ => {}
7629 }
7630 }
7631 _rows.push(_row_vals);
7632 }
7633 let _query = #root::core::BulkUpdateQuery::new(
7634 <Self as #root::core::Model>::SCHEMA,
7635 _update_columns,
7636 _rows,
7637 );
7638 #root::sql::bulk_update_pool(pool, &_query).await
7639 }
7640 }
7641 }
7642 };
7643
7644 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7645 quote! {
7646 /// Hidden runtime accessor for the primary-key value as a
7647 /// [`SqlValue`]. Used by reverse-relation helpers
7648 /// (`<parent>::<child>_set`) emitted from sibling models'
7649 /// FK fields. Not part of the public API.
7650 #[doc(hidden)]
7651 pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7652 ::core::convert::Into::<#root::core::SqlValue>::into(
7653 ::core::clone::Clone::clone(&self.#pk_ident)
7654 )
7655 }
7656 }
7657 });
7658
7659 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7660 quote! {
7661 impl #root::sql::HasPkValue for #struct_name {
7662 fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7663 ::core::convert::Into::<#root::core::SqlValue>::into(
7664 ::core::clone::Clone::clone(&self.#pk_ident)
7665 )
7666 }
7667 }
7668 }
7669 });
7670
7671 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7672
7673 // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7674 // dispatch to the right per-backend body without the macro emitting
7675 // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7676 // so audited models with non-Auto PKs (which still go through
7677 // `insert_one_with_audit` → `apply_auto_pk`) link.
7678 let assign_auto_pk_pool_impl = {
7679 let auto_assigns = &fields.auto_assigns;
7680 // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7681 // so the body is structurally identical to `auto_assigns` —
7682 // only the helper name swaps from `try_get_returning` to
7683 // `try_get_returning_sqlite` so the closure typechecks against
7684 // a `SqliteRow` instead of a `PgRow`.
7685 let auto_assigns_sqlite: Vec<TokenStream2> = fields
7686 .auto_field_idents
7687 .iter()
7688 .map(|(ident, column)| {
7689 quote! {
7690 self.#ident = #root::sql::try_get_returning_sqlite(
7691 _returning_row, #column
7692 )?;
7693 }
7694 })
7695 .collect();
7696 // #1028 — decode each `generated_as` column from the same
7697 // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7698 // own type drives the decode (no `Auto<T>` wrapper).
7699 let generated_assigns: Vec<TokenStream2> = fields
7700 .generated_field_idents
7701 .iter()
7702 .map(|(ident, column)| {
7703 quote! {
7704 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7705 }
7706 })
7707 .collect();
7708 let generated_assigns_sqlite: Vec<TokenStream2> = fields
7709 .generated_field_idents
7710 .iter()
7711 .map(|(ident, column)| {
7712 quote! {
7713 self.#ident = #root::sql::try_get_returning_sqlite(
7714 _returning_row, #column
7715 )?;
7716 }
7717 })
7718 .collect();
7719 let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
7720 // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7721 // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7722 // Auto<Uuid>/etc. fail to link against MySQL (intended —
7723 // those models can't use AUTO_INCREMENT). The trait is only
7724 // touched on the MySQL arm at runtime, so PG-only consumers
7725 // never see the bound failure.
7726 //
7727 // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7728 // Auto<i64> PK + auto_now_add timestamp) errored hard at
7729 // runtime with "multi-column RETURNING". MySQL has no
7730 // multi-column RETURNING semantic and a follow-up SELECT
7731 // would need cross-trait plumbing. Pragmatic shape: succeed
7732 // with the FIRST Auto field populated from LAST_INSERT_ID();
7733 // any other Auto fields stay `Auto::Unset`. Callers that
7734 // need the DB-defaulted timestamp / UUID can re-fetch the
7735 // row by PK after `save_pool`. Fixes the cookbook chapter
7736 // 12 dialect divergence.
7737 let value_ty = fields
7738 .first_auto_value_ty
7739 .as_ref()
7740 .expect("first_auto_value_ty set whenever first_auto_ident is");
7741 quote! {
7742 let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7743 ::rustango_from_mysql_auto_id(_id)?;
7744 self.#first = #root::sql::Auto::Set(_converted);
7745 ::core::result::Result::Ok(())
7746 }
7747 } else {
7748 quote! {
7749 let _ = _id;
7750 ::core::result::Result::Ok(())
7751 }
7752 };
7753 quote! {
7754 impl #root::sql::AssignAutoPkPool for #struct_name {
7755 fn __rustango_assign_from_pg_row(
7756 &mut self,
7757 _returning_row: &#root::sql::PgReturningRow,
7758 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7759 #( #auto_assigns )*
7760 #( #generated_assigns )*
7761 ::core::result::Result::Ok(())
7762 }
7763 fn __rustango_assign_from_mysql_id(
7764 &mut self,
7765 _id: i64,
7766 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7767 #mysql_body
7768 }
7769 fn __rustango_assign_from_sqlite_row(
7770 &mut self,
7771 _returning_row: &#root::sql::SqliteReturningRow,
7772 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7773 #( #auto_assigns_sqlite )*
7774 #( #generated_assigns_sqlite )*
7775 ::core::result::Result::Ok(())
7776 }
7777 }
7778 }
7779 };
7780
7781 let from_aliased_row_inits = &fields.from_aliased_row_inits;
7782 let aliased_row_helper = quote! {
7783 /// Decode a row's aliased target columns (produced by
7784 /// `select_related`'s LEFT JOIN) into a fresh instance of
7785 /// this model. Reads each column via
7786 /// `format!("{prefix}__{col}")`, matching the alias the
7787 /// SELECT writer emitted. Slice 9.0d.
7788 #[doc(hidden)]
7789 #[cfg(feature = "postgres")]
7790 pub fn __rustango_from_aliased_row(
7791 row: &#root::sql::sqlx::postgres::PgRow,
7792 prefix: &str,
7793 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7794 ::core::result::Result::Ok(Self {
7795 #( #from_aliased_row_inits ),*
7796 })
7797 }
7798 };
7799 // v0.23.0-batch8 — MySQL counterpart, gated through the
7800 // cfg-aware macro_rules so PG-only builds expand to nothing.
7801 let aliased_row_helper_my = quote! {
7802 #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7803 #( #from_aliased_row_inits ),*
7804 });
7805 };
7806
7807 // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7808 // pattern + cfg gate on the `sqlite` feature.
7809 let aliased_row_helper_sqlite = quote! {
7810 #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7811 #( #from_aliased_row_inits ),*
7812 });
7813 };
7814
7815 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7816 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7817 let load_related_impl_sqlite =
7818 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7819
7820 // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7821 // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7822 // default `Self::objects()`. Each accessor returns a fresh
7823 // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7824 // methods the user defined.
7825 let extra_manager_fns: Vec<TokenStream2> = manager_fns
7826 .iter()
7827 .map(|fn_ident| {
7828 let model_name_str = struct_name.to_string();
7829 let fn_name_str = fn_ident.to_string();
7830 let doc = format!(
7831 "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7832 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7833 equivalent to `Self::objects()`. Chains with any \
7834 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7835 extension methods."
7836 );
7837 quote! {
7838 #[doc = #doc]
7839 #[must_use]
7840 pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7841 #root::query::QuerySet::new()
7842 }
7843 }
7844 })
7845 .collect();
7846
7847 quote! {
7848 impl #struct_name {
7849 /// Start a new `QuerySet` over this model.
7850 #[must_use]
7851 pub fn objects() -> #root::query::QuerySet<#struct_name> {
7852 #root::query::QuerySet::new()
7853 }
7854
7855 /// Eloquent-shape alias of [`Self::objects`]. Returns
7856 /// a fresh `QuerySet<Self>` ready for `.filter()` /
7857 /// `.where_()` / etc. Matches Laravel muscle-memory:
7858 ///
7859 /// ```ignore
7860 /// // Eloquent: Post::query()->where('published', true)
7861 /// // rustango: Post::query().filter("published", true)
7862 /// // or: Post::objects().filter("published", true)
7863 /// ```
7864 ///
7865 /// Both names point at the same underlying constructor;
7866 /// neither is preferred.
7867 #[must_use]
7868 pub fn query() -> #root::query::QuerySet<#struct_name> {
7869 #root::query::QuerySet::new()
7870 }
7871
7872 #( #extra_manager_fns )*
7873
7874 #insert_method
7875
7876 #bulk_insert_method
7877
7878 #bulk_upsert_pool_method
7879
7880 #bulk_update_method
7881
7882 #save_method
7883
7884 #pk_methods
7885
7886 #pk_value_helper
7887
7888 #aliased_row_helper
7889
7890 #column_consts
7891 }
7892
7893 #aliased_row_helper_my
7894
7895 #aliased_row_helper_sqlite
7896
7897 #load_related_impl
7898
7899 #load_related_impl_my
7900
7901 #load_related_impl_sqlite
7902
7903 #has_pk_value_impl
7904
7905 #fk_pk_access_impl
7906
7907 #assign_auto_pk_pool_impl
7908 }
7909}
7910
7911/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
7912/// `auto_assigns` but reading from `_returning_row` and writing to
7913/// `_row_mut` instead of `self`.
7914fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
7915 let root = rustango_root();
7916 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
7917 let col_lit = column.as_str();
7918 quote! {
7919 _row_mut.#ident = #root::sql::sqlx::Row::try_get(
7920 _returning_row,
7921 #col_lit,
7922 )?;
7923 }
7924 });
7925 quote! { #( #lines )* }
7926}
7927
7928/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
7929fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
7930 let lines = entries.iter().map(|e| {
7931 let ident = &e.ident;
7932 let col_ty = column_type_ident(ident);
7933 quote! {
7934 #[allow(non_upper_case_globals)]
7935 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
7936 }
7937 });
7938 quote! { #(#lines)* }
7939}
7940
7941/// Emit a hidden per-model module carrying one zero-sized type per field,
7942/// each with a `Column` impl pointing back at the model.
7943fn column_module_tokens(
7944 module_ident: &syn::Ident,
7945 struct_name: &syn::Ident,
7946 entries: &[ColumnEntry],
7947) -> TokenStream2 {
7948 let root = rustango_root();
7949 let items = entries.iter().map(|e| {
7950 let col_ty = column_type_ident(&e.ident);
7951 let value_ty = &e.value_ty;
7952 let name = &e.name;
7953 let column = &e.column;
7954 let field_type_tokens = &e.field_type_tokens;
7955 quote! {
7956 #[derive(::core::clone::Clone, ::core::marker::Copy)]
7957 pub struct #col_ty;
7958
7959 impl #root::core::Column for #col_ty {
7960 type Model = super::#struct_name;
7961 type Value = #value_ty;
7962 const NAME: &'static str = #name;
7963 const COLUMN: &'static str = #column;
7964 const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
7965 }
7966 }
7967 });
7968 quote! {
7969 #[doc(hidden)]
7970 #[allow(non_camel_case_types, non_snake_case)]
7971 pub mod #module_ident {
7972 // Re-import the parent scope so field types referencing
7973 // sibling models (e.g. `ForeignKey<Author>`) resolve
7974 // inside this submodule. Without this we'd hit
7975 // `proc_macro_derive_resolution_fallback` warnings.
7976 #[allow(unused_imports)]
7977 use super::*;
7978 #(#items)*
7979 }
7980 }
7981}
7982
7983fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
7984 syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
7985}
7986
7987fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
7988 syn::Ident::new(
7989 &format!("__rustango_cols_{struct_name}"),
7990 struct_name.span(),
7991 )
7992}
7993
7994fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
7995 let root = rustango_root();
7996 // The Postgres impl is always emitted — every rustango build pulls in
7997 // sqlx-postgres via the default `postgres` feature. The MySQL impl is
7998 // routed through `#root::__impl_my_from_row!`, a cfg-gated
7999 // macro_rules whose body collapses to nothing when rustango is built
8000 // without the `mysql` feature. No user-facing feature shim required.
8001 //
8002 // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8003 // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8004 // back into a comma-separated list inside square brackets. Since each
8005 // entry is already in `field: expr` shape, the existing tokens slot in.
8006 quote! {
8007 #[cfg(feature = "postgres")]
8008 impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8009 for #struct_name
8010 {
8011 fn from_row(
8012 row: &'r #root::sql::sqlx::postgres::PgRow,
8013 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8014 ::core::result::Result::Ok(Self {
8015 #( #from_row_inits ),*
8016 })
8017 }
8018 }
8019
8020 #root::__impl_my_from_row!(#struct_name, |row| {
8021 #( #from_row_inits ),*
8022 });
8023
8024 #root::__impl_sqlite_from_row!(#struct_name, |row| {
8025 #( #from_row_inits ),*
8026 });
8027 }
8028}
8029
8030struct ContainerAttrs {
8031 table: Option<String>,
8032 display: Option<(String, proc_macro2::Span)>,
8033 /// Explicit app label from `#[rustango(app = "blog")]`.
8034 /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8035 /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8036 /// at runtime — this attribute is the override for cases where
8037 /// the inference is wrong (e.g. a model that conceptually belongs
8038 /// to one app but is physically in another module).
8039 app: Option<String>,
8040 /// Per-model admin knobs from
8041 /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8042 /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8043 /// admin code falls back to `AdminConfig::DEFAULT`.
8044 admin: Option<AdminAttrs>,
8045 /// Per-model audit configuration from `#[rustango(audit(...))]`.
8046 /// `None` when the model isn't audited — write paths emit no
8047 /// audit entries. When present, single-row writes capture
8048 /// before/after for the listed fields and bulk writes batch
8049 /// snapshots into one INSERT into `rustango_audit_log`.
8050 audit: Option<AuditAttrs>,
8051 /// `true` when `#[rustango(permissions)]` is present. Signals that
8052 /// `auto_create_permissions` should seed the four CRUD codenames for
8053 /// this model.
8054 permissions: bool,
8055 /// Many-to-many relations declared via
8056 /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8057 /// src = "post_id", dst = "tag_id"))]`.
8058 m2m: Vec<M2MAttr>,
8059 /// Polymorphic M2M relations declared via
8060 /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8061 /// pk_column = "taggable_id", ct_column = "taggable_type",
8062 /// related_column = "tag_id"))]` (issue #818).
8063 generic_m2m: Vec<GenericM2MAttr>,
8064 /// Composite indexes declared via
8065 /// `#[rustango(index("col1, col2"))]` or
8066 /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8067 /// Single-column indexes from `#[rustango(index)]` on fields are
8068 /// accumulated here during field collection.
8069 indexes: Vec<IndexAttr>,
8070 /// Table-level CHECK constraints declared via
8071 /// `#[rustango(check(name = "…", expr = "…"))]`.
8072 checks: Vec<CheckAttr>,
8073 /// Table-level PG `EXCLUDE` constraints declared via
8074 /// `#[rustango(exclude(name = "…", using = "gist", elements =
8075 /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8076 /// migration writer renders them on Postgres and skips with a
8077 /// warning on MySQL/SQLite. Issue #319.
8078 excludes: Vec<ExcludeAttr>,
8079 /// Composite (multi-column) FKs declared via
8080 /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8081 /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8082 composite_fks: Vec<CompositeFkAttr>,
8083 /// Generic ("any model") FKs declared via
8084 /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8085 /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8086 generic_fks: Vec<GenericFkAttr>,
8087 /// Where this model lives in a tenancy deployment, declared via
8088 /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8089 /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8090 /// to partition diff output between registry-scoped and
8091 /// tenant-scoped migration files.
8092 scope: Option<String>,
8093 /// Custom-Manager extension-trait name from
8094 /// `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8095 /// When set, the macro emits an empty `pub trait <name>: Sized {}`
8096 /// adjacent to the model so users can write
8097 /// `impl FooManagerExt for QuerySet<Foo> { fn published(self) -> Self ... }`
8098 /// and discover the convention from the model definition.
8099 manager_ext: Option<syn::Ident>,
8100 /// Extra QuerySet accessor names from
8101 /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8102 /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8103 /// the default `Self::objects()`. Multiple attributes allowed.
8104 manager_fns: Vec<syn::Ident>,
8105 /// Default ordering declared via `#[rustango(default_order =
8106 /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8107 /// `(column_name, desc_flag, span_for_error_reporting)` — the
8108 /// `-` prefix means descending; the `+` prefix or no prefix means
8109 /// ascending.
8110 default_order: Vec<(String, bool, proc_macro2::Span)>,
8111 /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8112 /// Routes the emitted schema's `is_view = true` so the migration
8113 /// snapshot skips this model (its underlying SQL view is operator-
8114 /// managed, not rustango-managed).
8115 is_view: bool,
8116 /// `#[rustango(managed = false)]` — who owns the table.
8117 /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8118 /// the migration snapshot skips this model so `makemigrations` /
8119 /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8120 /// TABLE` against it (operator-managed schema).
8121 managed: bool,
8122 /// Name of the model's base manager, from
8123 /// `#[rustango(base_manager_name = "...")]`. Threaded into
8124 /// `ModelSchema::base_manager_name`. Declarative-only today.
8125 base_manager_name: Option<String>,
8126 /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8127 /// field this model's instances are ordered relative to.
8128 /// Declarative-only today; threaded onto
8129 /// `ModelSchema::order_with_respect_to`.
8130 order_with_respect_to: Option<String>,
8131 /// `#[rustango(proxy)]` / `#[rustango(proxy = true)]`.
8132 /// Marks the model as a proxy that
8133 /// shares its DB table with another struct. Threaded into
8134 /// `ModelSchema::proxy` so future codegen can skip table-owning
8135 /// behavior for proxies.
8136 proxy: bool,
8137 /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8138 /// Each comma-separated capability token surfaces on
8139 /// `ModelSchema::required_db_features` so `manage check --deploy`
8140 /// can warn when the active dialect lacks one.
8141 required_db_features: Vec<String>,
8142 /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8143 /// Normalized to the dialect name `manage check --deploy`
8144 /// compares against `Settings.database.backend`. Aliases
8145 /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8146 /// stored under the canonical name.
8147 required_db_vendor: Option<String>,
8148 /// `#[rustango(default_related_name = "...")]`. Threaded into
8149 /// `ModelSchema::default_related_name`. Reverse-relation accessor
8150 /// name to use when an FK / M2M field doesn't override it.
8151 /// Today rustango doesn't auto-emit reverse managers; the
8152 /// metadata is the foundation for that work.
8153 default_related_name: Option<String>,
8154 /// `#[rustango(db_table_comment = "...")]`. Threaded into
8155 /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8156 /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8157 /// inline `COMMENT='...'`, SQLite: no-op).
8158 db_table_comment: Option<String>,
8159 /// `#[rustango(get_latest_by = "created_at")]` /
8160 /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8161 /// `(column, descending)` where `descending = true` when the
8162 /// attribute value starts with `-`. Threaded into
8163 /// `ModelSchema::get_latest_by`.
8164 get_latest_by: Option<(String, bool)>,
8165 /// Extra `(codename, label)` permissions
8166 /// from `#[rustango(extra_permissions = "approve:Can approve,
8167 /// archive:Can archive")]`. Comma-separated `codename:label`
8168 /// pairs. Threaded into `ModelSchema::extra_permissions`.
8169 extra_permissions: Vec<(String, String)>,
8170 /// Which CRUD codenames (`"add"` / `"change"` / `"delete"` /
8171 /// `"view"`) the framework
8172 /// auto-creates. Empty `Vec` (default) means **all four**. Set via
8173 /// `#[rustango(default_permissions = "view,change")]` to opt out.
8174 /// Validated at parse time; unknown actions fail with a span-pointing
8175 /// error.
8176 default_permissions: Vec<String>,
8177 /// `#[rustango(verbose_name = "blog post")]` — the
8178 /// human-readable singular label for the model. Threaded into
8179 /// `ModelSchema::verbose_name` so admin section headers /
8180 /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8181 /// over the Rust struct identifier.
8182 verbose_name: Option<String>,
8183 /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8184 /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8185 /// When unset, `display_label_plural()` auto-suffixes `s`.
8186 verbose_name_plural: Option<String>,
8187 /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8188 /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8189 /// a name (used by `QuerySet::without_global_scope`) with a
8190 /// `fn() -> WhereExpr` path that the macro emits into
8191 /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8192 global_scopes: Vec<GlobalScopeAttr>,
8193 /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8194 /// `#[rustango(through(name, far, far_fk_column, intermediate,
8195 /// intermediate_fk_column, intermediate_pk_column))]` — issue
8196 /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8197 /// entry emits an inherent `<name>_through(&self)` accessor that
8198 /// returns a `QuerySet<Far>` filtered via a correlated subquery
8199 /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8200 /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8201 through_relations: Vec<ThroughAttr>,
8202 /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8203 /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8204 /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8205 /// Each entry emits two associated functions on the parent —
8206 /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8207 /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8208 /// over a correlated subquery against the child table. Users
8209 /// drop the result into `QuerySet::where_raw(...)`.
8210 reverse_has_relations: Vec<ReverseHasAttr>,
8211 /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8212 /// issue #830. Each emits a `Model::generic_reverse_relations()`
8213 /// entry so the relation-existence family resolves polymorphic
8214 /// children by name.
8215 generic_has_relations: Vec<GenericHasAttr>,
8216}
8217
8218/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8219/// declaration. Each entry becomes one `core::GlobalScope` in the
8220/// emitted schema literal; `apply` resolves at macro-expand time
8221/// against the consumer's scope so the function must be in scope at
8222/// the use site. Issue #820.
8223struct GlobalScopeAttr {
8224 name: String,
8225 apply: syn::Path,
8226}
8227
8228/// Parsed `#[rustango(through(...))]` declaration. Issue
8229/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8230/// `hasManyThrough` / `hasOneThrough` parity.
8231///
8232/// `Country hasManyThrough Post via User` declares as:
8233///
8234/// ```ignore
8235/// #[rustango(through(
8236/// name = "posts",
8237/// far = "Post",
8238/// far_fk_column = "author_id",
8239/// intermediate = "User",
8240/// intermediate_fk_column = "country_id",
8241/// ))]
8242/// struct Country { ... }
8243/// ```
8244///
8245/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8246/// which returns a queryset filtered via a correlated subquery —
8247/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8248/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8249/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8250/// because the subquery lives inside a `WhereExpr::InSubquery` node
8251/// and the outer queryset's pending list stays empty.
8252///
8253/// All four required arguments use **SQL column / table names**
8254/// (not Rust field names) to sidestep the multi-hop-filter substrate
8255/// gap. Once that substrate lands, a higher-level Rust-field-name
8256/// shorthand can be added without breaking this surface.
8257struct ThroughAttr {
8258 /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8259 name: String,
8260 /// Far model type identifier. `far = "Post"` → returns
8261 /// `QuerySet<Post>`. Resolved verbatim against the scope where
8262 /// the derive expands.
8263 far: syn::Ident,
8264 /// SQL column on the far model's table that references the
8265 /// intermediate model's primary key. For `Post`'s
8266 /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8267 /// default `<field>_id` convention) or whatever the user
8268 /// declared via `#[rustango(db_column = "...")]`.
8269 far_fk_column: String,
8270 /// Intermediate model type identifier. Needed to look up its
8271 /// `SCHEMA` so the subquery's `FROM` clause points at the
8272 /// intermediate table. `intermediate = "User"`.
8273 intermediate: syn::Ident,
8274 /// SQL column on the intermediate's table that references the
8275 /// source (this) model's primary key. For `User`'s
8276 /// `country: ForeignKey<Country>` the column is `"country_id"`.
8277 intermediate_fk_column: String,
8278 /// SQL primary-key column on the intermediate's table — the
8279 /// column the subquery projects. Optional; defaults to `"id"`
8280 /// (rustango's default PK column name). Override when the
8281 /// intermediate declares a custom PK column.
8282 intermediate_pk_column: String,
8283}
8284
8285/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8286/// child_fk_column = "..."))]` declaration. Issue
8287/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8288/// `whereHas` / `whereDoesntHave` parity.
8289///
8290/// `Post hasMany Comment` declares as:
8291///
8292/// ```ignore
8293/// #[rustango(reverse_has(
8294/// name = "comments",
8295/// child = "Comment",
8296/// child_fk_column = "post_id",
8297/// ))]
8298/// struct Post { ... }
8299/// ```
8300///
8301/// The macro emits two associated functions on `Post`:
8302///
8303/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8304/// `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8305/// <outer>.<self_pk_column>)`.
8306/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8307/// but `NOT EXISTS`, the `whereDoesntHave` analog.
8308///
8309/// User code:
8310///
8311/// ```ignore
8312/// // Posts with at least one comment:
8313/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8314/// // Posts with no comments:
8315/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8316/// ```
8317///
8318/// As with #817, all column / table identifiers are **SQL names**
8319/// (not Rust field names) so the substrate is independent of the
8320/// outstanding multi-hop filter resolver gap. The emitted
8321/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8322/// SQL-emit time via the writer's scope stack.
8323struct ReverseHasAttr {
8324 /// Accessor name. `name = "comments"` → emits
8325 /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8326 name: String,
8327 /// Child model type identifier. `child = "Comment"` — needed to
8328 /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8329 /// points at the child table.
8330 child: syn::Ident,
8331 /// SQL column on the child's table that references this model's
8332 /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8333 /// column is `"post_id"`.
8334 child_fk_column: String,
8335 /// SQL primary-key column on **this** model's table — the column
8336 /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8337 /// (rustango's default PK column name).
8338 self_pk_column: String,
8339}
8340
8341/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8342/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8343/// relation-existence family (issue #830). The child is a polymorphic,
8344/// content-type-discriminated model; emits a `Model::
8345/// generic_reverse_relations()` entry the queryset resolves by name.
8346struct GenericHasAttr {
8347 /// Accessor name, e.g. `name = "tags"`.
8348 name: String,
8349 /// Child model type identifier (`child = "Tag"`) — looked up for its
8350 /// `SCHEMA` so the subquery's `FROM` points at the child table.
8351 child: syn::Ident,
8352 /// Column on the child table holding the parent's content-type id.
8353 /// Optional; defaults to `"content_type_id"`.
8354 ct_column: String,
8355 /// Column on the child table holding the parent's PK value.
8356 /// Optional; defaults to `"object_pk"`.
8357 pk_column: String,
8358 /// SQL primary-key column on **this** (parent) model's table.
8359 /// Optional; defaults to `"id"`.
8360 self_pk_column: String,
8361}
8362
8363/// Parsed form of one index declaration (field-level or container-level).
8364struct IndexAttr {
8365 /// Index name; auto-derived when `None` at parse time.
8366 name: Option<String>,
8367 /// Column names in the index.
8368 columns: Vec<String>,
8369 /// `true` for `CREATE UNIQUE INDEX`.
8370 unique: bool,
8371 /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8372 /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8373 /// `"btree"` when the attribute is absent — the DDL writer omits
8374 /// the `USING` clause and the backend uses its own default
8375 /// (btree on every supported dialect).
8376 method: String,
8377 /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8378 /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8379 /// condition = "...", name = "..."))]`. `None` for plain indexes.
8380 where_clause: Option<String>,
8381 /// Covering-index columns (PG 11+ `INCLUDE (...)` clause).
8382 /// Empty `Vec` (the default) means "no covering columns".
8383 include: Vec<String>,
8384}
8385
8386/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8387struct CheckAttr {
8388 name: String,
8389 expr: String,
8390}
8391
8392/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8393/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8394/// PG-only — surfaced on every backend in the macro emit; the migration
8395/// writer skips the op on MySQL/SQLite. Issue #319.
8396struct ExcludeAttr {
8397 /// Constraint name (free-form Rust identifier).
8398 name: String,
8399 /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8400 using: String,
8401 /// Comma-separated `(column, operator)` pairs, in declaration
8402 /// order. Parsed from `"col WITH op, col WITH op"`.
8403 elements: Vec<(String, String)>,
8404 /// Optional `WHERE` predicate (raw SQL).
8405 where_clause: Option<String>,
8406}
8407
8408/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8409/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8410/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8411/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8412/// the model, not the field.
8413struct CompositeFkAttr {
8414 /// Logical relation name (free-form Rust identifier).
8415 name: String,
8416 /// SQL table name of the target.
8417 to: String,
8418 /// Source-side column names, in declaration order.
8419 from: Vec<String>,
8420 /// Target-side column names, same length / order as `from`.
8421 on: Vec<String>,
8422}
8423
8424/// Parsed form of one `#[rustango(generic_fk(name = "target",
8425/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8426/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8427/// generic ("any model") FKs live on the model, not the field.
8428struct GenericFkAttr {
8429 /// Logical relation name (free-form Rust identifier).
8430 name: String,
8431 /// Source-side column carrying the `content_type_id` value.
8432 ct_column: String,
8433 /// Source-side column carrying the target row's primary key.
8434 pk_column: String,
8435}
8436
8437/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8438struct M2MAttr {
8439 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8440 name: String,
8441 /// Target table (e.g. `"app_tags"`).
8442 to: String,
8443 /// Junction table (e.g. `"post_tags"`).
8444 through: String,
8445 /// Source FK column in the junction table (e.g. `"post_id"`).
8446 src: String,
8447 /// Destination FK column in the junction table (e.g. `"tag_id"`).
8448 dst: String,
8449 /// Whether the migration writer should auto-create the junction
8450 /// table. Default `true`. Set `auto_create = false` (#324) when
8451 /// the operator declares the through table as its own
8452 /// `#[derive(Model)]` struct with extra columns.
8453 auto_create: bool,
8454}
8455
8456/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8457/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8458/// junction carries a ContentType discriminator so unrelated models
8459/// share one pivot + related set.
8460struct GenericM2MAttr {
8461 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8462 name: String,
8463 /// Polymorphic junction table (e.g. `"taggables"`).
8464 through: String,
8465 /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8466 pk_column: String,
8467 /// Junction column holding the owning model's `content_type_id`
8468 /// discriminator (e.g. `"taggable_type"`).
8469 ct_column: String,
8470 /// Junction column holding the related model PK (e.g. `"tag_id"`).
8471 related_column: String,
8472}
8473
8474/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8475/// "user"))]`. `track` is a comma-separated list of field names whose
8476/// before/after values land in the JSONB `changes` column. `source`
8477/// is informational only — it pins a default source when the model
8478/// is written outside any `audit::with_source(...)` scope (rare).
8479#[derive(Default)]
8480struct AuditAttrs {
8481 /// Field names to capture in the `changes` JSONB. Validated
8482 /// against declared scalar fields at compile time. Empty means
8483 /// "track every scalar field".
8484 track: Option<(Vec<String>, proc_macro2::Span)>,
8485}
8486
8487/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8488/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8489/// comma-separated strings; we validate each ident against the model's
8490/// declared fields at compile time.
8491#[derive(Default)]
8492struct AdminAttrs {
8493 list_display: Option<(Vec<String>, proc_macro2::Span)>,
8494 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8495 list_per_page: Option<usize>,
8496 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8497 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8498 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8499 /// Bulk action names. No field-validation against model fields —
8500 /// these are action handlers, not column references.
8501 actions: Option<(Vec<String>, proc_macro2::Span)>,
8502 /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8503 /// sections, comma-separated fields per section, optional
8504 /// `Title:` prefix. Empty title omits the `<legend>`.
8505 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8506 /// `admin(list_display_links = "title")`. Names
8507 /// from `list_display` whose cells should link to detail/edit.
8508 /// Issue #350.
8509 list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8510 /// `admin(search_help_text = "...")`. Short
8511 /// caption rendered beside the admin list view's search box.
8512 /// Issue #353.
8513 search_help_text: Option<String>,
8514 /// `admin(actions_on_top = false)`. Hides the
8515 /// action-bar above the table. Default `true`. Issue #354.
8516 actions_on_top: Option<bool>,
8517 /// `admin(actions_on_bottom = true)`. Renders an
8518 /// additional action-bar below the table. Default `false`.
8519 /// Issue #354.
8520 actions_on_bottom: Option<bool>,
8521 /// `admin(date_hierarchy = "created_at")`. Name of
8522 /// a date / datetime field whose values render as a clickable
8523 /// year / month / day drill-down strip above the list table.
8524 /// Empty / unset disables the strip. Issue #355.
8525 date_hierarchy: Option<String>,
8526 /// `admin(prepopulated_fields = "slug:title")`.
8527 /// Each entry is `target:source[+source2]`; multiple entries are
8528 /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8529 /// The admin change-form emits JS that slugifies the source values
8530 /// into the target field on every keystroke. Issue #356.
8531 prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8532 /// `admin(raw_id_fields = "parent, owner")`. Names
8533 /// of FK fields whose change-form widget renders a lookup link
8534 /// next to the input. Issue #357.
8535 raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8536 /// `admin(autocomplete_fields = "author_id")`.
8537 /// Names of FK fields whose change-form widget renders an
8538 /// Ajax-driven typeahead populated from a `__autocomplete`
8539 /// endpoint on the target model. Issue #358.
8540 autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8541 /// `admin(list_select_related = "all" | "none" | "author, …")`
8542 /// — tunes the admin list view's FK auto-JOIN
8543 /// policy. Default `"all"` matches rustango's join-everything
8544 /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8545 list_select_related: Option<String>,
8546 /// `admin(formfield_overrides = "field:widget, field2:widget2")`.
8547 /// Each entry is `field_name:widget_name`; multiple
8548 /// entries comma-separated. Empty / unset → no overrides. The
8549 /// list of widget names supported is documented on
8550 /// `AdminConfig::formfield_overrides`. Issue #359.
8551 formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8552}
8553
8554fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8555 let mut out = ContainerAttrs {
8556 table: None,
8557 display: None,
8558 app: None,
8559 admin: None,
8560 audit: None,
8561 // Default `permissions = true` so every `#[derive(Model)]`
8562 // gets the four CRUD codenames seeded by `auto_create_permissions`
8563 // and is visible to non-superusers in the tenant admin without
8564 // manual per-model annotation. Models that intentionally don't
8565 // want permission rows (registry-internal types, framework
8566 // tables operators shouldn't manage directly) opt out via
8567 // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8568 // out-of-the-box admin invisibility regression (#62).
8569 permissions: true,
8570 m2m: Vec::new(),
8571 generic_m2m: Vec::new(),
8572 indexes: Vec::new(),
8573 checks: Vec::new(),
8574 excludes: Vec::new(),
8575 composite_fks: Vec::new(),
8576 generic_fks: Vec::new(),
8577 scope: None,
8578 manager_ext: None,
8579 manager_fns: Vec::new(),
8580 default_order: Vec::new(),
8581 is_view: false,
8582 managed: true,
8583 verbose_name: None,
8584 verbose_name_plural: None,
8585 base_manager_name: None,
8586 order_with_respect_to: None,
8587 proxy: false,
8588 required_db_features: Vec::new(),
8589 required_db_vendor: None,
8590 default_related_name: None,
8591 db_table_comment: None,
8592 get_latest_by: None,
8593 extra_permissions: Vec::new(),
8594 default_permissions: Vec::new(),
8595 global_scopes: Vec::new(),
8596 through_relations: Vec::new(),
8597 reverse_has_relations: Vec::new(),
8598 generic_has_relations: Vec::new(),
8599 };
8600 for attr in &input.attrs {
8601 if !attr.path().is_ident("rustango") {
8602 continue;
8603 }
8604 attr.parse_nested_meta(|meta| {
8605 if meta.path.is_ident("table") {
8606 let s: LitStr = meta.value()?.parse()?;
8607 let name = s.value();
8608 // v0.27.3 (#65) — macro-time guard against table names
8609 // that compile but break SQL downstream. Hyphens are
8610 // the common footgun: PostgreSQL accepts a quoted
8611 // `"intermediate-region"` in CREATE TABLE, but the
8612 // FK / index name derivation in `migrate::ddl`
8613 // emits `intermediate-region_field_fkey` unquoted,
8614 // which then fails the SQL parser. Same shape rule
8615 // as Postgres regular identifiers so the safe path
8616 // is the only path.
8617 validate_table_name(&name, s.span())?;
8618 out.table = Some(name);
8619 return Ok(());
8620 }
8621 if meta.path.is_ident("display") {
8622 let s: LitStr = meta.value()?.parse()?;
8623 out.display = Some((s.value(), s.span()));
8624 return Ok(());
8625 }
8626 if meta.path.is_ident("app") {
8627 let s: LitStr = meta.value()?.parse()?;
8628 out.app = Some(s.value());
8629 return Ok(());
8630 }
8631 if meta.path.is_ident("scope") {
8632 let s: LitStr = meta.value()?.parse()?;
8633 let val = s.value();
8634 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8635 return Err(meta.error(format!(
8636 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8637 )));
8638 }
8639 out.scope = Some(val);
8640 return Ok(());
8641 }
8642 if meta.path.is_ident("admin") {
8643 let mut admin = AdminAttrs::default();
8644 meta.parse_nested_meta(|inner| {
8645 if inner.path.is_ident("list_display") {
8646 let s: LitStr = inner.value()?.parse()?;
8647 admin.list_display =
8648 Some((split_field_list(&s.value()), s.span()));
8649 return Ok(());
8650 }
8651 if inner.path.is_ident("search_fields") {
8652 let s: LitStr = inner.value()?.parse()?;
8653 admin.search_fields =
8654 Some((split_field_list(&s.value()), s.span()));
8655 return Ok(());
8656 }
8657 if inner.path.is_ident("readonly_fields") {
8658 let s: LitStr = inner.value()?.parse()?;
8659 admin.readonly_fields =
8660 Some((split_field_list(&s.value()), s.span()));
8661 return Ok(());
8662 }
8663 if inner.path.is_ident("list_per_page") {
8664 let lit: syn::LitInt = inner.value()?.parse()?;
8665 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8666 return Ok(());
8667 }
8668 if inner.path.is_ident("ordering") {
8669 let s: LitStr = inner.value()?.parse()?;
8670 admin.ordering = Some((
8671 parse_ordering_list(&s.value()),
8672 s.span(),
8673 ));
8674 return Ok(());
8675 }
8676 if inner.path.is_ident("list_filter") {
8677 let s: LitStr = inner.value()?.parse()?;
8678 admin.list_filter =
8679 Some((split_field_list(&s.value()), s.span()));
8680 return Ok(());
8681 }
8682 if inner.path.is_ident("actions") {
8683 let s: LitStr = inner.value()?.parse()?;
8684 admin.actions =
8685 Some((split_field_list(&s.value()), s.span()));
8686 return Ok(());
8687 }
8688 if inner.path.is_ident("fieldsets") {
8689 let s: LitStr = inner.value()?.parse()?;
8690 admin.fieldsets =
8691 Some((parse_fieldset_list(&s.value()), s.span()));
8692 return Ok(());
8693 }
8694 if inner.path.is_ident("list_display_links") {
8695 let s: LitStr = inner.value()?.parse()?;
8696 admin.list_display_links =
8697 Some((split_field_list(&s.value()), s.span()));
8698 return Ok(());
8699 }
8700 if inner.path.is_ident("search_help_text") {
8701 let s: LitStr = inner.value()?.parse()?;
8702 admin.search_help_text = Some(s.value());
8703 return Ok(());
8704 }
8705 if inner.path.is_ident("actions_on_top") {
8706 let lit: syn::LitBool = inner.value()?.parse()?;
8707 admin.actions_on_top = Some(lit.value);
8708 return Ok(());
8709 }
8710 if inner.path.is_ident("actions_on_bottom") {
8711 let lit: syn::LitBool = inner.value()?.parse()?;
8712 admin.actions_on_bottom = Some(lit.value);
8713 return Ok(());
8714 }
8715 if inner.path.is_ident("date_hierarchy") {
8716 let s: LitStr = inner.value()?.parse()?;
8717 admin.date_hierarchy = Some(s.value());
8718 return Ok(());
8719 }
8720 if inner.path.is_ident("prepopulated_fields") {
8721 let s: LitStr = inner.value()?.parse()?;
8722 admin.prepopulated_fields =
8723 Some((parse_prepopulated_list(&s.value()), s.span()));
8724 return Ok(());
8725 }
8726 if inner.path.is_ident("raw_id_fields") {
8727 let s: LitStr = inner.value()?.parse()?;
8728 admin.raw_id_fields =
8729 Some((split_field_list(&s.value()), s.span()));
8730 return Ok(());
8731 }
8732 if inner.path.is_ident("autocomplete_fields") {
8733 let s: LitStr = inner.value()?.parse()?;
8734 admin.autocomplete_fields =
8735 Some((split_field_list(&s.value()), s.span()));
8736 return Ok(());
8737 }
8738 if inner.path.is_ident("list_select_related") {
8739 let s: LitStr = inner.value()?.parse()?;
8740 admin.list_select_related = Some(s.value());
8741 return Ok(());
8742 }
8743 if inner.path.is_ident("formfield_overrides") {
8744 let s: LitStr = inner.value()?.parse()?;
8745 admin.formfield_overrides =
8746 Some((parse_formfield_overrides(&s.value()), s.span()));
8747 return Ok(());
8748 }
8749 Err(inner.error(
8750 "unknown admin attribute (supported: \
8751 `list_display`, `list_display_links`, \
8752 `search_fields`, `search_help_text`, \
8753 `readonly_fields`, \
8754 `list_filter`, `list_per_page`, `ordering`, `actions`, \
8755 `actions_on_top`, `actions_on_bottom`, \
8756 `date_hierarchy`, \
8757 `prepopulated_fields`, \
8758 `raw_id_fields`, \
8759 `autocomplete_fields`, \
8760 `list_select_related`, \
8761 `formfield_overrides`, \
8762 `fieldsets`)",
8763 ))
8764 })?;
8765 out.admin = Some(admin);
8766 return Ok(());
8767 }
8768 if meta.path.is_ident("manager") {
8769 // `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8770 // Stretch `from_queryset = "..."` is left as a
8771 // follow-up — the issue's primary acceptance is the
8772 // `ext = ...` trait emission.
8773 meta.parse_nested_meta(|inner| {
8774 if inner.path.is_ident("ext") {
8775 let s: LitStr = inner.value()?.parse()?;
8776 let name = s.value();
8777 if name.is_empty() {
8778 return Err(inner.error("manager(ext = \"...\") cannot be empty"));
8779 }
8780 out.manager_ext =
8781 Some(syn::Ident::new(&name, s.span()));
8782 return Ok(());
8783 }
8784 Err(inner.error(
8785 "unknown manager attribute (supported: `ext = \"TraitName\"`)",
8786 ))
8787 })?;
8788 return Ok(());
8789 }
8790 if meta.path.is_ident("manager_fn") {
8791 // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8792 // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8793 // next to the default `Self::objects()`. Multiple
8794 // attributes accumulate.
8795 let s: LitStr = meta.value()?.parse()?;
8796 let name = s.value();
8797 if name.is_empty() {
8798 return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8799 }
8800 if name == "objects" {
8801 return Err(meta.error(
8802 "`manager_fn = \"objects\"` collides with the default \
8803 accessor — pick a different name",
8804 ));
8805 }
8806 let ident = syn::Ident::new(&name, s.span());
8807 if out.manager_fns.iter().any(|prev| *prev == ident) {
8808 return Err(meta.error(format!(
8809 "duplicate `manager_fn = \"{name}\"`"
8810 )));
8811 }
8812 out.manager_fns.push(ident);
8813 return Ok(());
8814 }
8815 if meta.path.is_ident("default_order") {
8816 // `#[rustango(default_order = "-created_at, status")]`
8817 // — issue #291 / T2.5. Comma-separated list; `-prefix`
8818 // means descending, `+prefix` or bare name means ascending.
8819 // Per-query opt-in via `QuerySet::with_default_order()`.
8820 let s: LitStr = meta.value()?.parse()?;
8821 let raw = s.value();
8822 let span = s.span();
8823 let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
8824 Vec::new();
8825 for entry in raw.split(',') {
8826 let trimmed = entry.trim();
8827 if trimmed.is_empty() {
8828 return Err(syn::Error::new(
8829 span,
8830 "`default_order = \"...\"` has an empty entry — \
8831 check for a stray comma",
8832 ));
8833 }
8834 let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
8835 (true, rest.trim().to_owned())
8836 } else if let Some(rest) = trimmed.strip_prefix('+') {
8837 (false, rest.trim().to_owned())
8838 } else {
8839 (false, trimmed.to_owned())
8840 };
8841 if name.is_empty() {
8842 return Err(syn::Error::new(
8843 span,
8844 "`default_order` entry has no column name after the prefix",
8845 ));
8846 }
8847 if parsed.iter().any(|(n, _, _)| *n == name) {
8848 return Err(syn::Error::new(
8849 span,
8850 format!("duplicate column `{name}` in `default_order`"),
8851 ));
8852 }
8853 parsed.push((name, desc, span));
8854 }
8855 if parsed.is_empty() {
8856 return Err(syn::Error::new(
8857 span,
8858 "`default_order = \"...\"` cannot be empty",
8859 ));
8860 }
8861 out.default_order = parsed;
8862 return Ok(());
8863 }
8864 if meta.path.is_ident("global_scope") {
8865 // `#[rustango(global_scope(name = "active", apply =
8866 // path::to::fn))]` — issue #820. The apply function
8867 // path resolves at macro-expand time in the consumer's
8868 // scope; the macro re-emits it verbatim into the
8869 // `ModelSchema::global_scopes` slice literal.
8870 let span = meta.path.span();
8871 let mut scope_name: Option<String> = None;
8872 let mut apply_path: Option<syn::Path> = None;
8873 meta.parse_nested_meta(|inner| {
8874 if inner.path.is_ident("name") {
8875 let s: LitStr = inner.value()?.parse()?;
8876 let raw = s.value();
8877 if raw.trim().is_empty() {
8878 return Err(syn::Error::new(
8879 s.span(),
8880 "`global_scope(name = \"...\")` must not be empty",
8881 ));
8882 }
8883 scope_name = Some(raw);
8884 return Ok(());
8885 }
8886 if inner.path.is_ident("apply") {
8887 let p: syn::Path = inner.value()?.parse()?;
8888 apply_path = Some(p);
8889 return Ok(());
8890 }
8891 Err(inner.error(
8892 "unknown `global_scope` attribute (supported: \
8893 `name`, `apply`)",
8894 ))
8895 })?;
8896 let Some(name) = scope_name else {
8897 return Err(syn::Error::new(
8898 span,
8899 "`global_scope` requires `name = \"...\"`",
8900 ));
8901 };
8902 let Some(apply) = apply_path else {
8903 return Err(syn::Error::new(
8904 span,
8905 "`global_scope` requires `apply = fn_path`",
8906 ));
8907 };
8908 if out.global_scopes.iter().any(|s| s.name == name) {
8909 return Err(syn::Error::new(
8910 span,
8911 format!(
8912 "duplicate global scope name `{name}` — \
8913 pick a unique identifier so \
8914 `QuerySet::without_global_scope(\"{name}\")` \
8915 is unambiguous"
8916 ),
8917 ));
8918 }
8919 out.global_scopes.push(GlobalScopeAttr { name, apply });
8920 return Ok(());
8921 }
8922 if meta.path.is_ident("through") {
8923 // `#[rustango(through(name = "posts", far = "Post",
8924 // far_fk_column = "author_id", intermediate = "User",
8925 // intermediate_fk_column = "country_id"
8926 // [, intermediate_pk_column = "id"]))]` — issue #817.
8927 let span = meta.path.span();
8928 let mut accessor_name: Option<String> = None;
8929 let mut far_ident: Option<syn::Ident> = None;
8930 let mut far_fk_column: Option<String> = None;
8931 let mut intermediate_ident: Option<syn::Ident> = None;
8932 let mut intermediate_fk_column: Option<String> = None;
8933 let mut intermediate_pk_column: Option<String> = None;
8934 fn parse_nonempty_string(
8935 inner: &syn::meta::ParseNestedMeta<'_>,
8936 field: &str,
8937 ) -> syn::Result<String> {
8938 let s: LitStr = inner.value()?.parse()?;
8939 let raw = s.value();
8940 let trimmed = raw.trim();
8941 if trimmed.is_empty() {
8942 return Err(syn::Error::new(
8943 s.span(),
8944 format!("`through({field} = \"...\")` must not be empty"),
8945 ));
8946 }
8947 Ok(trimmed.to_owned())
8948 }
8949 meta.parse_nested_meta(|inner| {
8950 if inner.path.is_ident("name") {
8951 accessor_name = Some(parse_nonempty_string(&inner, "name")?);
8952 return Ok(());
8953 }
8954 if inner.path.is_ident("far") {
8955 // Far model name accepted as a string literal
8956 // so the attribute fits inside the existing
8957 // `parse_nested_meta` shape; parsed back into a
8958 // `syn::Ident` so the emitted accessor can
8959 // reference the type directly.
8960 let s: LitStr = inner.value()?.parse()?;
8961 let raw = s.value();
8962 let trimmed = raw.trim();
8963 if trimmed.is_empty() {
8964 return Err(syn::Error::new(
8965 s.span(),
8966 "`through(far = \"...\")` must not be empty",
8967 ));
8968 }
8969 far_ident = Some(syn::Ident::new(trimmed, s.span()));
8970 return Ok(());
8971 }
8972 if inner.path.is_ident("far_fk_column") {
8973 far_fk_column =
8974 Some(parse_nonempty_string(&inner, "far_fk_column")?);
8975 return Ok(());
8976 }
8977 if inner.path.is_ident("intermediate") {
8978 let s: LitStr = inner.value()?.parse()?;
8979 let raw = s.value();
8980 let trimmed = raw.trim();
8981 if trimmed.is_empty() {
8982 return Err(syn::Error::new(
8983 s.span(),
8984 "`through(intermediate = \"...\")` must not be empty",
8985 ));
8986 }
8987 intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
8988 return Ok(());
8989 }
8990 if inner.path.is_ident("intermediate_fk_column") {
8991 intermediate_fk_column =
8992 Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
8993 return Ok(());
8994 }
8995 if inner.path.is_ident("intermediate_pk_column") {
8996 intermediate_pk_column =
8997 Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
8998 return Ok(());
8999 }
9000 Err(inner.error(
9001 "unknown `through` attribute (supported: \
9002 `name`, `far`, `far_fk_column`, \
9003 `intermediate`, `intermediate_fk_column`, \
9004 `intermediate_pk_column`)",
9005 ))
9006 })?;
9007 let Some(name) = accessor_name else {
9008 return Err(syn::Error::new(
9009 span,
9010 "`through` requires `name = \"...\"`",
9011 ));
9012 };
9013 let Some(far) = far_ident else {
9014 return Err(syn::Error::new(
9015 span,
9016 "`through` requires `far = \"FarModelType\"`",
9017 ));
9018 };
9019 let Some(far_fk_column) = far_fk_column else {
9020 return Err(syn::Error::new(
9021 span,
9022 "`through` requires `far_fk_column = \"<column>\"`",
9023 ));
9024 };
9025 let Some(intermediate) = intermediate_ident else {
9026 return Err(syn::Error::new(
9027 span,
9028 "`through` requires `intermediate = \"IntermediateModelType\"`",
9029 ));
9030 };
9031 let Some(intermediate_fk_column) = intermediate_fk_column else {
9032 return Err(syn::Error::new(
9033 span,
9034 "`through` requires `intermediate_fk_column = \"<column>\"`",
9035 ));
9036 };
9037 let intermediate_pk_column =
9038 intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9039 if out.through_relations.iter().any(|t| t.name == name) {
9040 return Err(syn::Error::new(
9041 span,
9042 format!(
9043 "duplicate `through(name = \"{name}\")` — \
9044 pick a unique accessor name"
9045 ),
9046 ));
9047 }
9048 out.through_relations.push(ThroughAttr {
9049 name,
9050 far,
9051 far_fk_column,
9052 intermediate,
9053 intermediate_fk_column,
9054 intermediate_pk_column,
9055 });
9056 return Ok(());
9057 }
9058 if meta.path.is_ident("reverse_has") {
9059 // `#[rustango(reverse_has(name = "comments",
9060 // child = "Comment", child_fk_column = "post_id"
9061 // [, self_pk_column = "id"]))]` — issue #830.
9062 let span = meta.path.span();
9063 let mut accessor_name: Option<String> = None;
9064 let mut child_ident: Option<syn::Ident> = None;
9065 let mut child_fk_column: Option<String> = None;
9066 let mut self_pk_column: Option<String> = None;
9067 meta.parse_nested_meta(|inner| {
9068 if inner.path.is_ident("name") {
9069 let s: LitStr = inner.value()?.parse()?;
9070 let raw = s.value();
9071 if raw.trim().is_empty() {
9072 return Err(syn::Error::new(
9073 s.span(),
9074 "`reverse_has(name = \"...\")` must not be empty",
9075 ));
9076 }
9077 accessor_name = Some(raw);
9078 return Ok(());
9079 }
9080 if inner.path.is_ident("child") {
9081 let s: LitStr = inner.value()?.parse()?;
9082 let raw = s.value();
9083 let trimmed = raw.trim();
9084 if trimmed.is_empty() {
9085 return Err(syn::Error::new(
9086 s.span(),
9087 "`reverse_has(child = \"...\")` must not be empty",
9088 ));
9089 }
9090 child_ident = Some(syn::Ident::new(trimmed, s.span()));
9091 return Ok(());
9092 }
9093 if inner.path.is_ident("child_fk_column") {
9094 let s: LitStr = inner.value()?.parse()?;
9095 let raw = s.value();
9096 let trimmed = raw.trim();
9097 if trimmed.is_empty() {
9098 return Err(syn::Error::new(
9099 s.span(),
9100 "`reverse_has(child_fk_column = \"...\")` must not be empty",
9101 ));
9102 }
9103 child_fk_column = Some(trimmed.to_owned());
9104 return Ok(());
9105 }
9106 if inner.path.is_ident("self_pk_column") {
9107 let s: LitStr = inner.value()?.parse()?;
9108 let raw = s.value();
9109 let trimmed = raw.trim();
9110 if trimmed.is_empty() {
9111 return Err(syn::Error::new(
9112 s.span(),
9113 "`reverse_has(self_pk_column = \"...\")` must not be empty",
9114 ));
9115 }
9116 self_pk_column = Some(trimmed.to_owned());
9117 return Ok(());
9118 }
9119 Err(inner.error(
9120 "unknown `reverse_has` attribute (supported: \
9121 `name`, `child`, `child_fk_column`, \
9122 `self_pk_column`)",
9123 ))
9124 })?;
9125 let Some(name) = accessor_name else {
9126 return Err(syn::Error::new(
9127 span,
9128 "`reverse_has` requires `name = \"...\"`",
9129 ));
9130 };
9131 let Some(child) = child_ident else {
9132 return Err(syn::Error::new(
9133 span,
9134 "`reverse_has` requires `child = \"ChildModelType\"`",
9135 ));
9136 };
9137 let Some(child_fk_column) = child_fk_column else {
9138 return Err(syn::Error::new(
9139 span,
9140 "`reverse_has` requires `child_fk_column = \"<column>\"`",
9141 ));
9142 };
9143 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9144 if out.reverse_has_relations.iter().any(|r| r.name == name) {
9145 return Err(syn::Error::new(
9146 span,
9147 format!(
9148 "duplicate `reverse_has(name = \"{name}\")` — \
9149 pick a unique accessor name"
9150 ),
9151 ));
9152 }
9153 out.reverse_has_relations.push(ReverseHasAttr {
9154 name,
9155 child,
9156 child_fk_column,
9157 self_pk_column,
9158 });
9159 return Ok(());
9160 }
9161 if meta.path.is_ident("generic_has") {
9162 // `#[rustango(generic_has(name = "tags",
9163 // child = "Tag", ct_column = "content_type_id",
9164 // pk_column = "object_pk" [, self_pk_column = "id"]))]`
9165 // — issue #830, the reverse generic-FK (GFK) arm of the
9166 // relation-existence family.
9167 let span = meta.path.span();
9168 let mut accessor_name: Option<String> = None;
9169 let mut child_ident: Option<syn::Ident> = None;
9170 let mut ct_column: Option<String> = None;
9171 let mut pk_column: Option<String> = None;
9172 let mut self_pk_column: Option<String> = None;
9173 meta.parse_nested_meta(|inner| {
9174 if inner.path.is_ident("name") {
9175 let s: LitStr = inner.value()?.parse()?;
9176 let raw = s.value();
9177 if raw.trim().is_empty() {
9178 return Err(syn::Error::new(
9179 s.span(),
9180 "`generic_has(name = \"...\")` must not be empty",
9181 ));
9182 }
9183 accessor_name = Some(raw);
9184 return Ok(());
9185 }
9186 if inner.path.is_ident("child") {
9187 let s: LitStr = inner.value()?.parse()?;
9188 let trimmed = s.value().trim().to_owned();
9189 if trimmed.is_empty() {
9190 return Err(syn::Error::new(
9191 s.span(),
9192 "`generic_has(child = \"...\")` must not be empty",
9193 ));
9194 }
9195 child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9196 return Ok(());
9197 }
9198 if inner.path.is_ident("ct_column") {
9199 let s: LitStr = inner.value()?.parse()?;
9200 let trimmed = s.value().trim().to_owned();
9201 if trimmed.is_empty() {
9202 return Err(syn::Error::new(
9203 s.span(),
9204 "`generic_has(ct_column = \"...\")` must not be empty",
9205 ));
9206 }
9207 ct_column = Some(trimmed);
9208 return Ok(());
9209 }
9210 if inner.path.is_ident("pk_column") {
9211 let s: LitStr = inner.value()?.parse()?;
9212 let trimmed = s.value().trim().to_owned();
9213 if trimmed.is_empty() {
9214 return Err(syn::Error::new(
9215 s.span(),
9216 "`generic_has(pk_column = \"...\")` must not be empty",
9217 ));
9218 }
9219 pk_column = Some(trimmed);
9220 return Ok(());
9221 }
9222 if inner.path.is_ident("self_pk_column") {
9223 let s: LitStr = inner.value()?.parse()?;
9224 let trimmed = s.value().trim().to_owned();
9225 if trimmed.is_empty() {
9226 return Err(syn::Error::new(
9227 s.span(),
9228 "`generic_has(self_pk_column = \"...\")` must not be empty",
9229 ));
9230 }
9231 self_pk_column = Some(trimmed);
9232 return Ok(());
9233 }
9234 Err(inner.error(
9235 "unknown `generic_has` attribute (supported: \
9236 `name`, `child`, `ct_column`, `pk_column`, \
9237 `self_pk_column`)",
9238 ))
9239 })?;
9240 let Some(name) = accessor_name else {
9241 return Err(syn::Error::new(
9242 span,
9243 "`generic_has` requires `name = \"...\"`",
9244 ));
9245 };
9246 let Some(child) = child_ident else {
9247 return Err(syn::Error::new(
9248 span,
9249 "`generic_has` requires `child = \"ChildModelType\"`",
9250 ));
9251 };
9252 let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9253 let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9254 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9255 if out.generic_has_relations.iter().any(|r| r.name == name) {
9256 return Err(syn::Error::new(
9257 span,
9258 format!(
9259 "duplicate `generic_has(name = \"{name}\")` — \
9260 pick a unique accessor name"
9261 ),
9262 ));
9263 }
9264 out.generic_has_relations.push(GenericHasAttr {
9265 name,
9266 child,
9267 ct_column,
9268 pk_column,
9269 self_pk_column,
9270 });
9271 return Ok(());
9272 }
9273 if meta.path.is_ident("audit") {
9274 let mut audit = AuditAttrs::default();
9275 meta.parse_nested_meta(|inner| {
9276 if inner.path.is_ident("track") {
9277 let s: LitStr = inner.value()?.parse()?;
9278 audit.track =
9279 Some((split_field_list(&s.value()), s.span()));
9280 return Ok(());
9281 }
9282 Err(inner.error(
9283 "unknown audit attribute (supported: `track`)",
9284 ))
9285 })?;
9286 out.audit = Some(audit);
9287 return Ok(());
9288 }
9289 if meta.path.is_ident("permissions") {
9290 // Two forms accepted:
9291 // #[rustango(permissions)] — flag form, true
9292 // #[rustango(permissions = false)] — explicit opt-out
9293 // #[rustango(permissions = true)] — explicit opt-in
9294 if let Ok(v) = meta.value() {
9295 let lit: syn::LitBool = v.parse()?;
9296 out.permissions = lit.value;
9297 } else {
9298 out.permissions = true;
9299 }
9300 return Ok(());
9301 }
9302 if meta.path.is_ident("view") {
9303 // Issue #293 / T2.10. Two forms accepted, matching
9304 // the `permissions` flag pattern:
9305 // #[rustango(view)] — flag form, true
9306 // #[rustango(view = false)] — explicit opt-out
9307 // #[rustango(view = true)] — explicit opt-in
9308 if let Ok(v) = meta.value() {
9309 let lit: syn::LitBool = v.parse()?;
9310 out.is_view = lit.value;
9311 } else {
9312 out.is_view = true;
9313 }
9314 return Ok(());
9315 }
9316 if meta.path.is_ident("managed") {
9317 // Who owns the table. Issue #321.
9318 // #[rustango(managed = false)] — operator-managed table
9319 // #[rustango(managed = true)] — rustango-managed (the default)
9320 // Bare-flag form is intentionally not accepted: writing
9321 // `#[rustango(managed)]` reads as "yes please manage it"
9322 // which is already the default. The opt-out is the only
9323 // useful state, so it must be explicit.
9324 let v = meta.value()?;
9325 let lit: syn::LitBool = v.parse()?;
9326 out.managed = lit.value;
9327 return Ok(());
9328 }
9329 if meta.path.is_ident("verbose_name") {
9330 let s: LitStr = meta.value()?.parse()?;
9331 out.verbose_name = Some(s.value());
9332 return Ok(());
9333 }
9334 if meta.path.is_ident("verbose_name_plural") {
9335 let s: LitStr = meta.value()?.parse()?;
9336 out.verbose_name_plural = Some(s.value());
9337 return Ok(());
9338 }
9339 if meta.path.is_ident("db_table_comment") {
9340 // Free-form table-level comment attached to the DB
9341 // catalog.
9342 let s: LitStr = meta.value()?.parse()?;
9343 out.db_table_comment = Some(s.value());
9344 return Ok(());
9345 }
9346 if meta.path.is_ident("proxy") {
9347 // Declarative flag
9348 // marking the struct as a proxy of another model that
9349 // shares its DB table. Stored on `ModelSchema::proxy`
9350 // so future codegen can skip `CreateTable` emission
9351 // for proxies (parent owns the table) and route
9352 // per-instance method resolution to the proxy class.
9353 //
9354 // Accepts `proxy` (bare → true) and `proxy = true/false`.
9355 let value = if meta.input.peek(syn::Token![=]) {
9356 meta.value()?.parse::<syn::LitBool>()?.value
9357 } else {
9358 true
9359 };
9360 out.proxy = value;
9361 return Ok(());
9362 }
9363 if meta.path.is_ident("order_with_respect_to") {
9364 // `order_with_respect_to = "parent_fk"` — the model's
9365 // instances are intrinsically ordered relative to
9366 // their parent FK.
9367 //
9368 // rustango stores the FK field name on
9369 // `ModelSchema::order_with_respect_to`. Declarative-only
9370 // today: the migration writer + admin surfaces still
9371 // treat every model identically. Future codegen will
9372 // key off the metadata to auto-emit the `_order`
9373 // column and reorder helpers.
9374 //
9375 // Validated as a Rust-shape identifier so the macro
9376 // can reject typos at derive time.
9377 let s: LitStr = meta.value()?.parse()?;
9378 let raw = s.value();
9379 if raw.is_empty() {
9380 return Err(syn::Error::new(
9381 s.span(),
9382 "`order_with_respect_to` must be a non-empty FK field name",
9383 ));
9384 }
9385 let valid = raw
9386 .chars()
9387 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9388 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9389 if !valid {
9390 return Err(syn::Error::new(
9391 s.span(),
9392 format!(
9393 "`order_with_respect_to` must be a valid Rust \
9394 identifier (letters / digits / underscores, \
9395 not starting with a digit); got `{raw}`"
9396 ),
9397 ));
9398 }
9399 out.order_with_respect_to = Some(raw);
9400 return Ok(());
9401 }
9402 if meta.path.is_ident("required_db_features") {
9403 // Capability tokens the model needs (e.g.
9404 // `"json_extract"`, `"window_functions"`,
9405 // `"row_security"`). Comma-separated.
9406 // `manage check --deploy` walks every model and warns
9407 // when the active backend doesn't advertise the
9408 // capability.
9409 //
9410 // rustango ships a small registry of capability tokens
9411 // each dialect supports — see `Dialect::supports`.
9412 // Unknown tokens still parse (they end up on the
9413 // schema and show up in the warning) so projects can
9414 // declare aspirational capabilities and the check
9415 // verb will keep nagging until the dialect implements
9416 // them.
9417 let s: LitStr = meta.value()?.parse()?;
9418 let raw = s.value();
9419 let features: Vec<String> = raw
9420 .split(',')
9421 .map(str::trim)
9422 .filter(|s| !s.is_empty())
9423 .map(str::to_owned)
9424 .collect();
9425 if features.is_empty() {
9426 return Err(syn::Error::new(
9427 s.span(),
9428 "`required_db_features` must list at least one \
9429 comma-separated capability token",
9430 ));
9431 }
9432 out.required_db_features = features;
9433 return Ok(());
9434 }
9435 if meta.path.is_ident("required_db_vendor") {
9436 // The model is only meant to run against the named DB
9437 // backend. `manage check --deploy` flags a mismatch so
9438 // ops catches "I forgot to switch DATABASE_URL" at
9439 // deploy time rather than runtime.
9440 //
9441 // Restricted to the three backends rustango ships
9442 // dialects for so the check verb can compare reliably.
9443 let s: LitStr = meta.value()?.parse()?;
9444 let raw = s.value().to_ascii_lowercase();
9445 match raw.as_str() {
9446 "postgresql" | "postgres" | "pg" => {
9447 out.required_db_vendor = Some("postgres".to_owned());
9448 }
9449 "mysql" | "mariadb" => {
9450 out.required_db_vendor = Some("mysql".to_owned());
9451 }
9452 "sqlite" | "sqlite3" => {
9453 out.required_db_vendor = Some("sqlite".to_owned());
9454 }
9455 _ => {
9456 return Err(syn::Error::new(
9457 s.span(),
9458 format!(
9459 "unknown required_db_vendor `{raw}` — \
9460 expected `postgres` (aliases: `postgresql`, `pg`), \
9461 `mysql` (alias: `mariadb`), or `sqlite` \
9462 (alias: `sqlite3`)"
9463 ),
9464 ));
9465 }
9466 }
9467 return Ok(());
9468 }
9469 if meta.path.is_ident("base_manager_name") {
9470 // Name of the manager that `<instance>.<relation>_set`
9471 // uses when resolving reverse-relation managers.
9472 // Distinct from `default_manager_name` (what
9473 // `Self::objects()` returns). Stored on
9474 // `ModelSchema::base_manager_name`.
9475 //
9476 // Validated as a Rust identifier so it stays safe to
9477 // re-emit as code in future reverse-manager codegen.
9478 let s: LitStr = meta.value()?.parse()?;
9479 let raw = s.value();
9480 if raw.is_empty() {
9481 return Err(syn::Error::new(
9482 s.span(),
9483 "`base_manager_name` must be a non-empty string",
9484 ));
9485 }
9486 let valid = raw
9487 .chars()
9488 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9489 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9490 if !valid {
9491 return Err(syn::Error::new(
9492 s.span(),
9493 format!(
9494 "`base_manager_name` must be a valid Rust \
9495 identifier (letters / digits / underscores, \
9496 not starting with a digit); got `{raw}`"
9497 ),
9498 ));
9499 }
9500 out.base_manager_name = Some(raw);
9501 return Ok(());
9502 }
9503 if meta.path.is_ident("default_related_name") {
9504 // The name reverse-relation accessors use when callers
9505 // don't override `related_name = "..."` on the FK / M2M
9506 // field. Stored on `ModelSchema::default_related_name`
9507 // so future reverse-manager codegen / schema emit /
9508 // admin templates can pick the right accessor name
9509 // (today rustango doesn't auto-emit reverse managers;
9510 // the metadata is the foundation for that work).
9511 //
9512 // Enforced non-empty + ASCII identifier-shape so the
9513 // string is safe to use as a Rust ident later.
9514 let s: LitStr = meta.value()?.parse()?;
9515 let raw = s.value();
9516 if raw.is_empty() {
9517 return Err(syn::Error::new(
9518 s.span(),
9519 "`default_related_name` must be a non-empty string",
9520 ));
9521 }
9522 let valid = raw
9523 .chars()
9524 .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9525 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9526 if !valid {
9527 return Err(syn::Error::new(
9528 s.span(),
9529 format!(
9530 "`default_related_name` must be snake_case ASCII \
9531 (lowercase letters / digits / underscores, not \
9532 starting with a digit); got `{raw}`"
9533 ),
9534 ));
9535 }
9536 out.default_related_name = Some(raw);
9537 return Ok(());
9538 }
9539 if meta.path.is_ident("extra_permissions") {
9540 // Comma-separated `codename:label` pairs.
9541 let s: LitStr = meta.value()?.parse()?;
9542 let raw = s.value();
9543 let mut pairs = Vec::new();
9544 for entry in raw.split(',') {
9545 let entry = entry.trim();
9546 if entry.is_empty() {
9547 continue;
9548 }
9549 let (codename, label) = match entry.split_once(':') {
9550 Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9551 None => (entry.to_owned(), entry.to_owned()),
9552 };
9553 if codename.is_empty() {
9554 return Err(meta.error(
9555 "`extra_permissions` entries must be `codename:label` pairs",
9556 ));
9557 }
9558 pairs.push((codename, label));
9559 }
9560 if pairs.is_empty() {
9561 return Err(meta
9562 .error("`extra_permissions = \"…\"` must list at least one pair"));
9563 }
9564 out.extra_permissions = pairs;
9565 return Ok(());
9566 }
9567 if meta.path.is_ident("default_permissions") {
9568 // Comma-separated subset of the CRUD action set.
9569 // Empty means all four (the framework default).
9570 let s: LitStr = meta.value()?.parse()?;
9571 let raw = s.value();
9572 let mut actions: Vec<String> = Vec::new();
9573 for entry in raw.split(',') {
9574 let action = entry.trim().to_ascii_lowercase();
9575 if action.is_empty() {
9576 continue;
9577 }
9578 match action.as_str() {
9579 "add" | "change" | "delete" | "view" => {}
9580 other => {
9581 return Err(syn::Error::new(
9582 s.span(),
9583 format!(
9584 "unknown default_permissions action `{other}` — \
9585 expected one of `add`, `change`, `delete`, `view`"
9586 ),
9587 ));
9588 }
9589 }
9590 if !actions.contains(&action) {
9591 actions.push(action);
9592 }
9593 }
9594 if actions.is_empty() {
9595 return Err(syn::Error::new(
9596 s.span(),
9597 "`default_permissions = \"…\"` must list at least one action; \
9598 use `permissions = false` on the container if you want NO \
9599 permissions seeded for this model.",
9600 ));
9601 }
9602 out.default_permissions = actions;
9603 return Ok(());
9604 }
9605 if meta.path.is_ident("get_latest_by") {
9606 // The `-` prefix selects descending order.
9607 let s: LitStr = meta.value()?.parse()?;
9608 let raw = s.value();
9609 let trimmed = raw.trim();
9610 if trimmed.is_empty() {
9611 return Err(meta.error("`get_latest_by` must name a column"));
9612 }
9613 let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9614 (stripped.to_owned(), true)
9615 } else if let Some(stripped) = trimmed.strip_prefix('+') {
9616 (stripped.to_owned(), false)
9617 } else {
9618 (trimmed.to_owned(), false)
9619 };
9620 if col.is_empty() {
9621 return Err(meta.error("`get_latest_by` must name a column"));
9622 }
9623 out.get_latest_by = Some((col, desc));
9624 return Ok(());
9625 }
9626 if meta.path.is_ident("unique_together") {
9627 // Composite UNIQUE index. Two syntaxes:
9628 //
9629 // #[rustango(unique_together = "org_id, user_id")] — auto-derived name
9630 // #[rustango(unique_together(columns = "org_id, user_id", name = "x"))] — explicit name
9631 //
9632 // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9633 // (col1, col2)`, where <name> defaults to
9634 // `<table>_<col1>_<col2>_uq` when not supplied.
9635 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9636 out.indexes.push(IndexAttr {
9637 name,
9638 columns,
9639 unique: true,
9640 method: "btree".to_owned(),
9641 where_clause: None,
9642 include: Vec::new(),
9643 });
9644 return Ok(());
9645 }
9646 if meta.path.is_ident("index_together") {
9647 // Composite (non-unique) index. Two syntaxes
9648 // mirroring `unique_together`.
9649 //
9650 // #[rustango(index_together = "created_at, status")]
9651 // #[rustango(index_together(columns = "created_at, status", name = "x"))]
9652 let (columns, name) = parse_together_attr(&meta, "index_together")?;
9653 out.indexes.push(IndexAttr {
9654 name,
9655 columns,
9656 unique: false,
9657 method: "btree".to_owned(),
9658 where_clause: None,
9659 include: Vec::new(),
9660 });
9661 return Ok(());
9662 }
9663 if meta.path.is_ident("unique_when") {
9664 // Partial unique index. Issue #265 / T1.3.
9665 //
9666 // #[rustango(unique_when(
9667 // columns = "email",
9668 // condition = "deleted_at IS NULL",
9669 // name = "unique_active_email"
9670 // ))]
9671 //
9672 // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9673 // on PG / SQLite (both ship partial indexes natively).
9674 // MySQL falls back to a plain UNIQUE index — the
9675 // condition is lost; document the limitation in the
9676 // generated migration.
9677 let mut columns: Option<Vec<String>> = None;
9678 let mut condition: Option<String> = None;
9679 let mut name: Option<String> = None;
9680 let mut include: Vec<String> = Vec::new();
9681 meta.parse_nested_meta(|inner| {
9682 if inner.path.is_ident("columns") {
9683 let s: LitStr = inner.value()?.parse()?;
9684 columns = Some(split_field_list(&s.value()));
9685 return Ok(());
9686 }
9687 if inner.path.is_ident("condition") {
9688 let s: LitStr = inner.value()?.parse()?;
9689 condition = Some(s.value());
9690 return Ok(());
9691 }
9692 if inner.path.is_ident("name") {
9693 let s: LitStr = inner.value()?.parse()?;
9694 name = Some(s.value());
9695 return Ok(());
9696 }
9697 if inner.path.is_ident("include") {
9698 // PG 11+ covering-index columns. Non-key columns
9699 // travel with the index leaf for index-only
9700 // scans. Dropped on MySQL/SQLite by the writer.
9701 let s: LitStr = inner.value()?.parse()?;
9702 include = split_field_list(&s.value());
9703 return Ok(());
9704 }
9705 Err(inner.error(
9706 "unknown unique_when attribute (supported: \
9707 `columns = \"...\"`, `condition = \"...\"`, \
9708 `name = \"...\"`, `include = \"...\"`)",
9709 ))
9710 })?;
9711 let columns = columns.ok_or_else(|| {
9712 meta.error("`unique_when(...)` requires `columns = \"...\"`")
9713 })?;
9714 let condition = condition.ok_or_else(|| {
9715 meta.error("`unique_when(...)` requires `condition = \"...\"`")
9716 })?;
9717 if columns.is_empty() {
9718 return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9719 }
9720 out.indexes.push(IndexAttr {
9721 name,
9722 columns,
9723 unique: true,
9724 method: "btree".to_owned(),
9725 where_clause: Some(condition),
9726 include,
9727 });
9728 return Ok(());
9729 }
9730 if meta.path.is_ident("index_when") {
9731 // Non-unique partial index. Sibling of `unique_when`
9732 // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9733 //
9734 // #[rustango(index_when(
9735 // columns = "status, created_at",
9736 // condition = "deleted_at IS NULL",
9737 // name = "active_status_created_idx"
9738 // ))]
9739 //
9740 // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9741 // on PG / SQLite (both ship partial indexes natively).
9742 // MySQL has no native partial-index support — the writer
9743 // emits a plain CREATE INDEX and the condition is lost;
9744 // operators wanting that selectivity on MySQL should
9745 // declare a covering index plus an application-level
9746 // filter.
9747 let mut columns: Option<Vec<String>> = None;
9748 let mut condition: Option<String> = None;
9749 let mut name: Option<String> = None;
9750 let mut method: String = "btree".to_owned();
9751 let mut include: Vec<String> = Vec::new();
9752 meta.parse_nested_meta(|inner| {
9753 if inner.path.is_ident("columns") {
9754 let s: LitStr = inner.value()?.parse()?;
9755 columns = Some(split_field_list(&s.value()));
9756 return Ok(());
9757 }
9758 if inner.path.is_ident("condition") {
9759 let s: LitStr = inner.value()?.parse()?;
9760 condition = Some(s.value());
9761 return Ok(());
9762 }
9763 if inner.path.is_ident("name") {
9764 let s: LitStr = inner.value()?.parse()?;
9765 name = Some(s.value());
9766 return Ok(());
9767 }
9768 if inner.path.is_ident("method") {
9769 let s: LitStr = inner.value()?.parse()?;
9770 method = s.value();
9771 return Ok(());
9772 }
9773 if inner.path.is_ident("include") {
9774 // PG 11+ covering-index columns; non-key
9775 // columns travel with the index leaf. Dropped
9776 // on MySQL/SQLite.
9777 let s: LitStr = inner.value()?.parse()?;
9778 include = split_field_list(&s.value());
9779 return Ok(());
9780 }
9781 Err(inner.error(
9782 "unknown index_when attribute (supported: \
9783 `columns = \"...\"`, `condition = \"...\"`, \
9784 `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9785 `include = \"...\"`)",
9786 ))
9787 })?;
9788 let columns = columns
9789 .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9790 let condition = condition.ok_or_else(|| {
9791 meta.error("`index_when(...)` requires `condition = \"...\"`")
9792 })?;
9793 if columns.is_empty() {
9794 return Err(meta.error("`index_when(columns = \"\")` is empty"));
9795 }
9796 out.indexes.push(IndexAttr {
9797 name,
9798 columns,
9799 unique: false,
9800 method,
9801 where_clause: Some(condition),
9802 include,
9803 });
9804 return Ok(());
9805 }
9806 if meta.path.is_ident("index") {
9807 // Container-level composite index. Two syntaxes:
9808 // #[rustango(index = "col1, col2")] — bare, non-unique btree
9809 // #[rustango(index("col1, col2"))] — call form (same result)
9810 // #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
9811 // The call form takes the column list as a leading string
9812 // literal, then optional `unique` / `name = "..."` /
9813 // `method = "..."` flags (a leading literal can't compose
9814 // under `parse_nested_meta`, so the paren body is parsed by
9815 // hand). `unique_together` / `index_together` remain
9816 // aliases for the same feature.
9817 let cols_lit: LitStr;
9818 let mut unique = false;
9819 let mut name: Option<String> = None;
9820 let mut method = "btree".to_owned();
9821 if meta.input.peek(syn::token::Paren) {
9822 let content;
9823 syn::parenthesized!(content in meta.input);
9824 cols_lit = content.parse()?;
9825 while content.peek(syn::Token![,]) {
9826 content.parse::<syn::Token![,]>()?;
9827 if content.is_empty() {
9828 break;
9829 }
9830 let flag: syn::Ident = content.parse()?;
9831 if flag == "unique" {
9832 unique = true;
9833 } else if flag == "name" {
9834 content.parse::<syn::Token![=]>()?;
9835 let s: LitStr = content.parse()?;
9836 name = Some(s.value());
9837 } else if flag == "method" {
9838 content.parse::<syn::Token![=]>()?;
9839 let s: LitStr = content.parse()?;
9840 let v = s.value();
9841 match v.as_str() {
9842 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
9843 | "bloom" => method = v,
9844 other => {
9845 return Err(syn::Error::new(
9846 s.span(),
9847 format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
9848 ));
9849 }
9850 }
9851 } else {
9852 return Err(syn::Error::new(
9853 flag.span(),
9854 "unknown index flag (supported: `unique`, `name`, `method`)",
9855 ));
9856 }
9857 }
9858 } else {
9859 cols_lit = meta.value()?.parse()?;
9860 }
9861 let columns = split_field_list(&cols_lit.value());
9862 out.indexes.push(IndexAttr {
9863 name,
9864 columns,
9865 unique,
9866 method,
9867 where_clause: None,
9868 include: Vec::new(),
9869 });
9870 return Ok(());
9871 }
9872 if meta.path.is_ident("check") {
9873 // #[rustango(check(name = "…", expr = "…"))]
9874 let mut name: Option<String> = None;
9875 let mut expr: Option<String> = None;
9876 meta.parse_nested_meta(|inner| {
9877 if inner.path.is_ident("name") {
9878 let s: LitStr = inner.value()?.parse()?;
9879 name = Some(s.value());
9880 return Ok(());
9881 }
9882 if inner.path.is_ident("expr") {
9883 let s: LitStr = inner.value()?.parse()?;
9884 expr = Some(s.value());
9885 return Ok(());
9886 }
9887 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
9888 })?;
9889 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
9890 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
9891 out.checks.push(CheckAttr { name, expr });
9892 return Ok(());
9893 }
9894 if meta.path.is_ident("exclude") {
9895 // #[rustango(exclude(name = "…", using = "gist",
9896 // elements = "col WITH op, col WITH op",
9897 // where = "…"))]
9898 let mut name: Option<String> = None;
9899 let mut using: Option<String> = None;
9900 let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
9901 let mut where_clause: Option<String> = None;
9902 meta.parse_nested_meta(|inner| {
9903 if inner.path.is_ident("name") {
9904 let s: LitStr = inner.value()?.parse()?;
9905 name = Some(s.value());
9906 return Ok(());
9907 }
9908 if inner.path.is_ident("using") {
9909 let s: LitStr = inner.value()?.parse()?;
9910 using = Some(s.value());
9911 return Ok(());
9912 }
9913 if inner.path.is_ident("elements") {
9914 let s: LitStr = inner.value()?.parse()?;
9915 elements_raw = Some((s.value(), s.span()));
9916 return Ok(());
9917 }
9918 if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
9919 let s: LitStr = inner.value()?.parse()?;
9920 where_clause = Some(s.value());
9921 return Ok(());
9922 }
9923 Err(inner.error(
9924 "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
9925 ))
9926 })?;
9927 let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
9928 let using = using.unwrap_or_else(|| "gist".to_owned());
9929 let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
9930 meta.error(
9931 "exclude requires `elements = \"col WITH op, col WITH op\"`",
9932 )
9933 })?;
9934 // Parse `col WITH op` pairs separated by commas.
9935 let mut elements: Vec<(String, String)> = Vec::new();
9936 for pair in elements_str.split(',') {
9937 let pair = pair.trim();
9938 if pair.is_empty() {
9939 continue;
9940 }
9941 let mut split = pair.splitn(2, |c: char| c.is_whitespace());
9942 let col = split.next().unwrap_or("").trim();
9943 let rest = split.next().unwrap_or("").trim();
9944 // `WITH op` — case-insensitive on `WITH`, then op.
9945 let rest_lc = rest.to_ascii_lowercase();
9946 let op = rest_lc
9947 .strip_prefix("with")
9948 .map(|r| r.trim_start())
9949 .filter(|r| !r.is_empty())
9950 .map(|_| {
9951 // Pull the original-case op from `rest` after the
9952 // `WITH ` token (5 chars).
9953 rest[4..].trim_start().to_owned()
9954 });
9955 let Some(op) = op else {
9956 return Err(syn::Error::new(
9957 elements_span,
9958 format!(
9959 "exclude elements: `{pair}` must be `<col> WITH <op>` \
9960 (e.g. `room_id WITH =` or `during WITH &&`)"
9961 ),
9962 ));
9963 };
9964 if col.is_empty() || op.is_empty() {
9965 return Err(syn::Error::new(
9966 elements_span,
9967 format!(
9968 "exclude elements: `{pair}` must be `<col> WITH <op>` \
9969 (both sides non-empty)"
9970 ),
9971 ));
9972 }
9973 elements.push((col.to_owned(), op));
9974 }
9975 if elements.is_empty() {
9976 return Err(syn::Error::new(
9977 elements_span,
9978 "exclude requires at least one `col WITH op` element",
9979 ));
9980 }
9981 out.excludes.push(ExcludeAttr {
9982 name,
9983 using,
9984 elements,
9985 where_clause,
9986 });
9987 return Ok(());
9988 }
9989 if meta.path.is_ident("generic_fk") {
9990 let mut gfk = GenericFkAttr {
9991 name: String::new(),
9992 ct_column: String::new(),
9993 pk_column: String::new(),
9994 };
9995 meta.parse_nested_meta(|inner| {
9996 if inner.path.is_ident("name") {
9997 let s: LitStr = inner.value()?.parse()?;
9998 gfk.name = s.value();
9999 return Ok(());
10000 }
10001 if inner.path.is_ident("ct_column") {
10002 let s: LitStr = inner.value()?.parse()?;
10003 gfk.ct_column = s.value();
10004 return Ok(());
10005 }
10006 if inner.path.is_ident("pk_column") {
10007 let s: LitStr = inner.value()?.parse()?;
10008 gfk.pk_column = s.value();
10009 return Ok(());
10010 }
10011 Err(inner.error(
10012 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10013 ))
10014 })?;
10015 if gfk.name.is_empty() {
10016 return Err(meta.error("generic_fk requires `name = \"...\"`"));
10017 }
10018 if gfk.ct_column.is_empty() {
10019 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10020 }
10021 if gfk.pk_column.is_empty() {
10022 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10023 }
10024 out.generic_fks.push(gfk);
10025 return Ok(());
10026 }
10027 if meta.path.is_ident("fk_composite") {
10028 let mut fk = CompositeFkAttr {
10029 name: String::new(),
10030 to: String::new(),
10031 from: Vec::new(),
10032 on: Vec::new(),
10033 };
10034 meta.parse_nested_meta(|inner| {
10035 if inner.path.is_ident("name") {
10036 let s: LitStr = inner.value()?.parse()?;
10037 fk.name = s.value();
10038 return Ok(());
10039 }
10040 if inner.path.is_ident("to") {
10041 let s: LitStr = inner.value()?.parse()?;
10042 fk.to = s.value();
10043 return Ok(());
10044 }
10045 // `on = ("col1", "col2", ...)` — parse a parenthesised
10046 // comma-list of string literals.
10047 if inner.path.is_ident("on") || inner.path.is_ident("from") {
10048 let value = inner.value()?;
10049 let content;
10050 syn::parenthesized!(content in value);
10051 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10052 content.parse_terminated(
10053 |p| p.parse::<syn::LitStr>(),
10054 syn::Token![,],
10055 )?;
10056 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10057 if inner.path.is_ident("on") {
10058 fk.on = cols;
10059 } else {
10060 fk.from = cols;
10061 }
10062 return Ok(());
10063 }
10064 Err(inner.error(
10065 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10066 ))
10067 })?;
10068 if fk.name.is_empty() {
10069 return Err(meta.error("fk_composite requires `name = \"...\"`"));
10070 }
10071 if fk.to.is_empty() {
10072 return Err(meta.error("fk_composite requires `to = \"...\"`"));
10073 }
10074 if fk.from.is_empty() || fk.on.is_empty() {
10075 return Err(meta.error(
10076 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10077 ));
10078 }
10079 if fk.from.len() != fk.on.len() {
10080 return Err(meta.error(format!(
10081 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10082 fk.from.len(),
10083 fk.on.len(),
10084 )));
10085 }
10086 out.composite_fks.push(fk);
10087 return Ok(());
10088 }
10089 if meta.path.is_ident("m2m") {
10090 let mut m2m = M2MAttr {
10091 name: String::new(),
10092 to: String::new(),
10093 through: String::new(),
10094 src: String::new(),
10095 dst: String::new(),
10096 auto_create: true,
10097 };
10098 meta.parse_nested_meta(|inner| {
10099 if inner.path.is_ident("name") {
10100 let s: LitStr = inner.value()?.parse()?;
10101 m2m.name = s.value();
10102 return Ok(());
10103 }
10104 if inner.path.is_ident("to") {
10105 let s: LitStr = inner.value()?.parse()?;
10106 m2m.to = s.value();
10107 return Ok(());
10108 }
10109 if inner.path.is_ident("through") {
10110 let s: LitStr = inner.value()?.parse()?;
10111 m2m.through = s.value();
10112 return Ok(());
10113 }
10114 if inner.path.is_ident("src") {
10115 let s: LitStr = inner.value()?.parse()?;
10116 m2m.src = s.value();
10117 return Ok(());
10118 }
10119 if inner.path.is_ident("dst") {
10120 let s: LitStr = inner.value()?.parse()?;
10121 m2m.dst = s.value();
10122 return Ok(());
10123 }
10124 if inner.path.is_ident("auto_create") {
10125 let lit: syn::LitBool = inner.value()?.parse()?;
10126 m2m.auto_create = lit.value;
10127 return Ok(());
10128 }
10129 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10130 })?;
10131 if m2m.name.is_empty() {
10132 return Err(meta.error("m2m requires `name = \"...\"`"));
10133 }
10134 if m2m.to.is_empty() {
10135 return Err(meta.error("m2m requires `to = \"...\"`"));
10136 }
10137 if m2m.through.is_empty() {
10138 return Err(meta.error("m2m requires `through = \"...\"`"));
10139 }
10140 if m2m.src.is_empty() {
10141 return Err(meta.error("m2m requires `src = \"...\"`"));
10142 }
10143 if m2m.dst.is_empty() {
10144 return Err(meta.error("m2m requires `dst = \"...\"`"));
10145 }
10146 out.m2m.push(m2m);
10147 return Ok(());
10148 }
10149 if meta.path.is_ident("generic_m2m") {
10150 let mut gm = GenericM2MAttr {
10151 name: String::new(),
10152 through: String::new(),
10153 pk_column: String::new(),
10154 ct_column: String::new(),
10155 related_column: String::new(),
10156 };
10157 meta.parse_nested_meta(|inner| {
10158 let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10159 let s: LitStr = inner.value()?.parse()?;
10160 Ok(s.value())
10161 };
10162 if inner.path.is_ident("name") {
10163 gm.name = field(&inner)?;
10164 return Ok(());
10165 }
10166 if inner.path.is_ident("through") {
10167 gm.through = field(&inner)?;
10168 return Ok(());
10169 }
10170 if inner.path.is_ident("pk_column") {
10171 gm.pk_column = field(&inner)?;
10172 return Ok(());
10173 }
10174 if inner.path.is_ident("ct_column") {
10175 gm.ct_column = field(&inner)?;
10176 return Ok(());
10177 }
10178 if inner.path.is_ident("related_column") {
10179 gm.related_column = field(&inner)?;
10180 return Ok(());
10181 }
10182 Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10183 })?;
10184 for (val, label) in [
10185 (&gm.name, "name"),
10186 (&gm.through, "through"),
10187 (&gm.pk_column, "pk_column"),
10188 (&gm.ct_column, "ct_column"),
10189 (&gm.related_column, "related_column"),
10190 ] {
10191 if val.is_empty() {
10192 return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10193 }
10194 }
10195 out.generic_m2m.push(gm);
10196 return Ok(());
10197 }
10198 Err(meta.error("unknown rustango container attribute"))
10199 })?;
10200 }
10201 Ok(out)
10202}
10203
10204/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10205/// owned field names, trimming whitespace and skipping empty entries.
10206/// Field-name validation against the model is done by the caller.
10207fn split_field_list(raw: &str) -> Vec<String> {
10208 raw.split(',')
10209 .map(str::trim)
10210 .filter(|s| !s.is_empty())
10211 .map(str::to_owned)
10212 .collect()
10213}
10214
10215/// Shared parser for `unique_together` and `index_together` container
10216/// attrs. Accepts both shapes:
10217///
10218/// * `attr = "col1, col2"` — auto-derived index name.
10219/// * `attr(columns = "col1, col2", name = "...")` — explicit name.
10220///
10221/// Returns `(columns, name)`.
10222fn parse_together_attr(
10223 meta: &syn::meta::ParseNestedMeta<'_>,
10224 attr: &str,
10225) -> syn::Result<(Vec<String>, Option<String>)> {
10226 // Disambiguate by whether the next token is `=` (key-value) or
10227 // `(` (parenthesized).
10228 if meta.input.peek(syn::Token![=]) {
10229 let cols_lit: LitStr = meta.value()?.parse()?;
10230 let columns = split_field_list(&cols_lit.value());
10231 check_together_columns(meta, attr, &columns)?;
10232 return Ok((columns, None));
10233 }
10234 let mut columns: Option<Vec<String>> = None;
10235 let mut name: Option<String> = None;
10236 meta.parse_nested_meta(|inner| {
10237 if inner.path.is_ident("columns") {
10238 let s: LitStr = inner.value()?.parse()?;
10239 columns = Some(split_field_list(&s.value()));
10240 return Ok(());
10241 }
10242 if inner.path.is_ident("name") {
10243 let s: LitStr = inner.value()?.parse()?;
10244 name = Some(s.value());
10245 return Ok(());
10246 }
10247 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10248 })?;
10249 let columns = columns.ok_or_else(|| {
10250 meta.error(format!(
10251 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10252 ))
10253 })?;
10254 check_together_columns(meta, attr, &columns)?;
10255 Ok((columns, name))
10256}
10257
10258fn check_together_columns(
10259 meta: &syn::meta::ParseNestedMeta<'_>,
10260 attr: &str,
10261 columns: &[String],
10262) -> syn::Result<()> {
10263 if columns.len() < 2 {
10264 let single = if attr == "unique_together" {
10265 "#[rustango(unique)] on the field"
10266 } else {
10267 "#[rustango(index)] on the field"
10268 };
10269 return Err(meta.error(format!(
10270 "{attr} expects two or more columns; for a single-column equivalent use {single}",
10271 )));
10272 }
10273 Ok(())
10274}
10275
10276/// Parse the fieldsets DSL: pipe-separated sections, optional
10277/// `"Title:"` prefix on each, comma-separated field names after.
10278/// Examples:
10279/// * `"name, office"` → one untitled section with two fields
10280/// * `"Identity: name, office | Metadata: created_at"` → two titled
10281/// sections
10282///
10283/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10284fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10285 raw.split('|')
10286 .map(str::trim)
10287 .filter(|s| !s.is_empty())
10288 .map(|section| {
10289 // Split off an optional `Title:` prefix (first colon).
10290 let (title, rest) = match section.split_once(':') {
10291 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10292 _ => (String::new(), section),
10293 };
10294 let fields = split_field_list(rest);
10295 (title, fields)
10296 })
10297 .collect()
10298}
10299
10300/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10301/// comma-separated entry maps a target field to one or more source
10302/// fields joined with `+`. Whitespace around tokens is trimmed.
10303/// Entries missing `:` or with empty target/source lists are dropped.
10304fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10305 raw.split(',')
10306 .map(str::trim)
10307 .filter(|s| !s.is_empty())
10308 .filter_map(|entry| {
10309 let (target, sources_raw) = entry.split_once(':')?;
10310 let target = target.trim().to_owned();
10311 if target.is_empty() {
10312 return None;
10313 }
10314 let sources: Vec<String> = sources_raw
10315 .split('+')
10316 .map(|s| s.trim().to_owned())
10317 .filter(|s| !s.is_empty())
10318 .collect();
10319 if sources.is_empty() {
10320 return None;
10321 }
10322 Some((target, sources))
10323 })
10324 .collect()
10325}
10326
10327/// Parse `formfield_overrides` — `"field:widget,field2:widget2"`
10328/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10329/// and empty halves drop silently — the macro layer only enforces shape,
10330/// not field-name vs. widget-name validity (those checks happen at
10331/// `AdminConfig` consumption time). Issue #359.
10332fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10333 raw.split(',')
10334 .map(str::trim)
10335 .filter(|s| !s.is_empty())
10336 .filter_map(|entry| {
10337 let (field, widget) = entry.split_once(':')?;
10338 let field = field.trim().to_owned();
10339 let widget = widget.trim().to_owned();
10340 if field.is_empty() || widget.is_empty() {
10341 return None;
10342 }
10343 Some((field, widget))
10344 })
10345 .collect()
10346}
10347
10348/// Parse an ordering list — `"name"` is ASC, `"-name"` is DESC.
10349/// Returns `(field_name, desc)` pairs in the same order as the input.
10350fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10351 raw.split(',')
10352 .map(str::trim)
10353 .filter(|s| !s.is_empty())
10354 .map(|spec| {
10355 spec.strip_prefix('-')
10356 .map_or((spec.to_owned(), false), |rest| {
10357 (rest.trim().to_owned(), true)
10358 })
10359 })
10360 .collect()
10361}
10362
10363struct FieldAttrs {
10364 column: Option<String>,
10365 primary_key: bool,
10366 fk: Option<String>,
10367 o2o: Option<String>,
10368 on: Option<String>,
10369 /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10370 /// "set_default" | "no_action")]` — what happens to this row when
10371 /// the row it points at is deleted.
10372 /// Only meaningful when `fk` / `o2o` is
10373 /// also set; the macro errors at compile time if applied to a
10374 /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10375 /// DDL writer renders `ON DELETE <action>` after the constraint
10376 /// clause when this is `Some`; `None` falls back to the database
10377 /// default (NO ACTION on every backend rustango supports).
10378 on_delete: Option<String>,
10379 /// `#[rustango(related_name = "...")]` — per-FK
10380 /// reverse-accessor override. When set, the derive emits
10381 /// `Parent::<related_name>[_pool]` instead of the container-level
10382 /// `default_related_name` or the `<child_snake>_set[_pool]`
10383 /// fallback. Only meaningful when `fk` / `o2o` is also set;
10384 /// silently ignored on non-FK fields. Follow-up to #816.
10385 related_name: Option<String>,
10386 max_length: Option<u32>,
10387 /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10388 /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10389 /// unconstrained `vector` column.
10390 vector_dims: Option<u32>,
10391 /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10392 /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10393 /// unconstrained `geometry(Point)` column (SRID 0).
10394 geometry_srid: Option<u32>,
10395 min: Option<i64>,
10396 max: Option<i64>,
10397 default: Option<String>,
10398 /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10399 /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10400 /// "gen_random_uuid()"`. The Rust field type must be
10401 /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10402 /// INSERTs so the DB DEFAULT fires.
10403 auto_uuid: bool,
10404 /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10405 /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10406 /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10407 /// the field is `Auto::Unset`, then bound as a normal parameter
10408 /// rather than relying on a per-dialect DB function. Issue #823
10409 /// (Eloquent `HasUuids`).
10410 ///
10411 /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10412 /// Composes with every backend (PG / MySQL / SQLite) — no
10413 /// `gen_random_uuid()` requirement on the database.
10414 default_uuid_v7: bool,
10415 /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10416 /// Server-set on insert, immutable from app code afterwards.
10417 /// Implies `auto + default = "now()"`. Field type must be
10418 /// `DateTime<Utc>`.
10419 auto_now_add: bool,
10420 /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10421 /// every insert AND every update. Implies `auto + default =
10422 /// "now()"`; the macro additionally rewrites `update_on` /
10423 /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10424 /// field value.
10425 auto_now: bool,
10426 /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10427 /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10428 /// `soft_delete_on(executor)` and `restore_on(executor)`
10429 /// methods on the model.
10430 soft_delete: bool,
10431 /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10432 /// the column in the generated DDL.
10433 unique: bool,
10434 /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10435 /// generates a `CREATE INDEX` for this column. `unique` here means
10436 /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10437 index: bool,
10438 index_unique: bool,
10439 index_name: Option<String>,
10440 /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10441 /// `"btree"`. Issue #34.
10442 index_method: String,
10443 /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10444 /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10445 /// the macro skips this column from every INSERT and UPDATE
10446 /// path, so the database always recomputes the value from
10447 /// `EXPR`. Backlog item #35.
10448 generated_as: Option<String>,
10449 /// `#[rustango(help_text = "…")]` — help text
10450 /// rendered below the admin form's input. Threaded into
10451 /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10452 /// layers can read it.
10453 help_text: Option<String>,
10454 /// `#[rustango(choices = "value:Label, value:Label")]` — the
10455 /// enumerated allowed values. Threaded into `FieldSchema::choices`
10456 /// as a `&'static [(&'static str, &'static str)]` slice. When
10457 /// present, the admin form renders a `<select>` instead of `<input>`
10458 /// and the validator rejects values not in the list. Only meaningful
10459 /// for `FieldType::String`; the macro errors at compile time if
10460 /// applied to a non-string field.
10461 choices: Option<Vec<(String, String)>>,
10462 /// `#[rustango(db_comment = "…")]` — DB-side column
10463 /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10464 /// the comment in CREATE TABLE; Postgres emits a separate
10465 /// `COMMENT ON COLUMN` statement after the table is created;
10466 /// SQLite silently drops the value (no native column comments).
10467 db_comment: Option<String>,
10468 /// `#[rustango(verbose_name = "…")]` — human-readable
10469 /// label for the field. Threaded into `FieldSchema::verbose_name`
10470 /// so admin column headers, form labels, and other display
10471 /// surfaces can prefer the friendly caption over the Rust
10472 /// identifier. `None` means renderers fall back to the field name.
10473 verbose_name: Option<String>,
10474 /// `#[rustango(editable = false)]` — opt out of
10475 /// auto-generated form rendering. Defaults to `true` so existing
10476 /// fields keep their current admin / form behavior; setting
10477 /// `false` removes the field from the admin change-form entirely
10478 /// (the value is still visible on detail / list views, just not
10479 /// editable).
10480 editable: bool,
10481 /// `#[rustango(blank)]` / `#[rustango(blank = true)]` —
10482 /// "form may submit empty even when DB is NOT NULL". Threaded into
10483 /// `FieldSchema::blank`. Defaults to `false`.
10484 blank: bool,
10485 /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10486 /// case-insensitive text column. Threaded into
10487 /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10488 /// fields; the macro errors at derive time if applied elsewhere.
10489 case_insensitive: bool,
10490 /// `#[rustango(validators = "email,url")]` — the
10491 /// model-level validator chain. Comma-separated names that
10492 /// dispatch to the `validators::*` family in `validate_value`.
10493 /// Empty by default; fires on every typed INSERT/UPDATE.
10494 validators: Vec<String>,
10495}
10496
10497fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10498 let mut out = FieldAttrs {
10499 column: None,
10500 primary_key: false,
10501 fk: None,
10502 o2o: None,
10503 on: None,
10504 on_delete: None,
10505 related_name: None,
10506 max_length: None,
10507 vector_dims: None,
10508 geometry_srid: None,
10509 min: None,
10510 max: None,
10511 default: None,
10512 auto_uuid: false,
10513 default_uuid_v7: false,
10514 auto_now_add: false,
10515 auto_now: false,
10516 soft_delete: false,
10517 unique: false,
10518 index: false,
10519 index_unique: false,
10520 index_name: None,
10521 index_method: "btree".to_owned(),
10522 generated_as: None,
10523 help_text: None,
10524 choices: None,
10525 db_comment: None,
10526 verbose_name: None,
10527 editable: true,
10528 blank: false,
10529 case_insensitive: false,
10530 validators: Vec::new(),
10531 };
10532 for attr in &field.attrs {
10533 if !attr.path().is_ident("rustango") {
10534 continue;
10535 }
10536 attr.parse_nested_meta(|meta| {
10537 if meta.path.is_ident("column") {
10538 let s: LitStr = meta.value()?.parse()?;
10539 let name = s.value();
10540 validate_sql_identifier(&name, "column", s.span())?;
10541 out.column = Some(name);
10542 return Ok(());
10543 }
10544 if meta.path.is_ident("primary_key") {
10545 out.primary_key = true;
10546 return Ok(());
10547 }
10548 if meta.path.is_ident("fk") {
10549 let s: LitStr = meta.value()?.parse()?;
10550 out.fk = Some(s.value());
10551 return Ok(());
10552 }
10553 if meta.path.is_ident("o2o") {
10554 let s: LitStr = meta.value()?.parse()?;
10555 out.o2o = Some(s.value());
10556 return Ok(());
10557 }
10558 if meta.path.is_ident("on") {
10559 let s: LitStr = meta.value()?.parse()?;
10560 out.on = Some(s.value());
10561 return Ok(());
10562 }
10563 if meta.path.is_ident("on_delete") {
10564 let s: LitStr = meta.value()?.parse()?;
10565 let raw = s.value();
10566 let normalized = raw.trim().to_ascii_lowercase();
10567 // Validate at parse time so the user gets a clear span
10568 // rather than a downstream compile error in the emit.
10569 match normalized.as_str() {
10570 "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10571 _ => {
10572 return Err(syn::Error::new(
10573 s.span(),
10574 format!(
10575 "unknown on_delete action `{raw}`; expected one of \
10576 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10577 ),
10578 ));
10579 }
10580 }
10581 out.on_delete = Some(normalized);
10582 return Ok(());
10583 }
10584 if meta.path.is_ident("related_name") {
10585 let s: LitStr = meta.value()?.parse()?;
10586 let raw = s.value();
10587 if raw.trim().is_empty() {
10588 return Err(syn::Error::new(
10589 s.span(),
10590 "`related_name` must be a non-empty identifier",
10591 ));
10592 }
10593 // Validate as a Rust-ident shape — the value becomes
10594 // a method name on the parent type. Same rule as
10595 // `default_related_name` so the two surfaces match.
10596 if !raw
10597 .chars()
10598 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10599 || raw.starts_with(char::is_numeric)
10600 {
10601 return Err(syn::Error::new(
10602 s.span(),
10603 "`related_name` must be snake_case ASCII (lowercase letters, \
10604 digits, underscores; no leading digit)",
10605 ));
10606 }
10607 out.related_name = Some(raw);
10608 return Ok(());
10609 }
10610 if meta.path.is_ident("max_length") {
10611 let lit: syn::LitInt = meta.value()?.parse()?;
10612 out.max_length = Some(lit.base10_parse::<u32>()?);
10613 return Ok(());
10614 }
10615 // `#[rustango(vector(dims = N))]` — pgvector column
10616 // dimension (#824). Nested-meta form so it reads like the
10617 // other typed-column attrs.
10618 if meta.path.is_ident("vector") {
10619 meta.parse_nested_meta(|inner| {
10620 if inner.path.is_ident("dims") {
10621 let lit: syn::LitInt = inner.value()?.parse()?;
10622 out.vector_dims = Some(lit.base10_parse::<u32>()?);
10623 return Ok(());
10624 }
10625 Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10626 })?;
10627 return Ok(());
10628 }
10629 // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10630 // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10631 if meta.path.is_ident("geometry") {
10632 meta.parse_nested_meta(|inner| {
10633 if inner.path.is_ident("srid") {
10634 let lit: syn::LitInt = inner.value()?.parse()?;
10635 out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10636 return Ok(());
10637 }
10638 Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10639 })?;
10640 return Ok(());
10641 }
10642 if meta.path.is_ident("min") {
10643 out.min = Some(parse_signed_i64(&meta)?);
10644 return Ok(());
10645 }
10646 if meta.path.is_ident("max") {
10647 out.max = Some(parse_signed_i64(&meta)?);
10648 return Ok(());
10649 }
10650 if meta.path.is_ident("default") {
10651 let s: LitStr = meta.value()?.parse()?;
10652 out.default = Some(s.value());
10653 return Ok(());
10654 }
10655 if meta.path.is_ident("generated_as") {
10656 let s: LitStr = meta.value()?.parse()?;
10657 out.generated_as = Some(s.value());
10658 return Ok(());
10659 }
10660 if meta.path.is_ident("help_text") {
10661 let s: LitStr = meta.value()?.parse()?;
10662 out.help_text = Some(s.value());
10663 return Ok(());
10664 }
10665 if meta.path.is_ident("choices") {
10666 let s: LitStr = meta.value()?.parse()?;
10667 let raw = s.value();
10668 let mut pairs: Vec<(String, String)> = Vec::new();
10669 for chunk in raw.split(',') {
10670 let chunk = chunk.trim();
10671 if chunk.is_empty() {
10672 continue;
10673 }
10674 let (value, label) = match chunk.split_once(':') {
10675 Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10676 None => (chunk.to_owned(), chunk.to_owned()),
10677 };
10678 if value.is_empty() {
10679 return Err(syn::Error::new(
10680 s.span(),
10681 "`choices` entry has empty value before `:`",
10682 ));
10683 }
10684 pairs.push((value, label));
10685 }
10686 if pairs.is_empty() {
10687 return Err(syn::Error::new(
10688 s.span(),
10689 "`choices = \"…\"` must contain at least one value",
10690 ));
10691 }
10692 out.choices = Some(pairs);
10693 return Ok(());
10694 }
10695 if meta.path.is_ident("db_comment") {
10696 let s: LitStr = meta.value()?.parse()?;
10697 out.db_comment = Some(s.value());
10698 return Ok(());
10699 }
10700 if meta.path.is_ident("verbose_name") {
10701 let s: LitStr = meta.value()?.parse()?;
10702 out.verbose_name = Some(s.value());
10703 return Ok(());
10704 }
10705 if meta.path.is_ident("editable") {
10706 // Two forms accepted:
10707 // #[rustango(editable = false)] / true — explicit
10708 // #[rustango(editable)] — flag form (= true, the
10709 // default, so harmless; included for symmetry)
10710 if let Ok(v) = meta.value() {
10711 let lit: syn::LitBool = v.parse()?;
10712 out.editable = lit.value;
10713 } else {
10714 out.editable = true;
10715 }
10716 return Ok(());
10717 }
10718 if meta.path.is_ident("blank") {
10719 // Two forms accepted:
10720 // #[rustango(blank)] — flag form, true
10721 // #[rustango(blank = false)] / true — explicit
10722 if let Ok(v) = meta.value() {
10723 let lit: syn::LitBool = v.parse()?;
10724 out.blank = lit.value;
10725 } else {
10726 out.blank = true;
10727 }
10728 return Ok(());
10729 }
10730 if meta.path.is_ident("citext") {
10731 // Case-insensitive text column (#344). Two forms:
10732 // #[rustango(citext)] — flag form, true
10733 // #[rustango(citext = true)] — explicit
10734 // #[rustango(citext = false)] — explicit opt-out
10735 // String-only validation lives in the field-type
10736 // emitter (the FieldType discriminant is computed in
10737 // `detect_type`); the macro records the flag and the
10738 // DDL writer emits dialect-specific COLLATE / CITEXT.
10739 if let Ok(v) = meta.value() {
10740 let lit: syn::LitBool = v.parse()?;
10741 out.case_insensitive = lit.value;
10742 } else {
10743 out.case_insensitive = true;
10744 }
10745 return Ok(());
10746 }
10747 if meta.path.is_ident("validators") {
10748 let s: LitStr = meta.value()?.parse()?;
10749 let raw = s.value();
10750 out.validators = raw
10751 .split(',')
10752 .map(str::trim)
10753 .filter(|s| !s.is_empty())
10754 .map(str::to_owned)
10755 .collect();
10756 if out.validators.is_empty() {
10757 return Err(syn::Error::new(
10758 s.span(),
10759 "`validators = \"…\"` must list at least one name",
10760 ));
10761 }
10762 return Ok(());
10763 }
10764 if meta.path.is_ident("auto_uuid") {
10765 out.auto_uuid = true;
10766 // Implied: PK + auto + DEFAULT gen_random_uuid().
10767 // Each is also explicitly settable; the explicit
10768 // value wins if conflicting.
10769 out.primary_key = true;
10770 if out.default.is_none() {
10771 out.default = Some("gen_random_uuid()".into());
10772 }
10773 return Ok(());
10774 }
10775 if meta.path.is_ident("default_uuid_v7") {
10776 // Backend-neutral counterpart of `auto_uuid` — issue #823.
10777 // No SQL DEFAULT (the macro fills the value Rust-side
10778 // before binding); just mark the field as PK + Auto
10779 // so the insert path is the `Auto::Unset → generate`
10780 // branch.
10781 out.default_uuid_v7 = true;
10782 out.primary_key = true;
10783 return Ok(());
10784 }
10785 if meta.path.is_ident("auto_now_add") {
10786 out.auto_now_add = true;
10787 if out.default.is_none() {
10788 out.default = Some("now()".into());
10789 }
10790 return Ok(());
10791 }
10792 if meta.path.is_ident("auto_now") {
10793 out.auto_now = true;
10794 if out.default.is_none() {
10795 out.default = Some("now()".into());
10796 }
10797 return Ok(());
10798 }
10799 if meta.path.is_ident("soft_delete") {
10800 out.soft_delete = true;
10801 return Ok(());
10802 }
10803 if meta.path.is_ident("unique") {
10804 out.unique = true;
10805 return Ok(());
10806 }
10807 if meta.path.is_ident("index") {
10808 out.index = true;
10809 // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
10810 if meta.input.peek(syn::token::Paren) {
10811 meta.parse_nested_meta(|inner| {
10812 if inner.path.is_ident("unique") {
10813 out.index_unique = true;
10814 return Ok(());
10815 }
10816 if inner.path.is_ident("name") {
10817 let s: LitStr = inner.value()?.parse()?;
10818 out.index_name = Some(s.value());
10819 return Ok(());
10820 }
10821 if inner.path.is_ident("method") {
10822 let s: LitStr = inner.value()?.parse()?;
10823 let v = s.value();
10824 match v.as_str() {
10825 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
10826 out.index_method = v;
10827 }
10828 other => {
10829 return Err(inner.error(format!(
10830 "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
10831 )));
10832 }
10833 }
10834 return Ok(());
10835 }
10836 Err(inner.error(
10837 "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
10838 ))
10839 })?;
10840 }
10841 return Ok(());
10842 }
10843 Err(meta.error("unknown rustango field attribute"))
10844 })?;
10845 }
10846 Ok(out)
10847}
10848
10849/// Parse a signed integer literal, accepting optional leading `-`.
10850fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
10851 let expr: syn::Expr = meta.value()?.parse()?;
10852 match expr {
10853 syn::Expr::Lit(syn::ExprLit {
10854 lit: syn::Lit::Int(lit),
10855 ..
10856 }) => lit.base10_parse::<i64>(),
10857 syn::Expr::Unary(syn::ExprUnary {
10858 op: syn::UnOp::Neg(_),
10859 expr,
10860 ..
10861 }) => {
10862 if let syn::Expr::Lit(syn::ExprLit {
10863 lit: syn::Lit::Int(lit),
10864 ..
10865 }) = *expr
10866 {
10867 let v: i64 = lit.base10_parse()?;
10868 Ok(-v)
10869 } else {
10870 Err(syn::Error::new_spanned(expr, "expected integer literal"))
10871 }
10872 }
10873 other => Err(syn::Error::new_spanned(
10874 other,
10875 "expected integer literal (signed)",
10876 )),
10877 }
10878}
10879
10880struct FieldInfo<'a> {
10881 ident: &'a syn::Ident,
10882 column: String,
10883 primary_key: bool,
10884 /// `true` when the Rust type was `Auto<T>` — the INSERT path will
10885 /// skip this column when `Auto::Unset` and emit it under
10886 /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
10887 auto: bool,
10888 /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
10889 /// the `Column::Value` associated type for typed-column tokens.
10890 value_ty: &'a Type,
10891 /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
10892 field_type_tokens: TokenStream2,
10893 schema: TokenStream2,
10894 from_row_init: TokenStream2,
10895 /// Variant of [`Self::from_row_init`] that reads the column via
10896 /// `format!("{prefix}__{col}")` so a model can be decoded out of
10897 /// the aliased columns of a JOINed row. Drives slice 9.0d's
10898 /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
10899 /// helper that `select_related` calls when stitching loaded FKs.
10900 from_aliased_row_init: TokenStream2,
10901 /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
10902 /// relation helper emit (`Author::<child>_set`) needs to know `T`
10903 /// to point the generated method at the right child model.
10904 fk_inner: Option<Type>,
10905 /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
10906 /// `kind` (since ForeignKey detection sets `kind` to K's
10907 /// underlying type) but stored separately for clarity at the
10908 /// `FkRelation` construction site, which only sees the FK's
10909 /// surface fields.
10910 fk_pk_kind: DetectedKind,
10911 /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
10912 /// the bare `ForeignKey<T, K>`. Routes the load_related and
10913 /// fk_pk_access emitters to wrap assignments / accessors in
10914 /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
10915 /// FK column compiles end-to-end. The DDL writer reads this off
10916 /// the field schema (`nullable` flag); the macro just needs to
10917 /// keep the Rust-side codegen consistent.
10918 nullable: bool,
10919 /// `true` when this column was marked `#[rustango(auto_now)]` —
10920 /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
10921 /// column instead of the user-supplied value, so `updated_at`
10922 /// always reflects the latest write without the caller having
10923 /// to remember to set it.
10924 auto_now: bool,
10925 /// `true` when this column was marked `#[rustango(auto_now_add)]`
10926 /// — the column is server-set on INSERT (DB DEFAULT) and
10927 /// **immutable** afterwards. `update_on` / `save_on` skip the
10928 /// column entirely so a stale `created_at` value in memory never
10929 /// rewrites the persisted timestamp.
10930 auto_now_add: bool,
10931 /// `true` when this column was marked `#[rustango(soft_delete)]`.
10932 /// Triggers emission of `soft_delete_on(executor)` and
10933 /// `restore_on(executor)` on the model's inherent impl. There is
10934 /// at most one such column per model — emission asserts this.
10935 soft_delete: bool,
10936 /// `Some` when this column was marked
10937 /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
10938 /// every INSERT and UPDATE path; the database recomputes the
10939 /// value from `EXPR`. Backlog item #35.
10940 generated_as: Option<String>,
10941 /// `true` when this column was marked
10942 /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
10943 /// emit an `insert_push` that auto-fills an `Auto::Unset` value
10944 /// with `Uuid::now_v7()` before binding, so the PK is generated
10945 /// Rust-side and the column is always present in the INSERT
10946 /// statement (no DB DEFAULT requirement). Issue #823.
10947 default_uuid_v7: bool,
10948 /// `Some` when this FK field carried `#[rustango(related_name =
10949 /// "...")]`. Threaded into [`FkRelation::related_name`] and
10950 /// consumed by [`reverse_helper_tokens`] to override the default
10951 /// `<child_snake>_set` accessor name. Follow-up to #816.
10952 related_name: Option<String>,
10953}
10954
10955/// Reject table names that won't survive SQL identifier
10956/// derivation downstream. Postgres' regular-identifier rule
10957/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
10958/// through the framework's unquoted FK / index / constraint name
10959/// emission without surprises. We also disallow leading-digit and
10960/// the empty string for clarity.
10961///
10962/// Reserved-word collisions (`select`, `from`, …) aren't flagged
10963/// here — those produce a runtime error from the SQL parser,
10964/// which is loud enough; statically enumerating reserved words
10965/// across the three supported dialects is more friction than help.
10966///
10967/// Backlog item #65.
10968fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
10969 validate_sql_identifier(name, "table", span)
10970}
10971
10972/// Reject SQL identifiers that compile but break downstream SQL
10973/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
10974/// `kind` is "table" / "column" — used for the error message so users
10975/// see which attribute caused the failure.
10976fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
10977 if name.is_empty() {
10978 return Err(syn::Error::new(
10979 span,
10980 format!("`{kind} = \"\"` is not a valid SQL identifier"),
10981 ));
10982 }
10983 let mut chars = name.chars();
10984 let first = chars.next().unwrap();
10985 if !(first.is_ascii_alphabetic() || first == '_') {
10986 return Err(syn::Error::new(
10987 span,
10988 format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
10989 ));
10990 }
10991 for c in chars {
10992 if !(c.is_ascii_alphanumeric() || c == '_') {
10993 return Err(syn::Error::new(
10994 span,
10995 format!(
10996 "{kind} name `{name}` contains invalid character {c:?} — \
10997 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
10998 Hyphens in particular break FK / index name derivation \
10999 downstream; use underscores instead (e.g. `{}`)",
11000 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11001 ),
11002 ));
11003 }
11004 }
11005 Ok(())
11006}
11007
11008fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11009 let root = rustango_root();
11010 let attrs = parse_field_attrs(field)?;
11011 let ident = field
11012 .ident
11013 .as_ref()
11014 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11015 let name = ident.to_string();
11016 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11017 let primary_key = attrs.primary_key;
11018 let DetectedType {
11019 kind,
11020 nullable,
11021 auto: detected_auto,
11022 fk_inner,
11023 } = detect_type(&field.ty)?;
11024 check_bound_compatibility(field, &attrs, kind)?;
11025 let auto = detected_auto;
11026 // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11027 // INSERT path: the user must wrap the field in `Auto<T>`, which
11028 // marks the column as DB-default-supplied. The mixin attrs then
11029 // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11030 // for `auto_now`, force the value on UPDATE too.
11031 if attrs.auto_uuid {
11032 if kind != DetectedKind::Uuid {
11033 return Err(syn::Error::new_spanned(
11034 field,
11035 "`#[rustango(auto_uuid)]` requires the field type to be \
11036 `Auto<uuid::Uuid>`",
11037 ));
11038 }
11039 if !detected_auto {
11040 return Err(syn::Error::new_spanned(
11041 field,
11042 "`#[rustango(auto_uuid)]` requires the field type to be \
11043 wrapped in `Auto<...>` so the macro skips the column on \
11044 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11045 ));
11046 }
11047 }
11048 if attrs.default_uuid_v7 {
11049 if kind != DetectedKind::Uuid {
11050 return Err(syn::Error::new_spanned(
11051 field,
11052 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11053 `Auto<uuid::Uuid>`",
11054 ));
11055 }
11056 if !detected_auto {
11057 return Err(syn::Error::new_spanned(
11058 field,
11059 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11060 wrapped in `Auto<...>` so the macro can detect the \
11061 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11062 ));
11063 }
11064 if attrs.auto_uuid {
11065 return Err(syn::Error::new_spanned(
11066 field,
11067 "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11068 `#[rustango(auto_uuid)]` — the former generates the UUID \
11069 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11070 Pick one.",
11071 ));
11072 }
11073 }
11074 if attrs.auto_now_add || attrs.auto_now {
11075 if kind != DetectedKind::DateTime {
11076 return Err(syn::Error::new_spanned(
11077 field,
11078 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11079 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11080 ));
11081 }
11082 if !detected_auto {
11083 return Err(syn::Error::new_spanned(
11084 field,
11085 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11086 the field type to be wrapped in `Auto<...>` so the macro skips \
11087 the column on INSERT and the DB DEFAULT (`now()`) fires",
11088 ));
11089 }
11090 }
11091 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11092 return Err(syn::Error::new_spanned(
11093 field,
11094 "`#[rustango(soft_delete)]` requires the field type to be \
11095 `Option<chrono::DateTime<chrono::Utc>>`",
11096 ));
11097 }
11098 let is_mixin_auto =
11099 attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11100 if detected_auto && !primary_key && !is_mixin_auto {
11101 return Err(syn::Error::new_spanned(
11102 field,
11103 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11104 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11105 `auto_now`",
11106 ));
11107 }
11108 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11109 return Err(syn::Error::new_spanned(
11110 field,
11111 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11112 SERIAL / BIGSERIAL already supplies a default sequence.",
11113 ));
11114 }
11115 if fk_inner.is_some() && primary_key {
11116 return Err(syn::Error::new_spanned(
11117 field,
11118 "`ForeignKey<T>` is not allowed on a primary-key field — \
11119 a row's PK is its own identity, not a reference to a parent.",
11120 ));
11121 }
11122 if attrs.generated_as.is_some() {
11123 if primary_key {
11124 return Err(syn::Error::new_spanned(
11125 field,
11126 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11127 primary-key field — a PK must be writable so the row \
11128 has an identity at INSERT time.",
11129 ));
11130 }
11131 if attrs.default.is_some() {
11132 return Err(syn::Error::new_spanned(
11133 field,
11134 "`#[rustango(generated_as = \"…\")]` cannot combine with \
11135 `default = \"…\"` — Postgres rejects DEFAULT on \
11136 generated columns. The expression IS the default.",
11137 ));
11138 }
11139 if detected_auto {
11140 return Err(syn::Error::new_spanned(
11141 field,
11142 "`#[rustango(generated_as = \"…\")]` is not allowed on \
11143 an `Auto<T>` field — generated columns are computed \
11144 by the DB, not server-assigned via a sequence. Use a \
11145 plain Rust type (e.g. `f64`).",
11146 ));
11147 }
11148 if fk_inner.is_some() {
11149 return Err(syn::Error::new_spanned(
11150 field,
11151 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11152 ForeignKey field.",
11153 ));
11154 }
11155 }
11156 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11157 let column_lit = column.as_str();
11158 // pgvector (#824): the `vector(dims = N)` attribute supplies the
11159 // dimension that the bare `Vector` Rust type can't carry, so emit
11160 // `FieldType::Vector(N)` here rather than the `variant_tokens`
11161 // fallback of `Vector(0)`.
11162 let field_type_tokens = if kind == DetectedKind::Vector {
11163 let root = rustango_root();
11164 let dims = attrs.vector_dims.unwrap_or(0);
11165 quote!(#root::core::FieldType::Vector(#dims))
11166 } else if kind == DetectedKind::Geometry {
11167 // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11168 // SRID that the bare `Point` Rust type can't carry, so emit
11169 // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11170 let root = rustango_root();
11171 let srid = attrs.geometry_srid.unwrap_or(0);
11172 quote!(#root::core::FieldType::Geometry(#srid))
11173 } else {
11174 kind.variant_tokens()
11175 };
11176 let max_length = optional_u32(attrs.max_length);
11177 let min = optional_i64(attrs.min);
11178 let max = optional_i64(attrs.max);
11179 let default = optional_str(attrs.default.as_deref());
11180
11181 let unique = attrs.unique;
11182 let generated_as = optional_str(attrs.generated_as.as_deref());
11183 let help_text = optional_str(attrs.help_text.as_deref());
11184 let choices = optional_choices(attrs.choices.as_deref());
11185 let db_comment = optional_str(attrs.db_comment.as_deref());
11186 let verbose_name = optional_str(attrs.verbose_name.as_deref());
11187 let editable = attrs.editable;
11188 let blank = attrs.blank;
11189 let case_insensitive = attrs.case_insensitive;
11190 let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11191 if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() {
11192 return Err(syn::Error::new_spanned(
11193 field,
11194 "`#[rustango(on_delete = \"…\")]` requires either `fk = \"<table>\"` \
11195 or `o2o = \"<table>\"` on the same field — it has no meaning on a \
11196 non-FK column.",
11197 ));
11198 }
11199 let fk_on_delete = match attrs.on_delete.as_deref() {
11200 None => quote!(::core::option::Option::None),
11201 Some(action) => {
11202 let variant = match action {
11203 "cascade" => quote!(Cascade),
11204 "restrict" => quote!(Restrict),
11205 "set_null" => quote!(SetNull),
11206 "set_default" => quote!(SetDefault),
11207 "no_action" => quote!(NoAction),
11208 // parse_field_attrs already validated this — guard against future drift.
11209 other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11210 };
11211 quote!(::core::option::Option::Some(
11212 #root::core::OnDeleteAction::#variant
11213 ))
11214 }
11215 };
11216 let schema = quote! {{
11217 let mut f = #root::core::FieldSchema::new(#name, #column_lit, #field_type_tokens);
11218 f.nullable = #nullable;
11219 f.primary_key = #primary_key;
11220 f.relation = #relation;
11221 f.max_length = #max_length;
11222 f.min = #min;
11223 f.max = #max;
11224 f.default = #default;
11225 f.auto = #auto;
11226 f.unique = #unique;
11227 f.generated_as = #generated_as;
11228 f.help_text = #help_text;
11229 f.choices = #choices;
11230 f.db_comment = #db_comment;
11231 f.verbose_name = #verbose_name;
11232 f.editable = #editable;
11233 f.blank = #blank;
11234 f.case_insensitive = #case_insensitive;
11235 f.fk_on_delete = #fk_on_delete;
11236 f.validators = &[ #(#validators_lits),* ];
11237 f
11238 }};
11239
11240 let from_row_init = quote! {
11241 #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11242 };
11243 let from_aliased_row_init = quote! {
11244 #ident: #root::sql::sqlx::Row::try_get(
11245 row,
11246 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11247 )?
11248 };
11249
11250 Ok(FieldInfo {
11251 ident,
11252 column,
11253 primary_key,
11254 auto,
11255 value_ty: &field.ty,
11256 field_type_tokens,
11257 schema,
11258 from_row_init,
11259 from_aliased_row_init,
11260 fk_inner: fk_inner.cloned(),
11261 fk_pk_kind: kind,
11262 nullable,
11263 auto_now: attrs.auto_now,
11264 auto_now_add: attrs.auto_now_add,
11265 soft_delete: attrs.soft_delete,
11266 generated_as: attrs.generated_as.clone(),
11267 default_uuid_v7: attrs.default_uuid_v7,
11268 related_name: attrs.related_name.clone(),
11269 })
11270}
11271
11272fn check_bound_compatibility(
11273 field: &syn::Field,
11274 attrs: &FieldAttrs,
11275 kind: DetectedKind,
11276) -> syn::Result<()> {
11277 if attrs.max_length.is_some() && kind != DetectedKind::String {
11278 return Err(syn::Error::new_spanned(
11279 field,
11280 "`max_length` is only valid on `String` fields (or `Option<String>`)",
11281 ));
11282 }
11283 if attrs.choices.is_some() && kind != DetectedKind::String {
11284 return Err(syn::Error::new_spanned(
11285 field,
11286 "`choices` is only valid on `String` fields (or `Option<String>`) — \
11287 integer-valued enumerations should be modeled with a Rust enum and \
11288 custom (de)serializer for now",
11289 ));
11290 }
11291 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11292 return Err(syn::Error::new_spanned(
11293 field,
11294 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11295 ));
11296 }
11297 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11298 if min > max {
11299 return Err(syn::Error::new_spanned(
11300 field,
11301 format!("`min` ({min}) is greater than `max` ({max})"),
11302 ));
11303 }
11304 }
11305 Ok(())
11306}
11307
11308/// A `&'static [ty]` of `const fn` results. The slice needs its own
11309/// `const`: a `&[..]` assigned in a statement is not extended to `'static`.
11310fn const_slice(ty: &TokenStream2, items: impl Iterator<Item = TokenStream2>) -> TokenStream2 {
11311 quote!({
11312 const ITEMS: &[#ty] = &[ #(#items),* ];
11313 ITEMS
11314 })
11315}
11316
11317fn optional_u32(value: Option<u32>) -> TokenStream2 {
11318 if let Some(v) = value {
11319 quote!(::core::option::Option::Some(#v))
11320 } else {
11321 quote!(::core::option::Option::None)
11322 }
11323}
11324
11325fn optional_i64(value: Option<i64>) -> TokenStream2 {
11326 if let Some(v) = value {
11327 quote!(::core::option::Option::Some(#v))
11328 } else {
11329 quote!(::core::option::Option::None)
11330 }
11331}
11332
11333fn optional_str(value: Option<&str>) -> TokenStream2 {
11334 if let Some(v) = value {
11335 quote!(::core::option::Option::Some(#v))
11336 } else {
11337 quote!(::core::option::Option::None)
11338 }
11339}
11340
11341fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11342 let Some(pairs) = pairs else {
11343 return quote!(::core::option::Option::None);
11344 };
11345 let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11346 quote!(::core::option::Option::Some(&[#(#entries),*]))
11347}
11348
11349fn relation_tokens(
11350 field: &syn::Field,
11351 attrs: &FieldAttrs,
11352 fk_inner: Option<&syn::Type>,
11353 table: &str,
11354) -> syn::Result<TokenStream2> {
11355 let root = rustango_root();
11356 if let Some(inner) = fk_inner {
11357 if attrs.fk.is_some() || attrs.o2o.is_some() {
11358 return Err(syn::Error::new_spanned(
11359 field,
11360 "`ForeignKey<T>` already declares the FK target via the type parameter — \
11361 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11362 ));
11363 }
11364 let on = attrs.on.as_deref().unwrap_or("id");
11365 return Ok(quote! {
11366 ::core::option::Option::Some(#root::core::Relation::fk(
11367 <#inner as #root::core::Model>::SCHEMA.table,
11368 #on,
11369 ))
11370 });
11371 }
11372 match (&attrs.fk, &attrs.o2o) {
11373 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11374 field,
11375 "`fk` and `o2o` are mutually exclusive",
11376 )),
11377 (Some(to), None) => {
11378 let on = attrs.on.as_deref().unwrap_or("id");
11379 // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11380 // the model's own table. Threaded as a literal string at
11381 // macro-expansion time to sidestep the const-eval cycle
11382 // that `Self::SCHEMA.table` would create when referenced
11383 // inside Self::SCHEMA's own initializer.
11384 let resolved = if to == "self" { table } else { to };
11385 Ok(quote! {
11386 ::core::option::Option::Some(#root::core::Relation::fk(#resolved, #on))
11387 })
11388 }
11389 (None, Some(to)) => {
11390 let on = attrs.on.as_deref().unwrap_or("id");
11391 let resolved = if to == "self" { table } else { to };
11392 Ok(quote! {
11393 ::core::option::Option::Some(#root::core::Relation::o2o(#resolved, #on))
11394 })
11395 }
11396 (None, None) => {
11397 if attrs.on.is_some() {
11398 return Err(syn::Error::new_spanned(
11399 field,
11400 "`on` requires `fk` or `o2o`",
11401 ));
11402 }
11403 Ok(quote!(::core::option::Option::None))
11404 }
11405 }
11406}
11407
11408/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11409/// about kinds without depending on `rustango-core` (which would require a
11410/// proc-macro/normal split it doesn't have today).
11411#[derive(Clone, Copy, PartialEq, Eq)]
11412enum DetectedKind {
11413 I16,
11414 I32,
11415 I64,
11416 F32,
11417 F64,
11418 Bool,
11419 String,
11420 DateTime,
11421 Date,
11422 Time,
11423 Uuid,
11424 Json,
11425 Decimal,
11426 Binary,
11427 /// `Array<String>` → PG `text[]` (#341).
11428 ArrayText,
11429 /// `Array<i32>` → PG `integer[]` (#341).
11430 ArrayInt,
11431 /// `Array<i64>` → PG `bigint[]` (#341).
11432 ArrayBigInt,
11433 /// `Range<i32>` → PG `int4range` (#343).
11434 RangeInt,
11435 /// `Range<i64>` → PG `int8range` (#343).
11436 RangeBigInt,
11437 /// `Range<Decimal>` → PG `numrange` (#343).
11438 RangeNumeric,
11439 /// `Range<NaiveDate>` → PG `daterange` (#343).
11440 RangeDate,
11441 /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11442 RangeDateTime,
11443 /// `HStore` → PG `hstore` (#342).
11444 HStore,
11445 /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11446 /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11447 /// in at the `FieldType` emission site (not carried on this enum).
11448 Vector,
11449 /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11450 /// from the `#[rustango(geometry(srid = N))]` field attribute,
11451 /// threaded in at the `FieldType` emission site.
11452 Geometry,
11453}
11454
11455impl DetectedKind {
11456 fn variant_tokens(self) -> TokenStream2 {
11457 let root = rustango_root();
11458 match self {
11459 Self::I16 => quote!(#root::core::FieldType::I16),
11460 Self::I32 => quote!(#root::core::FieldType::I32),
11461 Self::I64 => quote!(#root::core::FieldType::I64),
11462 Self::F32 => quote!(#root::core::FieldType::F32),
11463 Self::F64 => quote!(#root::core::FieldType::F64),
11464 Self::Bool => quote!(#root::core::FieldType::Bool),
11465 Self::String => quote!(#root::core::FieldType::String),
11466 Self::DateTime => quote!(#root::core::FieldType::DateTime),
11467 Self::Date => quote!(#root::core::FieldType::Date),
11468 Self::Time => quote!(#root::core::FieldType::Time),
11469 Self::Uuid => quote!(#root::core::FieldType::Uuid),
11470 Self::Json => quote!(#root::core::FieldType::Json),
11471 Self::Decimal => quote!(#root::core::FieldType::Decimal),
11472 Self::Binary => quote!(#root::core::FieldType::Binary),
11473 Self::ArrayText => {
11474 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11475 }
11476 Self::ArrayInt => {
11477 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11478 }
11479 Self::ArrayBigInt => {
11480 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11481 }
11482 Self::RangeInt => {
11483 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11484 }
11485 Self::RangeBigInt => {
11486 quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11487 }
11488 Self::RangeNumeric => {
11489 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11490 }
11491 Self::RangeDate => {
11492 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11493 }
11494 Self::RangeDateTime => {
11495 quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11496 }
11497 Self::HStore => quote!(#root::core::FieldType::HStore),
11498 // Dimension comes from the `vector(dims = N)` attribute,
11499 // applied at the emission site; `0` here is just a fallback.
11500 Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11501 // SRID comes from the `geometry(srid = N)` attribute, applied
11502 // at the emission site; `0` here is just a fallback.
11503 Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11504 }
11505 }
11506
11507 fn is_integer(self) -> bool {
11508 matches!(self, Self::I16 | Self::I32 | Self::I64)
11509 }
11510
11511 /// `(SqlValue::<Variant>, default expr)` for emitting the
11512 /// `match SqlValue { … }` arm in `LoadRelated::__rustango_load_related`
11513 /// for a `ForeignKey<T, K>` FK whose K maps to `self`. The default
11514 /// fires only when the parent's `__rustango_pk_value` returns a
11515 /// different variant than expected, which is a compile-time bug —
11516 /// but we still need a value-typed fallback to keep the match
11517 /// total.
11518 fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
11519 let root = rustango_root();
11520 match self {
11521 Self::I16 => (quote!(I16), quote!(0i16)),
11522 Self::I32 => (quote!(I32), quote!(0i32)),
11523 Self::I64 => (quote!(I64), quote!(0i64)),
11524 Self::F32 => (quote!(F32), quote!(0f32)),
11525 Self::F64 => (quote!(F64), quote!(0f64)),
11526 Self::Bool => (quote!(Bool), quote!(false)),
11527 Self::String => (quote!(String), quote!(::std::string::String::new())),
11528 Self::DateTime => (
11529 quote!(DateTime),
11530 quote!(<#root::__chrono::DateTime<#root::__chrono::Utc> as ::std::default::Default>::default()),
11531 ),
11532 Self::Date => (
11533 quote!(Date),
11534 quote!(<#root::__chrono::NaiveDate as ::std::default::Default>::default()),
11535 ),
11536 Self::Time => (
11537 quote!(Time),
11538 quote!(<#root::__chrono::NaiveTime as ::std::default::Default>::default()),
11539 ),
11540 Self::Uuid => (quote!(Uuid), quote!(#root::__uuid::Uuid::nil())),
11541 Self::Json => (quote!(Json), quote!(#root::__serde_json::Value::Null)),
11542 Self::Decimal => (
11543 quote!(Decimal),
11544 quote!(<#root::__rust_decimal::Decimal as ::std::default::Default>::default()),
11545 ),
11546 Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
11547 // Arrays (#341) can never be a foreign-key primary key, so
11548 // this arm is never reached at runtime — but the match must
11549 // stay total. A bare empty `SqlValue::Array` is the dummy.
11550 Self::ArrayText | Self::ArrayInt | Self::ArrayBigInt => {
11551 (quote!(Array), quote!(::std::vec::Vec::new()))
11552 }
11553 // Ranges (#343) likewise can't be a FK PK — never reached.
11554 Self::RangeInt
11555 | Self::RangeBigInt
11556 | Self::RangeNumeric
11557 | Self::RangeDate
11558 | Self::RangeDateTime => (quote!(RangeLiteral), quote!(::std::string::String::new())),
11559 // HStore (#342) can't be a FK PK — never reached.
11560 Self::HStore => (quote!(HStore), quote!(::std::vec::Vec::new())),
11561 // Vector (#824) can't be a FK PK — never reached.
11562 Self::Vector => (quote!(Vector), quote!(::std::vec::Vec::new())),
11563 // Geometry (#443) can't be a FK PK — never reached (arm is
11564 // exhaustiveness-only; never interpolated into emitted code).
11565 Self::Geometry => (quote!(Geometry), quote!(::std::vec::Vec::new())),
11566 }
11567 }
11568}
11569
11570/// Result of walking a field's Rust type. `kind` is the underlying
11571/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11572/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11573/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11574/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11575#[derive(Clone, Copy)]
11576struct DetectedType<'a> {
11577 kind: DetectedKind,
11578 nullable: bool,
11579 auto: bool,
11580 fk_inner: Option<&'a syn::Type>,
11581}
11582
11583/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11584/// non-`Auto` types — the caller should already have routed Auto-only
11585/// codegen through this helper, so a `None` indicates a macro-internal
11586/// invariant break.
11587fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11588 let Type::Path(TypePath { path, qself: None }) = ty else {
11589 return None;
11590 };
11591 let last = path.segments.last()?;
11592 if last.ident != "Auto" {
11593 return None;
11594 }
11595 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11596 return None;
11597 };
11598 args.args.iter().find_map(|a| match a {
11599 syn::GenericArgument::Type(t) => Some(t),
11600 _ => None,
11601 })
11602}
11603
11604fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11605 let Type::Path(TypePath { path, qself: None }) = ty else {
11606 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11607 };
11608 let last = path
11609 .segments
11610 .last()
11611 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11612
11613 if last.ident == "Option" {
11614 let inner = generic_inner(ty, &last.arguments, "Option")?;
11615 let inner_det = detect_type(inner)?;
11616 if inner_det.nullable {
11617 return Err(syn::Error::new_spanned(
11618 ty,
11619 "nested Option is not supported",
11620 ));
11621 }
11622 if inner_det.auto {
11623 return Err(syn::Error::new_spanned(
11624 ty,
11625 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11626 ));
11627 }
11628 return Ok(DetectedType {
11629 nullable: true,
11630 ..inner_det
11631 });
11632 }
11633
11634 if last.ident == "Auto" {
11635 let inner = generic_inner(ty, &last.arguments, "Auto")?;
11636 let inner_det = detect_type(inner)?;
11637 if inner_det.auto {
11638 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11639 }
11640 if inner_det.nullable {
11641 return Err(syn::Error::new_spanned(
11642 ty,
11643 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11644 ));
11645 }
11646 if inner_det.fk_inner.is_some() {
11647 return Err(syn::Error::new_spanned(
11648 ty,
11649 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11650 ));
11651 }
11652 if !matches!(
11653 inner_det.kind,
11654 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11655 ) {
11656 return Err(syn::Error::new_spanned(
11657 ty,
11658 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11659 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11660 (DEFAULT now())",
11661 ));
11662 }
11663 return Ok(DetectedType {
11664 auto: true,
11665 ..inner_det
11666 });
11667 }
11668
11669 if last.ident == "ForeignKey" {
11670 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11671 // Resolve the FK column's underlying SQL type from `K`. When the
11672 // user wrote `ForeignKey<T>` without a key parameter, the type
11673 // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11674 // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11675 // recurse into K so the column DDL emits the right SQL type
11676 // (VARCHAR for String, UUID for Uuid, …) and the load_related
11677 // emitter knows which `SqlValue` variant to match.
11678 let kind = match key_ty {
11679 Some(k) => detect_type(k)?.kind,
11680 None => DetectedKind::I64,
11681 };
11682 return Ok(DetectedType {
11683 kind,
11684 nullable: false,
11685 auto: false,
11686 fk_inner: Some(inner),
11687 });
11688 }
11689
11690 let kind = match last.ident.to_string().as_str() {
11691 "i16" => DetectedKind::I16,
11692 "i32" => DetectedKind::I32,
11693 "i64" => DetectedKind::I64,
11694 "f32" => DetectedKind::F32,
11695 "f64" => DetectedKind::F64,
11696 "bool" => DetectedKind::Bool,
11697 "String" => DetectedKind::String,
11698 "DateTime" => DetectedKind::DateTime,
11699 "NaiveDate" => DetectedKind::Date,
11700 "NaiveTime" => DetectedKind::Time,
11701 "Uuid" => DetectedKind::Uuid,
11702 "Value" => DetectedKind::Json,
11703 "Decimal" => DetectedKind::Decimal,
11704 // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11705 // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11706 // — that would emit Binary DDL and decode-fail at runtime.
11707 "Vec" => {
11708 let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11709 if let Type::Path(TypePath { path, qself: None }) = inner {
11710 if let Some(seg) = path.segments.last() {
11711 if seg.ident == "u8" && seg.arguments.is_empty() {
11712 return Ok(DetectedType {
11713 kind: DetectedKind::Binary,
11714 nullable: false,
11715 auto: false,
11716 fk_inner: None,
11717 });
11718 }
11719 }
11720 }
11721 return Err(syn::Error::new_spanned(
11722 ty,
11723 "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11724 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11725 ));
11726 }
11727 // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11728 // `integer[]` / `bigint[]` (#341).
11729 "Array" => {
11730 let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11731 let elem = match inner {
11732 Type::Path(TypePath { path, qself: None }) => {
11733 path.segments.last().map(|s| s.ident.to_string())
11734 }
11735 _ => None,
11736 };
11737 let kind = match elem.as_deref() {
11738 Some("String") => DetectedKind::ArrayText,
11739 Some("i32") => DetectedKind::ArrayInt,
11740 Some("i64") => DetectedKind::ArrayBigInt,
11741 _ => {
11742 return Err(syn::Error::new_spanned(
11743 ty,
11744 "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
11745 `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
11746 ));
11747 }
11748 };
11749 return Ok(DetectedType {
11750 kind,
11751 nullable: false,
11752 auto: false,
11753 fk_inner: None,
11754 });
11755 }
11756 // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
11757 // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
11758 // `int8range` / `numrange` / `daterange` / `tstzrange` (#343).
11759 "Range" => {
11760 let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
11761 let elem = match inner {
11762 Type::Path(TypePath { path, qself: None }) => {
11763 path.segments.last().map(|s| s.ident.to_string())
11764 }
11765 _ => None,
11766 };
11767 let kind = match elem.as_deref() {
11768 Some("i32") => DetectedKind::RangeInt,
11769 Some("i64") => DetectedKind::RangeBigInt,
11770 Some("Decimal") => DetectedKind::RangeNumeric,
11771 Some("NaiveDate") => DetectedKind::RangeDate,
11772 Some("DateTime") => DetectedKind::RangeDateTime,
11773 _ => {
11774 return Err(syn::Error::new_spanned(
11775 ty,
11776 "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
11777 `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
11778 `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
11779 (→ tstzrange) are supported (#343)",
11780 ));
11781 }
11782 };
11783 return Ok(DetectedType {
11784 kind,
11785 nullable: false,
11786 auto: false,
11787 fk_inner: None,
11788 });
11789 }
11790 // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
11791 // (the `CastValue` impl bridges logical↔stored); the field's own
11792 // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
11793 // schema just needs `FieldType::String`.
11794 "Cast" => {
11795 return Ok(DetectedType {
11796 kind: DetectedKind::String,
11797 nullable: false,
11798 auto: false,
11799 fk_inner: None,
11800 });
11801 }
11802 // `HStore` → PG `hstore` (#342). No generic
11803 // parameter — always a string→string map.
11804 "HStore" => {
11805 return Ok(DetectedType {
11806 kind: DetectedKind::HStore,
11807 nullable: false,
11808 auto: false,
11809 fk_inner: None,
11810 });
11811 }
11812 // `Vector` → pgvector `vector(N)` (#824). The dimension is
11813 // supplied by `#[rustango(vector(dims = N))]`, not the type.
11814 "Vector" => {
11815 return Ok(DetectedType {
11816 kind: DetectedKind::Vector,
11817 nullable: false,
11818 auto: false,
11819 fk_inner: None,
11820 });
11821 }
11822 // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
11823 // supplied by `#[rustango(geometry(srid = N))]`, not the type.
11824 "Point" => {
11825 return Ok(DetectedType {
11826 kind: DetectedKind::Geometry,
11827 nullable: false,
11828 auto: false,
11829 fk_inner: None,
11830 });
11831 }
11832 other => {
11833 return Err(syn::Error::new_spanned(
11834 ty,
11835 format!("unsupported field type `{other}`; supports i16/i32/i64/f32/f64/bool/String/DateTime/NaiveDate/NaiveTime/Uuid/serde_json::Value/Decimal/Vec<u8>, optionally wrapped in Option or Auto (Auto only on integers/Uuid/DateTime)"),
11836 ));
11837 }
11838 };
11839 Ok(DetectedType {
11840 kind,
11841 nullable: false,
11842 auto: false,
11843 fk_inner: None,
11844 })
11845}
11846
11847fn generic_inner<'a>(
11848 ty: &'a Type,
11849 arguments: &'a PathArguments,
11850 wrapper: &str,
11851) -> syn::Result<&'a Type> {
11852 let PathArguments::AngleBracketed(args) = arguments else {
11853 return Err(syn::Error::new_spanned(
11854 ty,
11855 format!("{wrapper} requires a generic argument"),
11856 ));
11857 };
11858 args.args
11859 .iter()
11860 .find_map(|a| match a {
11861 GenericArgument::Type(t) => Some(t),
11862 _ => None,
11863 })
11864 .ok_or_else(|| {
11865 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11866 })
11867}
11868
11869/// Like [`generic_inner`] but pulls *two* type args — the first is
11870/// required, the second is optional. Used by the `ForeignKey<T, K>`
11871/// detection where K defaults to `i64` when omitted.
11872fn generic_pair<'a>(
11873 ty: &'a Type,
11874 arguments: &'a PathArguments,
11875 wrapper: &str,
11876) -> syn::Result<(&'a Type, Option<&'a Type>)> {
11877 let PathArguments::AngleBracketed(args) = arguments else {
11878 return Err(syn::Error::new_spanned(
11879 ty,
11880 format!("{wrapper} requires a generic argument"),
11881 ));
11882 };
11883 let mut types = args.args.iter().filter_map(|a| match a {
11884 GenericArgument::Type(t) => Some(t),
11885 _ => None,
11886 });
11887 let first = types.next().ok_or_else(|| {
11888 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11889 })?;
11890 let second = types.next();
11891 Ok((first, second))
11892}
11893
11894fn to_snake_case(s: &str) -> String {
11895 let mut out = String::with_capacity(s.len() + 4);
11896 for (i, ch) in s.chars().enumerate() {
11897 if ch.is_ascii_uppercase() {
11898 if i > 0 {
11899 out.push('_');
11900 }
11901 out.push(ch.to_ascii_lowercase());
11902 } else {
11903 out.push(ch);
11904 }
11905 }
11906 out
11907}
11908
11909// ============================================================
11910// #[derive(Form)] — slice 8.4B
11911// ============================================================
11912
11913/// Per-field `#[form(...)]` attributes recognised by the derive.
11914#[derive(Default)]
11915struct FormFieldAttrs {
11916 min: Option<i64>,
11917 max: Option<i64>,
11918 min_length: Option<u32>,
11919 max_length: Option<u32>,
11920 /// `#[form(clean = "fn_name")]` — per-field clean hook.
11921 /// The named static method on the form struct is called after the
11922 /// field's typed parse + length/range checks; it gets the parsed
11923 /// value by reference and returns `Result<<FieldType>, String>`.
11924 /// On Ok, the returned value replaces the parsed one; on Err, the
11925 /// message is attached to the field error list. Issue #372.
11926 clean: Option<syn::Ident>,
11927}
11928
11929/// Container-level `#[form(...)]` attributes. Currently only the
11930/// cross-field `validate` hook (issue #373).
11931#[derive(Default)]
11932struct FormContainerAttrs {
11933 /// `#[form(validate = "fn_name")]` — whole-form clean hook.
11934 /// After every per-field parse succeeds, the named method on the
11935 /// form struct is called with `&self` and may return
11936 /// `Result<(), FormErrors>`. Errors merge into the field error
11937 /// list. Issue #373.
11938 validate: Option<syn::Ident>,
11939}
11940
11941/// Detected shape of a form field's Rust type.
11942#[derive(Clone, Copy)]
11943enum FormFieldKind {
11944 String,
11945 I16,
11946 I32,
11947 I64,
11948 F32,
11949 F64,
11950 Bool,
11951}
11952
11953impl FormFieldKind {
11954 fn parse_method(self) -> &'static str {
11955 match self {
11956 Self::I16 => "i16",
11957 Self::I32 => "i32",
11958 Self::I64 => "i64",
11959 Self::F32 => "f32",
11960 Self::F64 => "f64",
11961 // String + Bool don't go through `str::parse`; the codegen
11962 // handles them inline.
11963 Self::String | Self::Bool => "",
11964 }
11965 }
11966}
11967
11968fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
11969 let root = rustango_root();
11970 let struct_name = &input.ident;
11971
11972 let Data::Struct(data) = &input.data else {
11973 return Err(syn::Error::new_spanned(
11974 struct_name,
11975 "Form can only be derived on structs",
11976 ));
11977 };
11978 let Fields::Named(named) = &data.fields else {
11979 return Err(syn::Error::new_spanned(
11980 struct_name,
11981 "Form requires a struct with named fields",
11982 ));
11983 };
11984
11985 // #373 — container-level `#[form(validate = "fn")]` hook.
11986 let container = parse_form_container_attrs(input)?;
11987 let post_field_clean: Vec<TokenStream2> = Vec::new();
11988 let _ = post_field_clean;
11989
11990 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
11991 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
11992
11993 for field in &named.named {
11994 let ident = field
11995 .ident
11996 .as_ref()
11997 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11998 let attrs = parse_form_field_attrs(field)?;
11999 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12000
12001 let name_lit = ident.to_string();
12002 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12003 // #372 — append the per-field `clean_<field>` call right after
12004 // the parse block when the attribute is set. The clean fn
12005 // takes &T and returns Result<T, String>; on Err we attach
12006 // the message to the field error list without aborting, so
12007 // the caller sees every field error at once.
12008 let clean_block = if let Some(clean_fn) = &attrs.clean {
12009 quote! {
12010 if __errors.fields().get(#name_lit).is_none() {
12011 match Self::#clean_fn(&#ident) {
12012 ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
12013 ::core::result::Result::Err(__msg) => {
12014 __errors.add(#name_lit, __msg);
12015 }
12016 }
12017 }
12018 }
12019 } else {
12020 quote! {}
12021 };
12022 field_blocks.push(quote! {
12023 #parse_block
12024 #clean_block
12025 });
12026 field_idents.push(ident);
12027 }
12028
12029 // #373 — after every per-field parse + clean succeeds, call the
12030 // cross-field validator if declared. Errors merge into the
12031 // outgoing FormErrors via the existing `FormErrors::merge` helper
12032 // (same primitive the serializer cross-field hook uses).
12033 let cross_field_call = if let Some(validate_fn) = &container.validate {
12034 quote! {
12035 if __errors.is_empty() {
12036 let __candidate = Self { #( #field_idents ),* };
12037 if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12038 __errors.merge(__other);
12039 }
12040 if !__errors.is_empty() {
12041 return ::core::result::Result::Err(__errors);
12042 }
12043 return ::core::result::Result::Ok(__candidate);
12044 }
12045 }
12046 } else {
12047 quote! {}
12048 };
12049
12050 Ok(quote! {
12051 impl #root::forms::Form for #struct_name {
12052 fn parse(
12053 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12054 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12055 let mut __errors = #root::forms::FormErrors::default();
12056 #( #field_blocks )*
12057 #cross_field_call
12058 if !__errors.is_empty() {
12059 return ::core::result::Result::Err(__errors);
12060 }
12061 ::core::result::Result::Ok(Self {
12062 #( #field_idents ),*
12063 })
12064 }
12065 }
12066 })
12067}
12068
12069fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12070 let mut out = FormContainerAttrs::default();
12071 for attr in &input.attrs {
12072 if !attr.path().is_ident("form") {
12073 continue;
12074 }
12075 attr.parse_nested_meta(|meta| {
12076 if meta.path.is_ident("validate") {
12077 let s: LitStr = meta.value()?.parse()?;
12078 out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12079 return Ok(());
12080 }
12081 Err(meta.error("unknown form container attribute (supported: `validate`)"))
12082 })?;
12083 }
12084 Ok(out)
12085}
12086
12087fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12088 let mut out = FormFieldAttrs::default();
12089 for attr in &field.attrs {
12090 if !attr.path().is_ident("form") {
12091 continue;
12092 }
12093 attr.parse_nested_meta(|meta| {
12094 if meta.path.is_ident("min") {
12095 let lit: syn::LitInt = meta.value()?.parse()?;
12096 out.min = Some(lit.base10_parse::<i64>()?);
12097 return Ok(());
12098 }
12099 if meta.path.is_ident("max") {
12100 let lit: syn::LitInt = meta.value()?.parse()?;
12101 out.max = Some(lit.base10_parse::<i64>()?);
12102 return Ok(());
12103 }
12104 if meta.path.is_ident("min_length") {
12105 let lit: syn::LitInt = meta.value()?.parse()?;
12106 out.min_length = Some(lit.base10_parse::<u32>()?);
12107 return Ok(());
12108 }
12109 if meta.path.is_ident("max_length") {
12110 let lit: syn::LitInt = meta.value()?.parse()?;
12111 out.max_length = Some(lit.base10_parse::<u32>()?);
12112 return Ok(());
12113 }
12114 if meta.path.is_ident("clean") {
12115 let s: LitStr = meta.value()?.parse()?;
12116 out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12117 return Ok(());
12118 }
12119 Err(meta.error(
12120 "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12121 ))
12122 })?;
12123 }
12124 Ok(out)
12125}
12126
12127fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12128 let Type::Path(TypePath { path, qself: None }) = ty else {
12129 return Err(syn::Error::new(
12130 span,
12131 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12132 ));
12133 };
12134 let last = path
12135 .segments
12136 .last()
12137 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12138
12139 if last.ident == "Option" {
12140 let inner = generic_inner(ty, &last.arguments, "Option")?;
12141 let (kind, nested) = detect_form_field(inner, span)?;
12142 if nested {
12143 return Err(syn::Error::new(
12144 span,
12145 "nested Option in Form fields is not supported",
12146 ));
12147 }
12148 return Ok((kind, true));
12149 }
12150
12151 let kind = match last.ident.to_string().as_str() {
12152 "String" => FormFieldKind::String,
12153 "i16" => FormFieldKind::I16,
12154 "i32" => FormFieldKind::I32,
12155 "i64" => FormFieldKind::I64,
12156 "f32" => FormFieldKind::F32,
12157 "f64" => FormFieldKind::F64,
12158 "bool" => FormFieldKind::Bool,
12159 other => {
12160 return Err(syn::Error::new(
12161 span,
12162 format!(
12163 "Form field type `{other}` is not supported in v0.8 — use String / \
12164 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12165 ),
12166 ));
12167 }
12168 };
12169 Ok((kind, false))
12170}
12171
12172#[allow(clippy::too_many_lines)]
12173fn render_form_field_parse(
12174 ident: &syn::Ident,
12175 name_lit: &str,
12176 kind: FormFieldKind,
12177 nullable: bool,
12178 attrs: &FormFieldAttrs,
12179) -> TokenStream2 {
12180 // Pull the raw &str from the payload. Uses variable name `data` to
12181 // match the new `Form::parse(data: &HashMap<…>)` signature.
12182 let lookup = quote! {
12183 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12184 };
12185
12186 let parsed_value = match kind {
12187 FormFieldKind::Bool => quote! {
12188 let __v: bool = match __raw {
12189 ::core::option::Option::None => false,
12190 ::core::option::Option::Some(__s) => !matches!(
12191 __s.to_ascii_lowercase().as_str(),
12192 "" | "false" | "0" | "off" | "no"
12193 ),
12194 };
12195 },
12196 FormFieldKind::String => {
12197 if nullable {
12198 quote! {
12199 let __v: ::core::option::Option<::std::string::String> = match __raw {
12200 ::core::option::Option::None => ::core::option::Option::None,
12201 ::core::option::Option::Some(__s) if __s.is_empty() => {
12202 ::core::option::Option::None
12203 }
12204 ::core::option::Option::Some(__s) => {
12205 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12206 }
12207 };
12208 }
12209 } else {
12210 quote! {
12211 let __v: ::std::string::String = match __raw {
12212 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12213 ::core::clone::Clone::clone(__s)
12214 }
12215 _ => {
12216 __errors.add(#name_lit, "This field is required.");
12217 ::std::string::String::new()
12218 }
12219 };
12220 }
12221 }
12222 }
12223 FormFieldKind::I16
12224 | FormFieldKind::I32
12225 | FormFieldKind::I64
12226 | FormFieldKind::F32
12227 | FormFieldKind::F64 => {
12228 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12229 let ty_lit = kind.parse_method();
12230 let default_val = match kind {
12231 FormFieldKind::I16 => quote! { 0i16 },
12232 FormFieldKind::I32 => quote! { 0i32 },
12233 FormFieldKind::I64 => quote! { 0i64 },
12234 FormFieldKind::F32 => quote! { 0f32 },
12235 FormFieldKind::F64 => quote! { 0f64 },
12236 _ => quote! { Default::default() },
12237 };
12238 if nullable {
12239 quote! {
12240 let __v: ::core::option::Option<#parse_ty> = match __raw {
12241 ::core::option::Option::None => ::core::option::Option::None,
12242 ::core::option::Option::Some(__s) if __s.is_empty() => {
12243 ::core::option::Option::None
12244 }
12245 ::core::option::Option::Some(__s) => {
12246 match __s.parse::<#parse_ty>() {
12247 ::core::result::Result::Ok(__n) => {
12248 ::core::option::Option::Some(__n)
12249 }
12250 ::core::result::Result::Err(__e) => {
12251 __errors.add(
12252 #name_lit,
12253 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12254 );
12255 ::core::option::Option::None
12256 }
12257 }
12258 }
12259 };
12260 }
12261 } else {
12262 quote! {
12263 let __v: #parse_ty = match __raw {
12264 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12265 match __s.parse::<#parse_ty>() {
12266 ::core::result::Result::Ok(__n) => __n,
12267 ::core::result::Result::Err(__e) => {
12268 __errors.add(
12269 #name_lit,
12270 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12271 );
12272 #default_val
12273 }
12274 }
12275 }
12276 _ => {
12277 __errors.add(#name_lit, "This field is required.");
12278 #default_val
12279 }
12280 };
12281 }
12282 }
12283 }
12284 };
12285
12286 let validators = render_form_validators(name_lit, kind, nullable, attrs);
12287
12288 quote! {
12289 // `mut` so the per-field `clean` hook (#372) can rewrite the
12290 // parsed value in-place when it returns Ok with a normalized
12291 // form (e.g. trim / lowercase).
12292 let mut #ident = {
12293 #lookup
12294 #parsed_value
12295 #validators
12296 __v
12297 };
12298 }
12299}
12300
12301fn render_form_validators(
12302 name_lit: &str,
12303 kind: FormFieldKind,
12304 nullable: bool,
12305 attrs: &FormFieldAttrs,
12306) -> TokenStream2 {
12307 let mut checks: Vec<TokenStream2> = Vec::new();
12308
12309 let val_ref = if nullable {
12310 quote! { __v.as_ref() }
12311 } else {
12312 quote! { ::core::option::Option::Some(&__v) }
12313 };
12314
12315 let is_string = matches!(kind, FormFieldKind::String);
12316 let is_numeric = matches!(
12317 kind,
12318 FormFieldKind::I16
12319 | FormFieldKind::I32
12320 | FormFieldKind::I64
12321 | FormFieldKind::F32
12322 | FormFieldKind::F64
12323 );
12324
12325 if is_string {
12326 if let Some(min_len) = attrs.min_length {
12327 let min_len_usize = min_len as usize;
12328 checks.push(quote! {
12329 if let ::core::option::Option::Some(__s) = #val_ref {
12330 if __s.len() < #min_len_usize {
12331 __errors.add(
12332 #name_lit,
12333 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12334 );
12335 }
12336 }
12337 });
12338 }
12339 if let Some(max_len) = attrs.max_length {
12340 let max_len_usize = max_len as usize;
12341 checks.push(quote! {
12342 if let ::core::option::Option::Some(__s) = #val_ref {
12343 if __s.len() > #max_len_usize {
12344 __errors.add(
12345 #name_lit,
12346 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12347 );
12348 }
12349 }
12350 });
12351 }
12352 }
12353
12354 if is_numeric {
12355 if let Some(min) = attrs.min {
12356 checks.push(quote! {
12357 if let ::core::option::Option::Some(__n) = #val_ref {
12358 if (*__n as f64) < (#min as f64) {
12359 __errors.add(
12360 #name_lit,
12361 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12362 );
12363 }
12364 }
12365 });
12366 }
12367 if let Some(max) = attrs.max {
12368 checks.push(quote! {
12369 if let ::core::option::Option::Some(__n) = #val_ref {
12370 if (*__n as f64) > (#max as f64) {
12371 __errors.add(
12372 #name_lit,
12373 ::std::format!("Ensure this value is less than or equal to {}.", #max),
12374 );
12375 }
12376 }
12377 });
12378 }
12379 }
12380
12381 quote! { #( #checks )* }
12382}
12383
12384// ============================================================
12385// #[derive(ViewSet)]
12386// ============================================================
12387
12388struct ViewSetAttrs {
12389 model: syn::Path,
12390 fields: Option<Vec<String>>,
12391 filter_fields: Vec<String>,
12392 search_fields: Vec<String>,
12393 /// (field_name, desc)
12394 ordering: Vec<(String, bool)>,
12395 page_size: Option<usize>,
12396 read_only: bool,
12397 perms: ViewSetPermsAttrs,
12398 /// `#[viewset(serializer = SomeSerializer)]` — render list /
12399 /// retrieve / create responses through this `#[derive(Serializer)]`
12400 /// type instead of the default field-level projection (requires the
12401 /// `serializer` feature). Tri-dialect.
12402 serializer: Option<syn::Path>,
12403}
12404
12405#[derive(Default)]
12406struct ViewSetPermsAttrs {
12407 list: Vec<String>,
12408 retrieve: Vec<String>,
12409 create: Vec<String>,
12410 update: Vec<String>,
12411 destroy: Vec<String>,
12412}
12413
12414fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12415 let root = rustango_root();
12416 let struct_name = &input.ident;
12417
12418 // Must be a unit struct or an empty named struct.
12419 match &input.data {
12420 Data::Struct(s) => match &s.fields {
12421 Fields::Unit | Fields::Named(_) => {}
12422 Fields::Unnamed(_) => {
12423 return Err(syn::Error::new_spanned(
12424 struct_name,
12425 "ViewSet can only be derived on a unit struct or an empty named struct",
12426 ));
12427 }
12428 },
12429 _ => {
12430 return Err(syn::Error::new_spanned(
12431 struct_name,
12432 "ViewSet can only be derived on a struct",
12433 ));
12434 }
12435 }
12436
12437 let attrs = parse_viewset_attrs(input)?;
12438 let model_path = &attrs.model;
12439
12440 // `.fields(&[...])` call — None means skip (use all scalar fields).
12441 let fields_call = if let Some(ref fields) = attrs.fields {
12442 let lits = fields.iter().map(|f| f.as_str());
12443 quote!(.fields(&[ #(#lits),* ]))
12444 } else {
12445 quote!()
12446 };
12447
12448 let filter_fields_call = if attrs.filter_fields.is_empty() {
12449 quote!()
12450 } else {
12451 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12452 quote!(.filter_fields(&[ #(#lits),* ]))
12453 };
12454
12455 let search_fields_call = if attrs.search_fields.is_empty() {
12456 quote!()
12457 } else {
12458 let lits = attrs.search_fields.iter().map(|f| f.as_str());
12459 quote!(.search_fields(&[ #(#lits),* ]))
12460 };
12461
12462 let ordering_call = if attrs.ordering.is_empty() {
12463 quote!()
12464 } else {
12465 let pairs = attrs.ordering.iter().map(|(f, desc)| {
12466 let f = f.as_str();
12467 quote!((#f, #desc))
12468 });
12469 quote!(.ordering(&[ #(#pairs),* ]))
12470 };
12471
12472 let page_size_call = if let Some(n) = attrs.page_size {
12473 quote!(.page_size(#n))
12474 } else {
12475 quote!()
12476 };
12477
12478 let read_only_call = if attrs.read_only {
12479 quote!(.read_only())
12480 } else {
12481 quote!()
12482 };
12483
12484 // `.serializer::<S>()` — reshape responses through a derived
12485 // serializer. Requires the downstream crate to enable the
12486 // `serializer` feature (the method is gated on it).
12487 let serializer_call = if let Some(ref ser) = attrs.serializer {
12488 quote!(.serializer::<#ser>())
12489 } else {
12490 quote!()
12491 };
12492
12493 let perms = &attrs.perms;
12494 let perms_call = if perms.list.is_empty()
12495 && perms.retrieve.is_empty()
12496 && perms.create.is_empty()
12497 && perms.update.is_empty()
12498 && perms.destroy.is_empty()
12499 {
12500 quote!()
12501 } else {
12502 let list_lits = perms.list.iter().map(|s| s.as_str());
12503 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12504 let create_lits = perms.create.iter().map(|s| s.as_str());
12505 let update_lits = perms.update.iter().map(|s| s.as_str());
12506 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12507 quote! {
12508 .permissions(#root::viewset::ViewSetPerms {
12509 list: ::std::vec![ #(#list_lits.to_owned()),* ],
12510 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12511 create: ::std::vec![ #(#create_lits.to_owned()),* ],
12512 update: ::std::vec![ #(#update_lits.to_owned()),* ],
12513 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
12514 })
12515 }
12516 };
12517
12518 Ok(quote! {
12519 impl #struct_name {
12520 /// Build an `axum::Router` with the six standard REST endpoints
12521 /// for this ViewSet, mounted at `prefix`.
12522 ///
12523 /// Accepts any backend's pool — a `sqlx::PgPool`,
12524 /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
12525 /// enum — because they all `Into<Pool>` (#1273). This is why
12526 /// the derive no longer names `PgPool`, which would not
12527 /// exist on a project built without the `postgres` feature.
12528 pub fn router(
12529 prefix: &str,
12530 pool: impl ::core::convert::Into<#root::sql::Pool>,
12531 ) -> #root::__axum::Router {
12532 #root::viewset::ViewSet::for_model(
12533 <#model_path as #root::core::Model>::SCHEMA
12534 )
12535 #fields_call
12536 #filter_fields_call
12537 #search_fields_call
12538 #ordering_call
12539 #page_size_call
12540 #perms_call
12541 #read_only_call
12542 #serializer_call
12543 .router_pool(prefix, pool.into())
12544 }
12545 }
12546 })
12547}
12548
12549fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
12550 let mut model: Option<syn::Path> = None;
12551 let mut fields: Option<Vec<String>> = None;
12552 let mut filter_fields: Vec<String> = Vec::new();
12553 let mut search_fields: Vec<String> = Vec::new();
12554 let mut ordering: Vec<(String, bool)> = Vec::new();
12555 let mut page_size: Option<usize> = None;
12556 let mut read_only = false;
12557 let mut perms = ViewSetPermsAttrs::default();
12558 let mut serializer: Option<syn::Path> = None;
12559
12560 for attr in &input.attrs {
12561 if !attr.path().is_ident("viewset") {
12562 continue;
12563 }
12564 attr.parse_nested_meta(|meta| {
12565 if meta.path.is_ident("model") {
12566 let path: syn::Path = meta.value()?.parse()?;
12567 model = Some(path);
12568 return Ok(());
12569 }
12570 if meta.path.is_ident("serializer") {
12571 let path: syn::Path = meta.value()?.parse()?;
12572 serializer = Some(path);
12573 return Ok(());
12574 }
12575 if meta.path.is_ident("fields") {
12576 let s: LitStr = meta.value()?.parse()?;
12577 fields = Some(split_field_list(&s.value()));
12578 return Ok(());
12579 }
12580 if meta.path.is_ident("filter_fields") {
12581 let s: LitStr = meta.value()?.parse()?;
12582 filter_fields = split_field_list(&s.value());
12583 return Ok(());
12584 }
12585 if meta.path.is_ident("search_fields") {
12586 let s: LitStr = meta.value()?.parse()?;
12587 search_fields = split_field_list(&s.value());
12588 return Ok(());
12589 }
12590 if meta.path.is_ident("ordering") {
12591 let s: LitStr = meta.value()?.parse()?;
12592 ordering = parse_ordering_list(&s.value());
12593 return Ok(());
12594 }
12595 if meta.path.is_ident("page_size") {
12596 let lit: syn::LitInt = meta.value()?.parse()?;
12597 page_size = Some(lit.base10_parse::<usize>()?);
12598 return Ok(());
12599 }
12600 if meta.path.is_ident("read_only") {
12601 read_only = true;
12602 return Ok(());
12603 }
12604 if meta.path.is_ident("permissions") {
12605 meta.parse_nested_meta(|inner| {
12606 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
12607 let s: LitStr = inner.value()?.parse()?;
12608 Ok(split_field_list(&s.value()))
12609 };
12610 if inner.path.is_ident("list") {
12611 perms.list = parse_codenames(&inner)?;
12612 } else if inner.path.is_ident("retrieve") {
12613 perms.retrieve = parse_codenames(&inner)?;
12614 } else if inner.path.is_ident("create") {
12615 perms.create = parse_codenames(&inner)?;
12616 } else if inner.path.is_ident("update") {
12617 perms.update = parse_codenames(&inner)?;
12618 } else if inner.path.is_ident("destroy") {
12619 perms.destroy = parse_codenames(&inner)?;
12620 } else {
12621 return Err(inner.error(
12622 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
12623 ));
12624 }
12625 Ok(())
12626 })?;
12627 return Ok(());
12628 }
12629 Err(meta.error(
12630 "unknown viewset attribute (supported: model, fields, filter_fields, \
12631 search_fields, ordering, page_size, read_only, serializer, permissions(...))",
12632 ))
12633 })?;
12634 }
12635
12636 let model = model.ok_or_else(|| {
12637 syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
12638 })?;
12639
12640 Ok(ViewSetAttrs {
12641 model,
12642 fields,
12643 filter_fields,
12644 search_fields,
12645 ordering,
12646 page_size,
12647 read_only,
12648 perms,
12649 serializer,
12650 })
12651}
12652
12653// ============================================================ #[derive(Serializer)]
12654
12655struct SerializerContainerAttrs {
12656 model: syn::Path,
12657 /// `#[serializer(validate = "fn_name")]` on the struct — the
12658 /// cross-field validation hook (#436). The named inherent method
12659 /// must take `&self` and return
12660 /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
12661 /// `validate()` runs every per-field validator first; then calls
12662 /// the cross-field method if declared; aggregates all errors into
12663 /// one `FormErrors`.
12664 cross_validate: Option<syn::Ident>,
12665}
12666
12667#[derive(Default)]
12668struct SerializerFieldAttrs {
12669 read_only: bool,
12670 write_only: bool,
12671 source: Option<String>,
12672 skip: bool,
12673 /// `#[serializer(method = "fn_name")]` — computed field. The macro
12674 /// emits a `from_model` initializer that calls
12675 /// `Self::fn_name(&model)` and stores the return value.
12676 method: Option<String>,
12677 /// `#[serializer(validate = "fn_name")]` — per-field validator
12678 /// callable run by `Self::validate(&self)`. Must return
12679 /// `Result<(), String>`. Errors land in `FormErrors` keyed by
12680 /// the field name.
12681 validate: Option<String>,
12682 /// `#[serializer(nested)]` on a field whose type is another
12683 /// `Serializer` — the macro emits `from_model` initializer that
12684 /// reads the parent via `model.<source>.value()` then calls the
12685 /// child serializer's `from_model(parent)`. When the FK is
12686 /// unloaded the field falls back to `Default::default()` (does
12687 /// NOT panic) so a missing prefetch in prod degrades gracefully.
12688 /// Source field on the model defaults to the field name; override
12689 /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
12690 /// panic-on-unloaded behavior for tests.
12691 nested: bool,
12692 /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
12693 /// strict behavior: panic when the FK isn't loaded. Useful in
12694 /// test code where forgetting select_related must trip a hard
12695 /// failure rather than render a blank nested object.
12696 nested_strict: bool,
12697 /// `#[serializer(many = TagSerializer)]` — declare the field as
12698 /// a list of nested serializers. Field type must be `Vec<S>`
12699 /// where `S` is the inner serializer. The macro initializes the
12700 /// field to `Vec::new()` in `from_model` and emits a typed
12701 /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
12702 /// maps each model row through `S::from_model`. Auto-load isn't
12703 /// possible (the M2M / one-to-many accessor is async); callers
12704 /// fetch the children + call the setter post-from_model.
12705 many: Option<syn::Type>,
12706 /// `#[serializer(slug = "name")]` — render a relation as one of
12707 /// its fields.
12708 /// Source field on the model must be a `ForeignKey<T>`; the
12709 /// macro emits `from_model` glue that walks
12710 /// `model.<source>.value()?.<slug>` and clones it. Field type on
12711 /// the serializer is typically `String` (whatever type the slug
12712 /// column has). When the FK is unloaded the field falls back to
12713 /// `Default::default()`, same graceful-degrade contract as
12714 /// `nested`. Source defaults to the field name; override with
12715 /// `source = "..."`. v0.44.
12716 slug: Option<String>,
12717 /// `#[serializer(max_length = N)]` — caps
12718 /// the character count of a string field on write. Overrides the
12719 /// model's `max_length`; when absent the model value is inherited.
12720 max_length: Option<u64>,
12721 /// `#[serializer(min_length = N)]` — minimum character count.
12722 /// Serializer-only (the model has no `min_length` column).
12723 min_length: Option<u64>,
12724 /// `#[serializer(min = N)]` — inclusive
12725 /// integer lower bound; overrides the model's `min` when given.
12726 min: Option<i64>,
12727 /// `#[serializer(max = N)]` — inclusive
12728 /// integer upper bound; overrides the model's `max` when given.
12729 max: Option<i64>,
12730}
12731
12732fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
12733 let mut model: Option<syn::Path> = None;
12734 let mut cross_validate: Option<syn::Ident> = None;
12735 for attr in &input.attrs {
12736 if !attr.path().is_ident("serializer") {
12737 continue;
12738 }
12739 attr.parse_nested_meta(|meta| {
12740 if meta.path.is_ident("model") {
12741 let _eq: syn::Token![=] = meta.input.parse()?;
12742 model = Some(meta.input.parse()?);
12743 return Ok(());
12744 }
12745 if meta.path.is_ident("validate") {
12746 // #436 — container-level `validate = "fn_name"` for
12747 // cross-field validation. Field-level
12748 // `#[serializer(validate = "...")]` on a field is
12749 // parsed separately in `parse_serializer_field_attrs`.
12750 let s: LitStr = meta.value()?.parse()?;
12751 cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
12752 return Ok(());
12753 }
12754 Err(meta.error(
12755 "unknown serializer container attribute \
12756 (supported: `model`, `validate`)",
12757 ))
12758 })?;
12759 }
12760 let model = model.ok_or_else(|| {
12761 syn::Error::new_spanned(
12762 &input.ident,
12763 "`#[serializer(model = SomeModel)]` is required",
12764 )
12765 })?;
12766 Ok(SerializerContainerAttrs {
12767 model,
12768 cross_validate,
12769 })
12770}
12771
12772fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
12773 let mut out = SerializerFieldAttrs::default();
12774 for attr in &field.attrs {
12775 if !attr.path().is_ident("serializer") {
12776 continue;
12777 }
12778 attr.parse_nested_meta(|meta| {
12779 if meta.path.is_ident("read_only") {
12780 out.read_only = true;
12781 return Ok(());
12782 }
12783 if meta.path.is_ident("write_only") {
12784 out.write_only = true;
12785 return Ok(());
12786 }
12787 if meta.path.is_ident("skip") {
12788 out.skip = true;
12789 return Ok(());
12790 }
12791 if meta.path.is_ident("source") {
12792 let s: LitStr = meta.value()?.parse()?;
12793 out.source = Some(s.value());
12794 return Ok(());
12795 }
12796 if meta.path.is_ident("method") {
12797 let s: LitStr = meta.value()?.parse()?;
12798 out.method = Some(s.value());
12799 return Ok(());
12800 }
12801 if meta.path.is_ident("validate") {
12802 let s: LitStr = meta.value()?.parse()?;
12803 out.validate = Some(s.value());
12804 return Ok(());
12805 }
12806 if meta.path.is_ident("many") {
12807 let _eq: syn::Token![=] = meta.input.parse()?;
12808 out.many = Some(meta.input.parse()?);
12809 return Ok(());
12810 }
12811 if meta.path.is_ident("nested") {
12812 out.nested = true;
12813 // Optional strict flag inside parentheses:
12814 // #[serializer(nested(strict))]
12815 if meta.input.peek(syn::token::Paren) {
12816 meta.parse_nested_meta(|inner| {
12817 if inner.path.is_ident("strict") {
12818 out.nested_strict = true;
12819 return Ok(());
12820 }
12821 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
12822 })?;
12823 }
12824 return Ok(());
12825 }
12826 if meta.path.is_ident("slug") {
12827 let s: LitStr = meta.value()?.parse()?;
12828 out.slug = Some(s.value());
12829 return Ok(());
12830 }
12831 if meta.path.is_ident("max_length") {
12832 let lit: syn::LitInt = meta.value()?.parse()?;
12833 out.max_length = Some(lit.base10_parse::<u64>()?);
12834 return Ok(());
12835 }
12836 if meta.path.is_ident("min_length") {
12837 let lit: syn::LitInt = meta.value()?.parse()?;
12838 out.min_length = Some(lit.base10_parse::<u64>()?);
12839 return Ok(());
12840 }
12841 if meta.path.is_ident("min") {
12842 let lit: syn::LitInt = meta.value()?.parse()?;
12843 out.min = Some(lit.base10_parse::<i64>()?);
12844 return Ok(());
12845 }
12846 if meta.path.is_ident("max") {
12847 let lit: syn::LitInt = meta.value()?.parse()?;
12848 out.max = Some(lit.base10_parse::<i64>()?);
12849 return Ok(());
12850 }
12851 Err(meta.error(
12852 "unknown serializer field attribute (supported: \
12853 `read_only`, `write_only`, `source`, `skip`, `method`, \
12854 `validate`, `nested`, `many`, `slug`, `max_length`, \
12855 `min_length`, `min`, `max`)",
12856 ))
12857 })?;
12858 }
12859 // Validate: read_only + write_only is nonsensical
12860 if out.read_only && out.write_only {
12861 return Err(syn::Error::new_spanned(
12862 field,
12863 "a field cannot be both `read_only` and `write_only`",
12864 ));
12865 }
12866 if out.method.is_some() && out.source.is_some() {
12867 return Err(syn::Error::new_spanned(
12868 field,
12869 "`method` and `source` are mutually exclusive — `method` computes \
12870 the value from a method, `source` reads it from a different model field",
12871 ));
12872 }
12873 if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
12874 return Err(syn::Error::new_spanned(
12875 field,
12876 "`slug` is mutually exclusive with `method`, `nested`, and `many` \
12877 — pick one strategy for populating the field",
12878 ));
12879 }
12880 Ok(out)
12881}
12882
12883fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
12884 let root = rustango_root();
12885 let struct_name = &input.ident;
12886 let struct_name_lit = struct_name.to_string();
12887
12888 let Data::Struct(data) = &input.data else {
12889 return Err(syn::Error::new_spanned(
12890 struct_name,
12891 "Serializer can only be derived on structs",
12892 ));
12893 };
12894 let Fields::Named(named) = &data.fields else {
12895 return Err(syn::Error::new_spanned(
12896 struct_name,
12897 "Serializer requires a struct with named fields",
12898 ));
12899 };
12900
12901 let container = parse_serializer_container_attrs(input)?;
12902 let model_path = &container.model;
12903
12904 // Classify each field. `ty` is only consumed by the
12905 // `#[cfg(feature = "openapi")]` block below, but we always
12906 // capture it to keep the field-info build a single pass.
12907 #[allow(dead_code)]
12908 struct FieldInfo {
12909 ident: syn::Ident,
12910 ty: syn::Type,
12911 attrs: SerializerFieldAttrs,
12912 }
12913 let mut fields_info: Vec<FieldInfo> = Vec::new();
12914 for field in &named.named {
12915 let ident = field.ident.clone().expect("named field has ident");
12916 let attrs = parse_serializer_field_attrs(field)?;
12917 fields_info.push(FieldInfo {
12918 ident,
12919 ty: field.ty.clone(),
12920 attrs,
12921 });
12922 }
12923
12924 // Generate from_model body: struct literal with each field assigned.
12925 let from_model_fields = fields_info.iter().map(|fi| {
12926 let ident = &fi.ident;
12927 let ty = &fi.ty;
12928 if let Some(_inner) = &fi.attrs.many {
12929 // Many — collection field. Initialize empty; caller
12930 // populates via the macro-emitted set_<field> helper
12931 // after fetching the M2M children.
12932 quote! { #ident: ::std::vec::Vec::new() }
12933 } else if let Some(method) = &fi.attrs.method {
12934 // SerializerMethodField: call Self::<method>(&model) to
12935 // compute the value. Method signature must be
12936 // `fn <method>(model: &T) -> <field type>`.
12937 let method_ident = syn::Ident::new(method, ident.span());
12938 quote! { #ident: Self::#method_ident(model) }
12939 } else if let Some(slug_field) = &fi.attrs.slug {
12940 // v0.44 — SlugRelatedField. Source defaults to the field
12941 // name on this struct; override via `source = "..."`. The
12942 // source field on the model is expected to be a
12943 // `ForeignKey<T>`; the slug field on the parent is named
12944 // by the attribute value. When the FK is unloaded the
12945 // field falls back to `Default::default()` — same
12946 // graceful-degrade contract as `nested`.
12947 let src_name = fi
12948 .attrs
12949 .source
12950 .as_deref()
12951 .unwrap_or(&fi.ident.to_string())
12952 .to_owned();
12953 let src_ident = syn::Ident::new(&src_name, ident.span());
12954 let slug_ident = syn::Ident::new(slug_field, ident.span());
12955 quote! {
12956 #ident: match model.#src_ident.value() {
12957 ::core::option::Option::Some(__loaded) =>
12958 ::core::clone::Clone::clone(&__loaded.#slug_ident),
12959 ::core::option::Option::None =>
12960 ::core::default::Default::default(),
12961 }
12962 }
12963 } else if fi.attrs.nested {
12964 // Nested serializer. Source defaults to the field name on
12965 // this struct; override via `source = "..."`. The source
12966 // field on the model is expected to be a `ForeignKey<T>`
12967 // whose `.value()` returns `Option<&T>` after lazy-load.
12968 //
12969 // Behavior matrix (tweakable per-field):
12970 // * FK loaded → nested object materializes via
12971 // ChildSerializer::from_model(parent).
12972 // * FK unloaded → fall back to ChildSerializer::default()
12973 // (so prod doesn't crash on a missing
12974 // prefetch — just renders a blank nested
12975 // object). Add `#[serializer(nested,
12976 // strict)]` to keep the v0.18.1
12977 // panic-on-unloaded behavior for tests
12978 // that want hard guardrails.
12979 let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
12980 let src_ident = syn::Ident::new(&src_name, ident.span());
12981 if fi.attrs.nested_strict {
12982 let panic_msg = format!(
12983 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
12984 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
12985 );
12986 quote! {
12987 #ident: <#ty as #root::serializer::ModelSerializer>::from_model(
12988 model.#src_ident.value().expect(#panic_msg),
12989 )
12990 }
12991 } else {
12992 quote! {
12993 #ident: match model.#src_ident.value() {
12994 ::core::option::Option::Some(__loaded) =>
12995 <#ty as #root::serializer::ModelSerializer>::from_model(__loaded),
12996 ::core::option::Option::None =>
12997 ::core::default::Default::default(),
12998 }
12999 }
13000 }
13001 } else if fi.attrs.write_only || fi.attrs.skip {
13002 // Not read from model — use default
13003 quote! { #ident: ::core::default::Default::default() }
13004 } else if let Some(src) = &fi.attrs.source {
13005 let src_ident = syn::Ident::new(src, ident.span());
13006 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13007 } else {
13008 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13009 }
13010 });
13011
13012 // is_writable predicate (also used by writable_lits /
13013 // writable_source_lits below).
13014 let is_writable = |fi: &&FieldInfo| {
13015 !fi.attrs.read_only
13016 && !fi.attrs.skip
13017 && fi.attrs.method.is_none()
13018 && !fi.attrs.nested
13019 && fi.attrs.many.is_none()
13020 && fi.attrs.slug.is_none()
13021 };
13022
13023 // Declarative field constraints: one block
13024 // per writable field, run inside `validate()`. Each resolves its
13025 // bounds as the serializer attr when given (`#[serializer(max_length
13026 // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13027 // `max` / `choices`), then dispatches on the field's JSON value via
13028 // `forms::validators::check_value` (string → length/choices, integer
13029 // → min/max).
13030 let opt_usize = |v: Option<u64>| match v {
13031 Some(n) => {
13032 let n = n as usize;
13033 quote!(::core::option::Option::Some(#n))
13034 }
13035 None => quote!(::core::option::Option::None),
13036 };
13037 let opt_i64 = |v: Option<i64>| match v {
13038 Some(n) => quote!(::core::option::Option::Some(#n)),
13039 None => quote!(::core::option::Option::None),
13040 };
13041 let constraint_blocks: Vec<_> = fields_info
13042 .iter()
13043 .filter(is_writable)
13044 .map(|fi| {
13045 let ident = &fi.ident;
13046 let fname = ident.to_string();
13047 let mname = fi
13048 .attrs
13049 .source
13050 .clone()
13051 .unwrap_or_else(|| fi.ident.to_string());
13052 let attr_max_len = opt_usize(fi.attrs.max_length);
13053 let attr_min_len = opt_usize(fi.attrs.min_length);
13054 let attr_min = opt_i64(fi.attrs.min);
13055 let attr_max = opt_i64(fi.attrs.max);
13056 quote! {
13057 {
13058 let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13059 let __max_length: ::core::option::Option<usize> = #attr_max_len
13060 .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13061 let __min_length: ::core::option::Option<usize> = #attr_min_len;
13062 let __min: ::core::option::Option<i64> =
13063 #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13064 let __max: ::core::option::Option<i64> =
13065 #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13066 let __choices = __sf.and_then(|__f| __f.choices);
13067 let __v = #root::__serde_json::to_value(&self.#ident)
13068 .unwrap_or(#root::__serde_json::Value::Null);
13069 #root::forms::validators::check_value(
13070 #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13071 &mut __errors,
13072 );
13073 }
13074 }
13075 })
13076 .collect();
13077 let has_constraints = !constraint_blocks.is_empty();
13078
13079 // Per-field validators. Emit a
13080 // `validate(&self)` method that runs each user-defined validator
13081 // and aggregates errors into `FormErrors`.
13082 let validator_calls: Vec<_> = fields_info
13083 .iter()
13084 .filter_map(|fi| {
13085 let ident = &fi.ident;
13086 let name_lit = ident.to_string();
13087 let method = fi.attrs.validate.as_ref()?;
13088 let method_ident = syn::Ident::new(method, ident.span());
13089 Some(quote! {
13090 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13091 __errors.add(#name_lit.to_owned(), __e);
13092 }
13093 })
13094 })
13095 .collect();
13096 // #436 — cross-field `validate(&self)`. If the
13097 // container declared `#[serializer(validate = "fn_name")]`,
13098 // the macro-generated `validate(&self)` runs every per-field
13099 // validator first, then calls the user's cross-field method,
13100 // merging its `FormErrors` into the per-field errors. Either
13101 // alone is enough to emit the wrapper.
13102 let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13103 quote! {
13104 // Merge cross-field errors into the per-field bucket so
13105 // a single .validate() call surfaces both layers.
13106 if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13107 __errors.merge(__cross);
13108 }
13109 }
13110 });
13111 let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13112 // Anything to run? Declarative constraints (for any writable field) +
13113 // per-field validators + cross-field hook.
13114 let has_run_validations = has_validators || has_constraints;
13115 // Shared body: declarative field constraints first (length / range /
13116 // choices, serializer-attr-or-model), then per-field validators, then
13117 // the cross-field hook.
13118 let validate_body = quote! {
13119 let mut __errors = #root::forms::FormErrors::default();
13120 #( #constraint_blocks )*
13121 #( #validator_calls )*
13122 #cross_validate_call
13123 if __errors.is_empty() {
13124 ::core::result::Result::Ok(())
13125 } else {
13126 ::core::result::Result::Err(__errors)
13127 }
13128 };
13129 // Inherent `validate(&self)` — kept for back-compat with direct
13130 // `serializer.validate()` calls that don't import `ModelSerializer`.
13131 // Emitted only when the serializer declares per-field/cross-field
13132 // validators (unchanged rule) so it never collides with a
13133 // hand-written inherent `validate`.
13134 let validate_method = if has_validators {
13135 quote! {
13136 impl #struct_name {
13137 /// Run the declarative field constraints, every
13138 /// `#[serializer(validate = "...")]` per-field validator,
13139 /// and (when declared) the container-level cross-field
13140 /// validator. Aggregates errors into `FormErrors` keyed by
13141 /// the field name. Returns `Ok(())` when all pass.
13142 pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13143 #validate_body
13144 }
13145 }
13146 }
13147 } else {
13148 quote! {}
13149 };
13150 // Trait-method override so the type-erased ViewSet write path can
13151 // dispatch `ModelSerializer::validate` generically. Emitted whenever
13152 // there's anything to run (declarative constraints inherited from the
13153 // model count); otherwise the trait's default no-op is used.
13154 let trait_validate_override = if has_run_validations {
13155 quote! {
13156 fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13157 #validate_body
13158 }
13159 }
13160 } else {
13161 quote! {}
13162 };
13163
13164 // For every `#[serializer(many = S)]` field, emit a
13165 // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13166 // helper that maps the parents through `S::from_model`.
13167 let many_setters: Vec<_> = fields_info
13168 .iter()
13169 .filter_map(|fi| {
13170 let many_ty = fi.attrs.many.as_ref()?;
13171 let ident = &fi.ident;
13172 let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
13173 Some(quote! {
13174 /// Populate this `many` field by mapping each parent model
13175 /// through the inner serializer's `from_model`. Call after
13176 /// fetching the M2M / one-to-many children since
13177 /// `from_model` itself can't await an SQL query.
13178 pub fn #setter(
13179 &mut self,
13180 models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13181 ) -> &mut Self {
13182 self.#ident = models.iter()
13183 .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13184 .collect();
13185 self
13186 }
13187 })
13188 })
13189 .collect();
13190 let many_setters_impl = if many_setters.is_empty() {
13191 quote! {}
13192 } else {
13193 quote! {
13194 impl #struct_name {
13195 #( #many_setters )*
13196 }
13197 }
13198 };
13199
13200 // Generate custom Serialize: skip write_only fields
13201 let output_fields: Vec<_> = fields_info
13202 .iter()
13203 .filter(|fi| !fi.attrs.write_only)
13204 .collect();
13205 let output_field_count = output_fields.len();
13206 let serialize_fields = output_fields.iter().map(|fi| {
13207 let ident = &fi.ident;
13208 let name_lit = ident.to_string();
13209 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13210 });
13211
13212 // writable_fields: normal + write_only.
13213 // Exclude:
13214 // - `read_only` — server-computed.
13215 // - `skip` — caller sets manually post-from_model.
13216 // - `method` — computed from a Self::fn(&model) call; accepting
13217 // it on write is meaningless.
13218 // - `nested` / `many` — populated from related-model data, not
13219 // from a field on the wire body.
13220 // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13221 // `many` in `writable_fields()`, which made the ViewSet write
13222 // path accept those fields from the JSON body and try to bind
13223 // them to the SQL UPDATE — a silent no-op at best, a type
13224 // mismatch at worst.
13225 // (`is_writable` is defined above, near the constraint codegen.)
13226 let writable_lits: Vec<_> = fields_info
13227 .iter()
13228 .filter(is_writable)
13229 .map(|fi| fi.ident.to_string())
13230 .collect();
13231
13232 // `writable_source_fields`: the MODEL field names of writable
13233 // serializer fields (source-resolved). The ViewSet write path skips
13234 // every model column NOT in this set, so `read_only` / `method` /
13235 // computed fields a client posts are ignored instead of written.
13236 let writable_source_lits: Vec<String> = fields_info
13237 .iter()
13238 .filter(is_writable)
13239 .map(|fi| {
13240 fi.attrs
13241 .source
13242 .clone()
13243 .unwrap_or_else(|| fi.ident.to_string())
13244 })
13245 .collect();
13246
13247 // `from_writable_json`: build a partial instance for input
13248 // validation. Writable fields are parsed from the JSON body (keyed
13249 // by serializer field name); every other field defaults. Per-field
13250 // type errors collect into `FormErrors` keyed by the field name.
13251 // Construction is field-by-field (not `Self::default()`) so it works
13252 // for serializers regardless of a struct-level `Default`.
13253 let from_writable_json_inits: Vec<_> = fields_info
13254 .iter()
13255 .map(|fi| {
13256 let ident = &fi.ident;
13257 let fname = ident.to_string();
13258 let ty = &fi.ty;
13259 if is_writable(&fi) {
13260 quote! {
13261 #ident: match __obj.and_then(|__o| __o.get(#fname)) {
13262 ::core::option::Option::Some(__v) => {
13263 match #root::__serde_json::from_value::<#ty>(
13264 ::core::clone::Clone::clone(__v),
13265 ) {
13266 ::core::result::Result::Ok(__x) => __x,
13267 ::core::result::Result::Err(__e) => {
13268 __errors.add(#fname.to_owned(), __e.to_string());
13269 ::core::default::Default::default()
13270 }
13271 }
13272 }
13273 ::core::option::Option::None => ::core::default::Default::default(),
13274 }
13275 }
13276 } else {
13277 quote! { #ident: ::core::default::Default::default() }
13278 }
13279 })
13280 .collect();
13281
13282 // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
13283 // Only includes fields shown in JSON output (skips write_only). For each
13284 // `Option<T>` field, omit from `required` and add `.nullable()`.
13285 let openapi_impl = {
13286 #[cfg(feature = "openapi")]
13287 {
13288 let property_calls = output_fields.iter().map(|fi| {
13289 let ident = &fi.ident;
13290 let name_lit = ident.to_string();
13291 let ty = &fi.ty;
13292 let nullable_call = if is_option(ty) {
13293 quote! { .nullable() }
13294 } else {
13295 quote! {}
13296 };
13297 quote! {
13298 .property(
13299 #name_lit,
13300 <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13301 #nullable_call,
13302 )
13303 }
13304 });
13305 let required_lits: Vec<_> = output_fields
13306 .iter()
13307 .filter(|fi| !is_option(&fi.ty))
13308 .map(|fi| fi.ident.to_string())
13309 .collect();
13310 quote! {
13311 impl #root::openapi::OpenApiSchema for #struct_name {
13312 fn openapi_schema() -> #root::openapi::Schema {
13313 #root::openapi::Schema::object()
13314 #( #property_calls )*
13315 .required([ #( #required_lits ),* ])
13316 }
13317 }
13318 }
13319 }
13320 #[cfg(not(feature = "openapi"))]
13321 {
13322 quote! {}
13323 }
13324 };
13325
13326 Ok(quote! {
13327 impl #root::serializer::ModelSerializer for #struct_name {
13328 type Model = #model_path;
13329
13330 fn from_model(model: &Self::Model) -> Self {
13331 Self {
13332 #( #from_model_fields ),*
13333 }
13334 }
13335
13336 fn writable_fields() -> &'static [&'static str] {
13337 &[ #( #writable_lits ),* ]
13338 }
13339
13340 fn writable_source_fields() -> &'static [&'static str] {
13341 &[ #( #writable_source_lits ),* ]
13342 }
13343
13344 fn from_writable_json(
13345 __body: &#root::__serde_json::Value,
13346 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
13347 let mut __errors = #root::forms::FormErrors::default();
13348 let __obj = __body.as_object();
13349 let __out = Self {
13350 #( #from_writable_json_inits ),*
13351 };
13352 if __errors.is_empty() {
13353 ::core::result::Result::Ok(__out)
13354 } else {
13355 ::core::result::Result::Err(__errors)
13356 }
13357 }
13358
13359 #trait_validate_override
13360 }
13361
13362 impl #root::__serde::Serialize for #struct_name {
13363 fn serialize<S>(&self, serializer: S)
13364 -> ::core::result::Result<S::Ok, S::Error>
13365 where
13366 S: #root::__serde::Serializer,
13367 {
13368 use #root::__serde::ser::SerializeStruct;
13369 let mut __state = serializer.serialize_struct(
13370 #struct_name_lit,
13371 #output_field_count,
13372 )?;
13373 #( #serialize_fields )*
13374 __state.end()
13375 }
13376 }
13377
13378 #openapi_impl
13379
13380 #validate_method
13381
13382 #many_setters_impl
13383 })
13384}
13385
13386/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
13387/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
13388/// when the feature is off.
13389#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
13390fn is_option(ty: &syn::Type) -> bool {
13391 if let syn::Type::Path(p) = ty {
13392 if let Some(last) = p.path.segments.last() {
13393 return last.ident == "Option";
13394 }
13395 }
13396 false
13397}
13398
13399#[cfg(test)]
13400mod main_attr_tests {
13401 use super::*;
13402
13403 fn expand(args: &str) -> String {
13404 let body = quote! { async fn main() { run().await } };
13405 expand_main(args.parse().unwrap(), body)
13406 .expect("expansion failed")
13407 .to_string()
13408 }
13409
13410 /// Asserts on the expansion rather than on `parse_logging`,
13411 /// because the defect was a subscriber reaching the output, not
13412 /// an argument being misread (#1465).
13413 #[test]
13414 fn logging_false_emits_no_subscriber() {
13415 let out = expand("logging = false");
13416 assert!(
13417 !out.contains("tracing_subscriber"),
13418 "`logging = false` must not install a subscriber:\n{out}",
13419 );
13420 assert!(
13421 out.contains("block_on"),
13422 "the runtime must still be built:\n{out}",
13423 );
13424 }
13425
13426 #[test]
13427 fn default_still_installs_a_subscriber() {
13428 // The control. Without it, an `expand_main` that dropped the
13429 // prologue unconditionally would pass the test above while
13430 // leaving every default project with no logging at all.
13431 let out = expand("");
13432 assert!(
13433 out.contains("tracing_subscriber"),
13434 "the default entrypoint must still install one:\n{out}",
13435 );
13436 }
13437
13438 #[test]
13439 fn logging_false_composes_with_flavor() {
13440 let out = expand("flavor = \"current_thread\", logging = false");
13441 assert!(!out.contains("tracing_subscriber"), "{out}");
13442 assert!(out.contains("new_current_thread"), "{out}");
13443 }
13444
13445 #[test]
13446 fn logging_true_is_the_default_shape() {
13447 // `logging = true` is accepted and means the default, so the
13448 // match must not fire on the substring `logging`.
13449 assert!(parse_logging(&"logging = true".parse().unwrap()));
13450 assert!(!parse_logging(&"logging=false".parse().unwrap()));
13451 }
13452}