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 #root::core::ModelEntry {
988 schema: <#struct_name as #root::core::Model>::SCHEMA,
989 // `module_path!()` evaluates at the registration site,
990 // so a Model declared in `crate::blog::models` records
991 // `"<crate>::blog::models"` and `resolved_app_label()`
992 // can infer "blog" without an explicit attribute.
993 module_path: ::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 {
1769 where_clause: WhereExpr::ExprCompare {
1770 lhs: Expr::Column(#child_fk_column),
1771 op: Op::Eq,
1772 rhs: Expr::OuterRef(#self_pk_column),
1773 },
1774 ..SelectQuery::new(child_schema)
1775 };
1776 WhereExpr::Exists(::std::boxed::Box::new(inner))
1777 }
1778
1779 #[doc = #not_exists_doc]
1780 pub fn #not_exists_ident() -> #root::core::WhereExpr {
1781 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1782 let child_schema =
1783 <#child as #root::core::Model>::SCHEMA;
1784 let inner = SelectQuery {
1785 where_clause: WhereExpr::ExprCompare {
1786 lhs: Expr::Column(#child_fk_column),
1787 op: Op::Eq,
1788 rhs: Expr::OuterRef(#self_pk_column),
1789 },
1790 ..SelectQuery::new(child_schema)
1791 };
1792 WhereExpr::NotExists(::std::boxed::Box::new(inner))
1793 }
1794
1795 #[doc = #count_doc]
1796 pub async fn #count_ident(
1797 &self,
1798 pool: &#root::sql::Pool,
1799 ) -> ::core::result::Result<
1800 i64,
1801 #root::sql::ExecError,
1802 > {
1803 use #root::sql::CounterPool as _;
1804 #root::query::QuerySet::<#child>::new()
1805 .filter(#child_fk_column, self.__rustango_pk_value())
1806 .count(pool)
1807 .await
1808 }
1809 }
1810 });
1811 quote! {
1812 impl #struct_name {
1813 #( #methods )*
1814 }
1815 }
1816}
1817
1818/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1819/// `#[rustango(through(...))]` attribute. Issue #817.
1820///
1821/// Each accessor builds a correlated subquery via
1822/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1823/// intermediate table, filters on the FK column pointing at this
1824/// model, and projects the intermediate PK. The outer queryset
1825/// filters the far model by its FK-to-intermediate column being in
1826/// that set.
1827///
1828/// The returned `QuerySet<Far>` is **chainable** — the subquery
1829/// lives inside a `where_raw` clause so the user's later
1830/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1831/// disturb it.
1832///
1833/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1834/// SQLite — no LATERAL or backend-specific syntax involved.
1835fn through_accessor_tokens(
1836 struct_name: &syn::Ident,
1837 through_relations: &[ThroughAttr],
1838) -> TokenStream2 {
1839 let root = rustango_root();
1840 if through_relations.is_empty() {
1841 return TokenStream2::new();
1842 }
1843 let methods = through_relations.iter().map(|rel| {
1844 let method_name = format!("{}_through", rel.name);
1845 let count_name = format!("{}_through_count", rel.name);
1846 let fetch_name = format!("{}_through_fetch", rel.name);
1847 let first_name = format!("{}_through_first", rel.name);
1848 let pluck_name = format!("{}_through_pluck", rel.name);
1849 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1850 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1851 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1852 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1853 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1854 let far = &rel.far;
1855 let intermediate = &rel.intermediate;
1856 let far_fk_column = rel.far_fk_column.as_str();
1857 let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1858 let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1859 let doc = format!(
1860 "Eloquent `hasManyThrough` accessor — returns a \
1861 `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1862 instance through the intermediate `{intermediate}` table. \
1863 Generated SQL shape: \
1864 `… WHERE {far_fk_column} IN (SELECT \
1865 {intermediate_pk_column} FROM <{intermediate}> WHERE \
1866 {intermediate_fk_column} = self.pk)`. Chainable like any \
1867 other QuerySet — compose `.filter()` / `.order_by()` / \
1868 `.limit()` etc. on top.",
1869 );
1870 let count_doc = format!(
1871 "Eloquent `$model->{name}->count()` analog for the \
1872 through-relation — returns the number of `{far}` rows \
1873 reachable through `{intermediate}`. Equivalent to \
1874 `self.{name}_through().count(pool)` but spelled \
1875 as a bare instance method for parity with the \
1876 `reverse_has` `<name>_count` shape.",
1877 name = rel.name,
1878 );
1879 quote! {
1880 #[doc = #doc]
1881 pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1882 use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1883 let intermediate_schema =
1884 <#intermediate as #root::core::Model>::SCHEMA;
1885 let sub = SelectQuery {
1886 where_clause: WhereExpr::Predicate(Filter {
1887 column: #intermediate_fk_column,
1888 op: Op::Eq,
1889 value: self.__rustango_pk_value(),
1890 }),
1891 projection: ::core::option::Option::Some(
1892 ::std::vec![#intermediate_pk_column],
1893 ),
1894 ..SelectQuery::new(intermediate_schema)
1895 };
1896 #root::query::QuerySet::<#far>::new().where_raw(
1897 WhereExpr::InSubquery {
1898 column: #far_fk_column,
1899 negated: false,
1900 subquery: ::std::boxed::Box::new(sub),
1901 },
1902 )
1903 }
1904
1905 #[doc = #count_doc]
1906 pub async fn #count_ident(
1907 &self,
1908 pool: &#root::sql::Pool,
1909 ) -> ::core::result::Result<
1910 i64,
1911 #root::sql::ExecError,
1912 > {
1913 use #root::sql::CounterPool as _;
1914 self.#method_ident().count(pool).await
1915 }
1916
1917 /// Eloquent `$model->relation->get()` for the
1918 /// through-relation — bare-name hot-path over
1919 /// `self.<name>_through().fetch(&pool)`. Use this
1920 /// when you don't need further `.filter()` /
1921 /// `.order_by()` composition; falls back to the
1922 /// chainable accessor when you do. Avoids the `_pool`
1923 /// suffix on the most common call-site shape.
1924 pub async fn #fetch_ident(
1925 &self,
1926 pool: &#root::sql::Pool,
1927 ) -> ::core::result::Result<
1928 ::std::vec::Vec<#far>,
1929 #root::sql::ExecError,
1930 > {
1931 use #root::sql::FetcherPool as _;
1932 self.#method_ident().fetch(pool).await
1933 }
1934
1935 /// Eloquent `hasOneThrough` analog — bare-name shortcut
1936 /// over `self.<name>_through().first(&pool)`. Returns
1937 /// `None` when no far rows are reachable through the
1938 /// intermediate. Useful when at most one row is
1939 /// expected (latest comment by country, primary tag,
1940 /// etc.).
1941 pub async fn #first_ident(
1942 &self,
1943 pool: &#root::sql::Pool,
1944 ) -> ::core::result::Result<
1945 ::core::option::Option<#far>,
1946 #root::sql::ExecError,
1947 > {
1948 self.#method_ident().first(pool).await
1949 }
1950
1951 /// Pluck a single column from the far rows into `Vec<U>`
1952 /// — cheaper than the typed `<Far>` decode when only one
1953 /// scalar column is needed.
1954 pub async fn #pluck_ident<U>(
1955 &self,
1956 col: &'static str,
1957 pool: &#root::sql::Pool,
1958 ) -> ::core::result::Result<
1959 ::std::vec::Vec<U>,
1960 #root::sql::ExecError,
1961 >
1962 where
1963 U: #root::sql::MaybePgScalar
1964 + #root::sql::MaybeMyScalar
1965 + #root::sql::MaybeSqliteScalar
1966 + ::core::marker::Send
1967 + ::core::marker::Unpin,
1968 {
1969 self.#method_ident()
1970 .values_list_flat(col)
1971 .fetch::<U>(pool)
1972 .await
1973 }
1974 }
1975 });
1976 quote! {
1977 impl #struct_name {
1978 #( #methods )*
1979 }
1980 }
1981}
1982
1983struct ColumnEntry {
1984 /// The struct field ident, used both for the inherent const name on
1985 /// the model and for the inner column type's name.
1986 ident: syn::Ident,
1987 /// The struct's field type, used as `Column::Value`.
1988 value_ty: Type,
1989 /// Rust-side field name (e.g. `"id"`).
1990 name: String,
1991 /// SQL-side column name (e.g. `"user_id"`).
1992 column: String,
1993 /// `#root::core::FieldType::I64` etc.
1994 field_type_tokens: TokenStream2,
1995}
1996
1997struct CollectedFields {
1998 field_schemas: Vec<TokenStream2>,
1999 from_row_inits: Vec<TokenStream2>,
2000 /// Aliased counterparts of `from_row_inits` — read columns via
2001 /// `format!("{prefix}__{col}")` aliases so a Model can be
2002 /// decoded from a JOINed row's projected target columns.
2003 from_aliased_row_inits: Vec<TokenStream2>,
2004 /// Static column-name list — used by the simple insert path
2005 /// (no `Auto<T>` fields). Aligned with `insert_values`.
2006 insert_columns: Vec<TokenStream2>,
2007 /// Static `Into<SqlValue>` expressions, one per field. Aligned
2008 /// with `insert_columns`. Used by the simple insert path only.
2009 insert_values: Vec<TokenStream2>,
2010 /// Per-field push expressions for the dynamic (Auto-aware)
2011 /// insert path. Each statement either unconditionally pushes
2012 /// `(column, value)` or, for an `Auto<T>` field, conditionally
2013 /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
2014 insert_pushes: Vec<TokenStream2>,
2015 /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
2016 /// when `has_auto == false`.
2017 returning_cols: Vec<TokenStream2>,
2018 /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
2019 /// field. Run after `insert_returning` to populate the model.
2020 auto_assigns: Vec<TokenStream2>,
2021 /// `(ident, column_literal)` pairs for every Auto field. Used by
2022 /// the bulk_insert codegen to rebuild assigns against `_row_mut`
2023 /// instead of `self`.
2024 auto_field_idents: Vec<(syn::Ident, String)>,
2025 /// #1028 — `(ident, column)` for each `generated_as` field. Drives
2026 /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
2027 /// back into the struct after insert (MySQL has no RETURNING → the
2028 /// field stays at its placeholder until the next read).
2029 generated_field_idents: Vec<(syn::Ident, String)>,
2030 /// Inner `T` of the first `Auto<T>` field, for the MySQL
2031 /// `LAST_INSERT_ID()` assignment in `AssignAutoPkPool`.
2032 first_auto_value_ty: Option<Type>,
2033 /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2034 /// by the all-Auto-Unset bulk path (Auto cols dropped from
2035 /// `columns`).
2036 bulk_pushes_no_auto: Vec<TokenStream2>,
2037 /// Bulk-insert per-row pushes for **all fields including Auto**.
2038 /// Used by the all-Auto-Set bulk path (Auto col included with the
2039 /// caller-supplied value).
2040 bulk_pushes_all: Vec<TokenStream2>,
2041 /// Column-name literals for non-Auto fields only (paired with
2042 /// `bulk_pushes_no_auto`).
2043 bulk_columns_no_auto: Vec<TokenStream2>,
2044 /// Column-name literals for every field including Auto (paired
2045 /// with `bulk_pushes_all`).
2046 bulk_columns_all: Vec<TokenStream2>,
2047 /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2048 /// + the loop that asserts every row matches. One pair per Auto
2049 /// field. Empty when `has_auto == false`.
2050 bulk_auto_uniformity: Vec<TokenStream2>,
2051 /// Identifier of the first Auto field, used as the witness for
2052 /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2053 first_auto_ident: Option<syn::Ident>,
2054 /// `true` if any field on the struct is `Auto<T>`.
2055 has_auto: bool,
2056 /// `true` when the primary-key field's Rust type is `Auto<T>`.
2057 /// Gates `save()` codegen — only Auto PKs let us infer
2058 /// insert-vs-update from the in-memory value.
2059 pk_is_auto: bool,
2060 /// `Assignment` constructors for every non-PK column. Drives the
2061 /// UPDATE branch of `save()`.
2062 update_assignments: Vec<TokenStream2>,
2063 /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2064 /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2065 upsert_update_columns: Vec<TokenStream2>,
2066 primary_key: Option<(syn::Ident, String)>,
2067 column_entries: Vec<ColumnEntry>,
2068 /// Rust-side field names, in declaration order. Used to validate
2069 /// container attributes like `display = "…"`.
2070 field_names: Vec<String>,
2071 /// FK fields on this child model. Drives the reverse-relation
2072 /// helper emit — for each FK, the macro adds an inherent
2073 /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2074 /// method on the parent type.
2075 fk_relations: Vec<FkRelation>,
2076 /// SQL column name of the `#[rustango(soft_delete)]` field, if
2077 /// the model has one. Drives emission of the `soft_delete_on` /
2078 /// `restore_on` inherent methods. At most one such column per
2079 /// model is allowed; collect_fields rejects duplicates.
2080 soft_delete_column: Option<String>,
2081 /// Rust field ident of the `#[rustango(soft_delete)]` field —
2082 /// companion to `soft_delete_column` for emitting predicates
2083 /// that need to read the field off `&self` (e.g. `trashed()`).
2084 soft_delete_field_ident: Option<syn::Ident>,
2085}
2086
2087#[derive(Clone)]
2088struct FkRelation {
2089 /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2090 /// helper is emitted as `impl <ParentType> { … }`.
2091 parent_type: Type,
2092 /// SQL column name on the child table for this FK (e.g. `"author"`).
2093 /// Used in the generated `WHERE <fk_column> = $1` clause.
2094 fk_column: String,
2095 /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2096 /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2097 /// default `ForeignKey<T>` (no explicit K); other kinds when the
2098 /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2099 pk_kind: DetectedKind,
2100 /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2101 /// FK column). Drives the `Some(...)` wrapping in load_related
2102 /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2103 /// the codegen matches the field's declared shape.
2104 nullable: bool,
2105 /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2106 /// override. When set, the reverse helper picks this name instead
2107 /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2108 /// #816 (issue's "Related" note re: per-FK override).
2109 related_name: Option<String>,
2110}
2111
2112fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2113 let root = rustango_root();
2114 let cap = named.named.len();
2115 let mut out = CollectedFields {
2116 field_schemas: Vec::with_capacity(cap),
2117 from_row_inits: Vec::with_capacity(cap),
2118 from_aliased_row_inits: Vec::with_capacity(cap),
2119 insert_columns: Vec::with_capacity(cap),
2120 insert_values: Vec::with_capacity(cap),
2121 insert_pushes: Vec::with_capacity(cap),
2122 returning_cols: Vec::new(),
2123 auto_assigns: Vec::new(),
2124 auto_field_idents: Vec::new(),
2125 generated_field_idents: Vec::new(),
2126 first_auto_value_ty: None,
2127 bulk_pushes_no_auto: Vec::with_capacity(cap),
2128 bulk_pushes_all: Vec::with_capacity(cap),
2129 bulk_columns_no_auto: Vec::with_capacity(cap),
2130 bulk_columns_all: Vec::with_capacity(cap),
2131 bulk_auto_uniformity: Vec::new(),
2132 first_auto_ident: None,
2133 has_auto: false,
2134 pk_is_auto: false,
2135 update_assignments: Vec::with_capacity(cap),
2136 upsert_update_columns: Vec::with_capacity(cap),
2137 primary_key: None,
2138 column_entries: Vec::with_capacity(cap),
2139 field_names: Vec::with_capacity(cap),
2140 fk_relations: Vec::new(),
2141 soft_delete_column: None,
2142 soft_delete_field_ident: None,
2143 };
2144
2145 for field in &named.named {
2146 let info = process_field(field, table)?;
2147 out.field_names.push(info.ident.to_string());
2148 out.field_schemas.push(info.schema);
2149 out.from_row_inits.push(info.from_row_init);
2150 out.from_aliased_row_inits.push(info.from_aliased_row_init);
2151 if let Some(parent_ty) = info.fk_inner.clone() {
2152 out.fk_relations.push(FkRelation {
2153 parent_type: parent_ty,
2154 fk_column: info.column.clone(),
2155 pk_kind: info.fk_pk_kind,
2156 nullable: info.nullable,
2157 related_name: info.related_name.clone(),
2158 });
2159 }
2160 if info.soft_delete {
2161 if out.soft_delete_column.is_some() {
2162 return Err(syn::Error::new_spanned(
2163 field,
2164 "only one field may be marked `#[rustango(soft_delete)]`",
2165 ));
2166 }
2167 out.soft_delete_column = Some(info.column.clone());
2168 out.soft_delete_field_ident = Some(info.ident.clone());
2169 }
2170 let column = info.column.as_str();
2171 let ident = info.ident;
2172 // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2173 // skip every write path — Postgres recomputes the value
2174 // from EXPR. Push only the column-entry record (so typed
2175 // column constants still exist for filtering / projection)
2176 // and the schema literal (already pushed above) and move
2177 // on. No insert_columns/values, no insert_pushes, no
2178 // bulk_*, no update_assignments, no upsert_update_columns,
2179 // no returning_cols.
2180 if info.generated_as.is_some() {
2181 out.column_entries.push(ColumnEntry {
2182 ident: ident.clone(),
2183 value_ty: info.value_ty.clone(),
2184 name: ident.to_string(),
2185 column: info.column.clone(),
2186 field_type_tokens: info.field_type_tokens,
2187 });
2188 // #1028 — refresh the DB-computed value after insert on
2189 // RETURNING-capable backends. The column joins `returning_cols`
2190 // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2191 // ident is recorded so the `AssignAutoPkPool` impl decodes it
2192 // back into the struct. MySQL has no `INSERT … RETURNING`, so
2193 // it keeps the placeholder until the next read.
2194 out.returning_cols.push(quote!(#column));
2195 out.generated_field_idents
2196 .push((ident.clone(), info.column.clone()));
2197 continue;
2198 }
2199 out.insert_columns.push(quote!(#column));
2200 out.insert_values.push(quote! {
2201 ::core::convert::Into::<#root::core::SqlValue>::into(
2202 ::core::clone::Clone::clone(&self.#ident)
2203 )
2204 });
2205 if info.auto {
2206 out.has_auto = true;
2207 if out.first_auto_ident.is_none() {
2208 out.first_auto_ident = Some(ident.clone());
2209 out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
2210 }
2211 // `default_uuid_v7` (issue #823) generates the PK Rust-side
2212 // before binding, so the value is already in
2213 // `self.#ident` after the insert_push — RETURNING is
2214 // redundant. Skip adding this column to returning_cols /
2215 // auto_assigns / auto_field_idents to avoid (a) an
2216 // unnecessary RETURNING column on every dialect, and (b)
2217 // the MySQL `LAST_INSERT_ID()` path that can only fill an
2218 // integer PK.
2219 // `auto_now_add` / `auto_now` join `default_uuid_v7` here
2220 // for the same reason: they are filled Rust-side below and
2221 // bound, so the value is already in `self` and RETURNING
2222 // would be a redundant column on every dialect.
2223 if !info.default_uuid_v7 && !info.auto_now_add && !info.auto_now {
2224 out.returning_cols.push(quote!(#column));
2225 out.auto_field_idents
2226 .push((ident.clone(), info.column.clone()));
2227 out.auto_assigns.push(quote! {
2228 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2229 });
2230 }
2231 if info.default_uuid_v7 {
2232 // Rust-side UUIDv7 generation (issue #823, Eloquent
2233 // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2234 // and bind; Auto::Set → bind the user's value. The
2235 // column is ALWAYS present in the INSERT statement —
2236 // no RETURNING / no DB DEFAULT needed.
2237 out.insert_pushes.push(quote! {
2238 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2239 self.#ident = #root::sql::Auto::Set(
2240 #root::__uuid::Uuid::now_v7(),
2241 );
2242 }
2243 if let #root::sql::Auto::Set(_v) = &self.#ident {
2244 _columns.push(#column);
2245 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2246 ::core::clone::Clone::clone(_v)
2247 ));
2248 }
2249 });
2250 } else if info.auto_now_add || info.auto_now {
2251 // Rust-side write timestamp (#1464), mirroring
2252 // `default_uuid_v7` above. `auto_now` has always bound
2253 // `Utc::now()` on UPDATE but not on INSERT, so the
2254 // first write of an `updated_at` took the DB default
2255 // while every later one took the clock — the same
2256 // column, filled two different ways.
2257 //
2258 // The default is the problem. `CREATE TABLE IF NOT
2259 // EXISTS` leaves an upgraded table's
2260 // `DEFAULT CURRENT_TIMESTAMP` in place, and SQLite's
2261 // ALTER TABLE grammar (RENAME/ADD/DROP) has no
2262 // statement that changes it — so every insert relying
2263 // on that default wrote the legacy shape forever, while
2264 // the migrate sweep converted the rows around it. The
2265 // column went permanently mixed and `ORDER BY` inverted
2266 // the admin audit log (#1616 rework review,
2267 // correctness-001 / dialects-005 / security-002).
2268 //
2269 // Binding it here makes the stale default unreachable
2270 // rather than trying to migrate it: the timestamp comes
2271 // from the application, not the database.
2272 out.insert_pushes.push(quote! {
2273 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2274 self.#ident = #root::sql::Auto::Set(
2275 #root::__chrono::Utc::now(),
2276 );
2277 }
2278 if let #root::sql::Auto::Set(_v) = &self.#ident {
2279 _columns.push(#column);
2280 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2281 ::core::clone::Clone::clone(_v)
2282 ));
2283 }
2284 });
2285 } else {
2286 out.insert_pushes.push(quote! {
2287 if let #root::sql::Auto::Set(_v) = &self.#ident {
2288 _columns.push(#column);
2289 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2290 ::core::clone::Clone::clone(_v)
2291 ));
2292 }
2293 });
2294 }
2295 // Bulk: Auto fields appear only in the all-Set path,
2296 // never in the Unset path (we drop them from `columns`).
2297 out.bulk_columns_all.push(quote!(#column));
2298 out.bulk_pushes_all.push(quote! {
2299 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2300 ::core::clone::Clone::clone(&_row.#ident)
2301 ));
2302 });
2303 // …except the timestamp columns, which must appear in BOTH
2304 // paths (#1464). The path is chosen by the *first* Auto
2305 // field — in practice the PK — so a bulk insert of rows
2306 // with an unset PK took the no-auto branch and dropped the
2307 // timestamp column, falling back to the DB default that
2308 // this change exists to stop depending on.
2309 //
2310 // `rows` is borrowed immutably here, so this fills per row
2311 // at push time rather than writing back into the struct. An
2312 // explicitly-Set value is still honoured; only `Unset`
2313 // takes the clock.
2314 if info.auto_now_add || info.auto_now {
2315 out.bulk_columns_no_auto.push(quote!(#column));
2316 out.bulk_pushes_no_auto.push(quote! {
2317 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2318 match &_row.#ident {
2319 #root::sql::Auto::Set(_v) => ::core::clone::Clone::clone(_v),
2320 #root::sql::Auto::Unset => #root::__chrono::Utc::now(),
2321 }
2322 ));
2323 });
2324 }
2325 // Uniformity check: every row's Auto state must match the
2326 // first row's. Mixed Set/Unset within one bulk_insert is
2327 // rejected here so the column list stays consistent.
2328 let ident_clone = ident.clone();
2329 out.bulk_auto_uniformity.push(quote! {
2330 for _r in rows.iter().skip(1) {
2331 if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2332 return ::core::result::Result::Err(
2333 #root::sql::ExecError::Sql(
2334 #root::sql::SqlError::BulkAutoMixed
2335 )
2336 );
2337 }
2338 }
2339 });
2340 } else {
2341 out.insert_pushes.push(quote! {
2342 _columns.push(#column);
2343 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2344 ::core::clone::Clone::clone(&self.#ident)
2345 ));
2346 });
2347 // Bulk: non-Auto fields appear in BOTH paths.
2348 out.bulk_columns_no_auto.push(quote!(#column));
2349 out.bulk_columns_all.push(quote!(#column));
2350 let push_expr = quote! {
2351 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2352 ::core::clone::Clone::clone(&_row.#ident)
2353 ));
2354 };
2355 out.bulk_pushes_no_auto.push(push_expr.clone());
2356 out.bulk_pushes_all.push(push_expr);
2357 }
2358 if info.primary_key {
2359 if out.primary_key.is_some() {
2360 return Err(syn::Error::new_spanned(
2361 field,
2362 "only one field may be marked `#[rustango(primary_key)]`",
2363 ));
2364 }
2365 out.primary_key = Some((ident.clone(), info.column.clone()));
2366 if info.auto {
2367 out.pk_is_auto = true;
2368 }
2369 } else if info.auto_now_add {
2370 // Immutable post-insert: skip from UPDATE entirely.
2371 } else if info.auto_now {
2372 // `auto_now` columns: bind `chrono::Utc::now()` on every
2373 // UPDATE so the column is always overridden with the
2374 // wall-clock at write time, regardless of what value the
2375 // user left in the struct field.
2376 out.update_assignments.push(quote! {
2377 #root::core::Assignment {
2378 column: #column,
2379 value: ::core::convert::Into::<#root::core::Expr>::into(
2380 ::core::convert::Into::<#root::core::SqlValue>::into(
2381 #root::__chrono::Utc::now()
2382 )
2383 ),
2384 }
2385 });
2386 out.upsert_update_columns.push(quote!(#column));
2387 } else {
2388 out.update_assignments.push(quote! {
2389 #root::core::Assignment {
2390 column: #column,
2391 value: ::core::convert::Into::<#root::core::Expr>::into(
2392 ::core::convert::Into::<#root::core::SqlValue>::into(
2393 ::core::clone::Clone::clone(&self.#ident)
2394 )
2395 ),
2396 }
2397 });
2398 out.upsert_update_columns.push(quote!(#column));
2399 }
2400 out.column_entries.push(ColumnEntry {
2401 ident: ident.clone(),
2402 value_ty: info.value_ty.clone(),
2403 name: ident.to_string(),
2404 column: info.column.clone(),
2405 field_type_tokens: info.field_type_tokens,
2406 });
2407 }
2408 Ok(out)
2409}
2410
2411fn model_impl_tokens(
2412 struct_name: &syn::Ident,
2413 model_name: &str,
2414 table: &str,
2415 display: Option<&str>,
2416 app_label: Option<&str>,
2417 admin: Option<&AdminAttrs>,
2418 default_order: &[(String, bool, proc_macro2::Span)],
2419 field_schemas: &[TokenStream2],
2420 soft_delete_column: Option<&str>,
2421 permissions: bool,
2422 audit_track: Option<&[String]>,
2423 m2m_relations: &[M2MAttr],
2424 indexes: &[IndexAttr],
2425 checks: &[CheckAttr],
2426 excludes: &[ExcludeAttr],
2427 composite_fks: &[CompositeFkAttr],
2428 generic_fks: &[GenericFkAttr],
2429 scope: Option<&str>,
2430 is_view: bool,
2431 verbose_name: Option<&str>,
2432 verbose_name_plural: Option<&str>,
2433 managed: bool,
2434 base_manager_name: Option<&str>,
2435 order_with_respect_to: Option<&str>,
2436 proxy: bool,
2437 required_db_features: &[String],
2438 required_db_vendor: Option<&str>,
2439 default_related_name: Option<&str>,
2440 db_table_comment: Option<&str>,
2441 get_latest_by: Option<(&str, bool)>,
2442 extra_permissions: &[(String, String)],
2443 default_permissions: &[String],
2444 global_scopes: &[GlobalScopeAttr],
2445 reverse_has_relations: &[ReverseHasAttr],
2446 generic_has_relations: &[GenericHasAttr],
2447) -> TokenStream2 {
2448 let root = rustango_root();
2449 let display_tokens = if let Some(name) = display {
2450 quote!(::core::option::Option::Some(#name))
2451 } else {
2452 quote!(::core::option::Option::None)
2453 };
2454 let app_label_tokens = if let Some(name) = app_label {
2455 quote!(::core::option::Option::Some(#name))
2456 } else {
2457 quote!(::core::option::Option::None)
2458 };
2459 let soft_delete_tokens = if let Some(col) = soft_delete_column {
2460 quote!(::core::option::Option::Some(#col))
2461 } else {
2462 quote!(::core::option::Option::None)
2463 };
2464 let audit_track_tokens = match audit_track {
2465 None => quote!(::core::option::Option::None),
2466 Some(names) => {
2467 let lits = names.iter().map(|n| n.as_str());
2468 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2469 }
2470 };
2471 let admin_tokens = admin_config_tokens(admin);
2472 // Default `tenant` so single-tenant projects (no `scope` attr
2473 // anywhere) keep the v0.24.x behavior. Container-attr parser
2474 // already validated the value is "registry" or "tenant".
2475 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2476 Some("registry") => quote!(#root::core::ModelScope::Registry),
2477 _ => quote!(#root::core::ModelScope::Tenant),
2478 };
2479 let verbose_name_tokens = optional_str(verbose_name);
2480 let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2481 let base_manager_name_tokens = optional_str(base_manager_name);
2482 let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2483 let required_db_features_lits: Vec<&str> =
2484 required_db_features.iter().map(String::as_str).collect();
2485 let required_db_vendor_tokens = optional_str(required_db_vendor);
2486 let default_related_name_tokens = optional_str(default_related_name);
2487 let db_table_comment_tokens = optional_str(db_table_comment);
2488 let get_latest_by_tokens = match get_latest_by {
2489 Some((col, desc)) => {
2490 quote!(::core::option::Option::Some((#col, #desc)))
2491 }
2492 None => quote!(::core::option::Option::None),
2493 };
2494 let extra_permission_tokens: Vec<_> = extra_permissions
2495 .iter()
2496 .map(|(c, l)| quote!((#c, #l)))
2497 .collect();
2498 let default_permission_tokens: Vec<_> = default_permissions
2499 .iter()
2500 .map(|action| quote!(#action))
2501 .collect();
2502 let indexes_tokens = indexes.iter().map(|idx| {
2503 // When no explicit `name = "..."` was given, derive a stable,
2504 // collision-free name from the table + columns
2505 // (`<table>_<col>_<col>_idx`) instead of a shared literal that
2506 // would clash the moment a model declares two unnamed indexes.
2507 // Capped at Postgres's 63-char identifier limit.
2508 let derived_name = idx.name.clone().unwrap_or_else(|| {
2509 let mut n = format!("{table}_{}_idx", idx.columns.join("_"));
2510 if n.len() > 63 {
2511 n.truncate(63);
2512 }
2513 n
2514 });
2515 let name = derived_name.as_str();
2516 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2517 let unique = idx.unique;
2518 // Map the parsed method string onto the IndexMethod enum
2519 // variant — kept at the codegen layer so the IR doesn't
2520 // carry the string form.
2521 let method_variant = match idx.method.as_str() {
2522 "gin" => quote!(#root::core::IndexMethod::Gin),
2523 "gist" => quote!(#root::core::IndexMethod::Gist),
2524 "brin" => quote!(#root::core::IndexMethod::Brin),
2525 "spgist" => quote!(#root::core::IndexMethod::SpGist),
2526 "hash" => quote!(#root::core::IndexMethod::Hash),
2527 "bloom" => quote!(#root::core::IndexMethod::Bloom),
2528 _ => quote!(#root::core::IndexMethod::BTree),
2529 };
2530 let where_clause = match &idx.where_clause {
2531 Some(s) => quote!(::core::option::Option::Some(#s)),
2532 None => quote!(::core::option::Option::None),
2533 };
2534 let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2535 quote! {
2536 #root::core::IndexSchema {
2537 name: #name,
2538 columns: &[ #(#cols),* ],
2539 unique: #unique,
2540 method: #method_variant,
2541 where_clause: #where_clause,
2542 include: &[ #(#include_lits),* ],
2543 }
2544 }
2545 });
2546 let checks_tokens = checks.iter().map(|c| {
2547 let name = c.name.as_str();
2548 let expr = c.expr.as_str();
2549 quote! {
2550 #root::core::CheckConstraint {
2551 name: #name,
2552 expr: #expr,
2553 }
2554 }
2555 });
2556 let excludes_tokens = excludes.iter().map(|e| {
2557 let name = e.name.as_str();
2558 let using = e.using.as_str();
2559 let element_tokens = e.elements.iter().map(|(col, op)| {
2560 let col_s = col.as_str();
2561 let op_s = op.as_str();
2562 quote!((#col_s, #op_s))
2563 });
2564 let where_tokens = match e.where_clause.as_deref() {
2565 Some(w) => quote!(::core::option::Option::Some(#w)),
2566 None => quote!(::core::option::Option::None),
2567 };
2568 quote! {
2569 #root::core::ExclusionConstraint {
2570 name: #name,
2571 using: #using,
2572 elements: &[ #(#element_tokens),* ],
2573 where_clause: #where_tokens,
2574 }
2575 }
2576 });
2577 let composite_fk_tokens = composite_fks.iter().map(|rel| {
2578 let name = rel.name.as_str();
2579 let to = rel.to.as_str();
2580 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2581 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2582 quote! {
2583 #root::core::CompositeFkRelation {
2584 name: #name,
2585 to: #to,
2586 from: &[ #(#from_cols),* ],
2587 on: &[ #(#on_cols),* ],
2588 }
2589 }
2590 });
2591 let generic_fk_tokens = generic_fks.iter().map(|rel| {
2592 let name = rel.name.as_str();
2593 let ct_col = rel.ct_column.as_str();
2594 let pk_col = rel.pk_column.as_str();
2595 quote! {
2596 #root::core::GenericRelation {
2597 name: #name,
2598 ct_column: #ct_col,
2599 pk_column: #pk_col,
2600 }
2601 }
2602 });
2603 // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2604 // no `#[rustango(default_order = "...")]` attribute was supplied.
2605 let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2606 let col_lit = col.as_str();
2607 quote! { (#col_lit, #desc) }
2608 });
2609
2610 // Issue #820 — `global_scopes` slice literal. Empty when no
2611 // `#[rustango(global_scope(...))]` attribute was supplied. The
2612 // `apply` path is re-emitted verbatim so it resolves in the
2613 // consumer's scope; the name is stored as a string literal.
2614 let global_scope_tokens = global_scopes.iter().map(|s| {
2615 let name = s.name.as_str();
2616 let apply = &s.apply;
2617 quote! {
2618 #root::core::GlobalScope {
2619 name: #name,
2620 apply: #apply,
2621 }
2622 }
2623 });
2624
2625 let m2m_tokens = m2m_relations.iter().map(|rel| {
2626 let name = rel.name.as_str();
2627 let to = rel.to.as_str();
2628 let through = rel.through.as_str();
2629 let src = rel.src.as_str();
2630 let dst = rel.dst.as_str();
2631 let auto_create = rel.auto_create;
2632 quote! {
2633 #root::core::M2MRelation {
2634 name: #name,
2635 to: #to,
2636 through: #through,
2637 src_col: #src,
2638 dst_col: #dst,
2639 auto_create: #auto_create,
2640 }
2641 }
2642 });
2643 // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2644 // when the model declares `#[rustango(reverse_has(...))]`. Each
2645 // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2646 // a const expression. Models without reverse_has fall through to
2647 // the trait's empty default — no override emitted.
2648 let reverse_relations_override = if reverse_has_relations.is_empty() {
2649 quote!()
2650 } else {
2651 let entries = reverse_has_relations.iter().map(|rel| {
2652 let name = rel.name.as_str();
2653 let child = &rel.child;
2654 let child_fk_column = rel.child_fk_column.as_str();
2655 let self_pk_column = rel.self_pk_column.as_str();
2656 quote! {
2657 #root::core::ReverseRelation {
2658 name: #name,
2659 child_schema: <#child as #root::core::Model>::SCHEMA,
2660 child_fk_column: #child_fk_column,
2661 self_pk_column: #self_pk_column,
2662 }
2663 }
2664 });
2665 quote! {
2666 fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2667 const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2668 RELS
2669 }
2670 }
2671 };
2672 // Issue #830 — `Model::generic_reverse_relations()` override for
2673 // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2674 let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2675 quote!()
2676 } else {
2677 let entries = generic_has_relations.iter().map(|rel| {
2678 let name = rel.name.as_str();
2679 let child = &rel.child;
2680 let ct_column = rel.ct_column.as_str();
2681 let pk_column = rel.pk_column.as_str();
2682 let self_pk_column = rel.self_pk_column.as_str();
2683 quote! {
2684 #root::core::GenericReverseRelation {
2685 name: #name,
2686 child_schema: <#child as #root::core::Model>::SCHEMA,
2687 ct_column: #ct_column,
2688 pk_column: #pk_column,
2689 self_pk_column: #self_pk_column,
2690 }
2691 }
2692 });
2693 quote! {
2694 fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2695 const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2696 RELS
2697 }
2698 }
2699 };
2700 quote! {
2701 impl #root::core::Model for #struct_name {
2702 const SCHEMA: &'static #root::core::ModelSchema = &#root::core::ModelSchema {
2703 name: #model_name,
2704 table: #table,
2705 fields: &[ #(#field_schemas),* ],
2706 display: #display_tokens,
2707 app_label: #app_label_tokens,
2708 admin: #admin_tokens,
2709 soft_delete_column: #soft_delete_tokens,
2710 permissions: #permissions,
2711 audit_track: #audit_track_tokens,
2712 m2m: &[ #(#m2m_tokens),* ],
2713 indexes: &[ #(#indexes_tokens),* ],
2714 check_constraints: &[ #(#checks_tokens),* ],
2715 exclusion_constraints: &[ #(#excludes_tokens),* ],
2716 composite_relations: &[ #(#composite_fk_tokens),* ],
2717 generic_relations: &[ #(#generic_fk_tokens),* ],
2718 scope: #scope_tokens,
2719 default_order: &[ #(#default_order_tokens),* ],
2720 is_view: #is_view,
2721 verbose_name: #verbose_name_tokens,
2722 verbose_name_plural: #verbose_name_plural_tokens,
2723 managed: #managed,
2724 base_manager_name: #base_manager_name_tokens,
2725 order_with_respect_to: #order_with_respect_to_tokens,
2726 proxy: #proxy,
2727 required_db_features: &[ #(#required_db_features_lits),* ],
2728 required_db_vendor: #required_db_vendor_tokens,
2729 default_related_name: #default_related_name_tokens,
2730 db_table_comment: #db_table_comment_tokens,
2731 get_latest_by: #get_latest_by_tokens,
2732 extra_permissions: &[ #(#extra_permission_tokens),* ],
2733 default_permissions: &[ #(#default_permission_tokens),* ],
2734 global_scopes: &[ #(#global_scope_tokens),* ],
2735 };
2736
2737 #reverse_relations_override
2738 #generic_reverse_relations_override
2739 }
2740 }
2741}
2742
2743/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2744/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2745/// otherwise a static reference to a populated `AdminConfig`.
2746fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2747 let root = rustango_root();
2748 let Some(admin) = admin else {
2749 return quote!(::core::option::Option::None);
2750 };
2751
2752 let list_display = admin
2753 .list_display
2754 .as_ref()
2755 .map(|(v, _)| v.as_slice())
2756 .unwrap_or(&[]);
2757 let list_display_lits = list_display.iter().map(|s| s.as_str());
2758
2759 let search_fields = admin
2760 .search_fields
2761 .as_ref()
2762 .map(|(v, _)| v.as_slice())
2763 .unwrap_or(&[]);
2764 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2765
2766 let readonly_fields = admin
2767 .readonly_fields
2768 .as_ref()
2769 .map(|(v, _)| v.as_slice())
2770 .unwrap_or(&[]);
2771 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2772
2773 let list_filter = admin
2774 .list_filter
2775 .as_ref()
2776 .map(|(v, _)| v.as_slice())
2777 .unwrap_or(&[]);
2778 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2779
2780 let actions = admin
2781 .actions
2782 .as_ref()
2783 .map(|(v, _)| v.as_slice())
2784 .unwrap_or(&[]);
2785 let actions_lits = actions.iter().map(|s| s.as_str());
2786
2787 let fieldsets = admin
2788 .fieldsets
2789 .as_ref()
2790 .map(|(v, _)| v.as_slice())
2791 .unwrap_or(&[]);
2792 let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
2793 let title = title.as_str();
2794 let field_lits = fields.iter().map(|s| s.as_str());
2795 quote!(#root::core::Fieldset {
2796 title: #title,
2797 fields: &[ #( #field_lits ),* ],
2798 })
2799 });
2800
2801 let list_display_links = admin
2802 .list_display_links
2803 .as_ref()
2804 .map(|(v, _)| v.as_slice())
2805 .unwrap_or(&[]);
2806 let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2807
2808 let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2809 let actions_on_top = admin.actions_on_top.unwrap_or(true);
2810 let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2811 let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2812
2813 let prepopulated = admin
2814 .prepopulated_fields
2815 .as_ref()
2816 .map(|(v, _)| v.as_slice())
2817 .unwrap_or(&[]);
2818 let prepopulated_tokens = prepopulated.iter().map(|(target, sources)| {
2819 let target = target.as_str();
2820 let source_lits = sources.iter().map(|s| s.as_str());
2821 quote!(#root::core::PrepopulatedField {
2822 target: #target,
2823 sources: &[ #( #source_lits ),* ],
2824 })
2825 });
2826
2827 let raw_id_fields = admin
2828 .raw_id_fields
2829 .as_ref()
2830 .map(|(v, _)| v.as_slice())
2831 .unwrap_or(&[]);
2832 let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2833
2834 let autocomplete_fields = admin
2835 .autocomplete_fields
2836 .as_ref()
2837 .map(|(v, _)| v.as_slice())
2838 .unwrap_or(&[]);
2839 let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
2840
2841 // #352 — list_select_related accepts "all" | "none" | "field, field, …".
2842 let list_select_related_tokens = match admin.list_select_related.as_deref() {
2843 None | Some("all") => quote!(#root::core::ListSelectRelated::All),
2844 Some("none") => quote!(#root::core::ListSelectRelated::None),
2845 Some(raw) => {
2846 let names: Vec<&str> = raw
2847 .split(',')
2848 .map(str::trim)
2849 .filter(|s| !s.is_empty())
2850 .collect();
2851 quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
2852 }
2853 };
2854
2855 // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
2856 // a Vec<(String, String)>. Empty / unset → no overrides.
2857 let formfield_pairs: Vec<(&str, &str)> = admin
2858 .formfield_overrides
2859 .as_ref()
2860 .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
2861 .unwrap_or_default();
2862 let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
2863 let field = *field;
2864 let widget = *widget;
2865 quote!((#field, #widget))
2866 });
2867
2868 let list_per_page = admin.list_per_page.unwrap_or(0);
2869
2870 let ordering_pairs = admin
2871 .ordering
2872 .as_ref()
2873 .map(|(v, _)| v.as_slice())
2874 .unwrap_or(&[]);
2875 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
2876 let name = name.as_str();
2877 let desc = *desc;
2878 quote!((#name, #desc))
2879 });
2880
2881 quote! {
2882 ::core::option::Option::Some(&#root::core::AdminConfig {
2883 list_display: &[ #( #list_display_lits ),* ],
2884 search_fields: &[ #( #search_fields_lits ),* ],
2885 list_per_page: #list_per_page,
2886 ordering: &[ #( #ordering_tokens ),* ],
2887 readonly_fields: &[ #( #readonly_fields_lits ),* ],
2888 list_filter: &[ #( #list_filter_lits ),* ],
2889 actions: &[ #( #actions_lits ),* ],
2890 fieldsets: &[ #( #fieldset_tokens ),* ],
2891 list_display_links: &[ #( #list_display_links_lits ),* ],
2892 search_help_text: #search_help_text,
2893 actions_on_top: #actions_on_top,
2894 actions_on_bottom: #actions_on_bottom,
2895 date_hierarchy: #date_hierarchy,
2896 prepopulated_fields: &[ #( #prepopulated_tokens ),* ],
2897 raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
2898 autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
2899 list_select_related: #list_select_related_tokens,
2900 formfield_overrides: &[ #( #formfield_tokens ),* ],
2901 })
2902 }
2903}
2904
2905fn inherent_impl_tokens(
2906 struct_name: &syn::Ident,
2907 fields: &CollectedFields,
2908 primary_key: Option<&(syn::Ident, String)>,
2909 column_consts: &TokenStream2,
2910 audited_fields: Option<&[&ColumnEntry]>,
2911 indexes: &[IndexAttr],
2912 manager_fns: &[syn::Ident],
2913) -> TokenStream2 {
2914 let root = rustango_root();
2915 // Audit-emit fragments threaded into write paths. Non-empty only
2916 // when the model carries `#[rustango(audit(...))]`. They reborrow
2917 // `_executor` (a `&mut PgConnection` for audited models — the
2918 // macro switches the signature below) so the data write and the
2919 // audit INSERT both run on the same caller-supplied connection.
2920 let executor_passes_to_data_write = if audited_fields.is_some() {
2921 quote!(&mut *_executor)
2922 } else {
2923 quote!(_executor)
2924 };
2925 let executor_param = if audited_fields.is_some() {
2926 quote!(_executor: &mut #root::sql::sqlx::PgConnection)
2927 } else {
2928 quote!(_executor: _E)
2929 };
2930 let executor_generics = if audited_fields.is_some() {
2931 quote!()
2932 } else {
2933 quote!(<'_c, _E>)
2934 };
2935 let executor_where = if audited_fields.is_some() {
2936 quote!()
2937 } else {
2938 quote! {
2939 where
2940 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
2941 }
2942 };
2943 // For audited models the `_on` methods take `&mut PgConnection`, so
2944 // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
2945 // must acquire a connection first. Non-audited models keep the
2946 // direct delegation since `&PgPool` IS an Executor.
2947 let pool_to_save_on = if audited_fields.is_some() {
2948 quote! {
2949 let mut _conn = pool.acquire().await?;
2950 self.save_on(&mut *_conn).await
2951 }
2952 } else {
2953 quote!(self.save_on(pool).await)
2954 };
2955 let pool_to_insert_on = if audited_fields.is_some() {
2956 quote! {
2957 let mut _conn = pool.acquire().await?;
2958 self.insert_on(&mut *_conn).await
2959 }
2960 } else {
2961 quote!(self.insert_on(pool).await)
2962 };
2963 let pool_to_delete_on = if audited_fields.is_some() {
2964 quote! {
2965 let mut _conn = pool.acquire().await?;
2966 self.delete_on(&mut *_conn).await
2967 }
2968 } else {
2969 quote!(self.delete_on(pool).await)
2970 };
2971 let pool_to_bulk_insert_on = if audited_fields.is_some() {
2972 quote! {
2973 let mut _conn = pool.acquire().await?;
2974 Self::bulk_insert_on(rows, &mut *_conn).await
2975 }
2976 } else {
2977 quote!(Self::bulk_insert_on(rows, pool).await)
2978 };
2979 // Pre-existing bug surfaced by batch 22's first audited Auto<T>
2980 // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
2981 // directly, but `upsert_on` for audited models takes
2982 // `&mut PgConnection` (the audit emit needs a real connection).
2983 // Add the missing acquire shim to keep audited Auto-PK upsert
2984 // compiling.
2985 let pool_to_upsert_on = if audited_fields.is_some() {
2986 quote! {
2987 let mut _conn = pool.acquire().await?;
2988 self.upsert_on(&mut *_conn).await
2989 }
2990 } else {
2991 quote!(self.upsert_on(pool).await)
2992 };
2993
2994 // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
2995 // (audit-on-connection over &Pool needs a bi-dialect transaction
2996 // helper, deferred). Two body shapes:
2997 // - has_auto: build InsertQuery skipping Auto::Unset columns,
2998 // request Auto cols in `returning`, dispatch via
2999 // `insert_returning_pool`, then on the returned `PgRow` /
3000 // `MySqlAutoId(id)` enum — pull each Auto field from the PG
3001 // row OR drop the single i64 into the first Auto field on MySQL
3002 // (multi-Auto models on MySQL error at runtime since
3003 // `LAST_INSERT_ID()` only reports one)
3004 // - non-Auto: build InsertQuery with explicit columns/values and
3005 // call `insert_pool` (no returning needed)
3006 // pool_insert_method body for the audited Auto-PK case is moved
3007 // to after audit_pair_tokens / audit_pk_to_string (they live
3008 // ~150 lines below). This block keeps the non-audited and
3009 // non-Auto branches in place — the audited Auto-PK arm is
3010 // computed below and merged via the dispatch helper variable.
3011 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
3012 // Audited models with explicit (non-Auto) PKs go through
3013 // the non-Auto insert path below — the audit emit is one
3014 // round-trip after the INSERT inside the same tx via
3015 // audit::save_one_with_audit? No, INSERT semantics
3016 // differ. For non-Auto PK + audited, route through a
3017 // dedicated insert + audit emit on the same tx, but defer
3018 // the macro emission to the audit-bundle-aware block below
3019 // — this `quote!()` placeholder gets overwritten there.
3020 quote!()
3021 } else if audited_fields.is_some() && fields.has_auto {
3022 // Audited Auto-PK insert_pool — assembled after the audit
3023 // bundles. Placeholder; real emission below.
3024 quote!()
3025 } else if fields.has_auto {
3026 let pushes = &fields.insert_pushes;
3027 let returning_cols = &fields.returning_cols;
3028 // When every `Auto<T>` field is filled Rust-side
3029 // (`default_uuid_v7`, issue #823), there is no column to read
3030 // back from the database — `returning_cols` is empty. Route
3031 // through plain `insert_pool` instead of
3032 // `insert_returning_pool` to skip the redundant RETURNING /
3033 // LAST_INSERT_ID round-trip.
3034 if fields.returning_cols.is_empty() {
3035 quote! {
3036 /// Insert this row against either backend. Every
3037 /// `Auto<T>` PK on this model is filled Rust-side
3038 /// (e.g. `default_uuid_v7`) before binding, so no
3039 /// RETURNING round-trip is needed.
3040 ///
3041 /// # Errors
3042 /// As [`Self::insert`].
3043 pub async fn insert_pool(
3044 &mut self,
3045 pool: &#root::sql::Pool,
3046 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3047 let mut _columns: ::std::vec::Vec<&'static str> =
3048 ::std::vec::Vec::new();
3049 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3050 ::std::vec::Vec::new();
3051 #( #pushes )*
3052 let _query = #root::core::InsertQuery {
3053 model: <Self as #root::core::Model>::SCHEMA,
3054 columns: _columns,
3055 values: _values,
3056 returning: ::std::vec::Vec::new(),
3057 on_conflict: ::core::option::Option::None,
3058 };
3059 #root::sql::insert_pool(pool, &_query).await
3060 }
3061
3062 /// Eloquent `Model::insertOrIgnore()` — same shape
3063 /// as the auto-PK branch above. Returns `Ok(true)`
3064 /// when inserted, `Ok(false)` when a conflict caused
3065 /// the INSERT to silently skip.
3066 ///
3067 /// # Errors
3068 /// As [`Self::insert`].
3069 pub async fn insert_or_ignore(
3070 &mut self,
3071 pool: &#root::sql::Pool,
3072 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3073 let mut _columns: ::std::vec::Vec<&'static str> =
3074 ::std::vec::Vec::new();
3075 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3076 ::std::vec::Vec::new();
3077 #( #pushes )*
3078 let _query = #root::core::InsertQuery {
3079 model: <Self as #root::core::Model>::SCHEMA,
3080 columns: _columns,
3081 values: _values,
3082 returning: ::std::vec::Vec::new(),
3083 on_conflict: ::core::option::Option::Some(
3084 #root::core::ConflictClause::DoNothing,
3085 ),
3086 };
3087 let dialect = pool.dialect();
3088 let stmt = dialect.compile_insert(&_query)?;
3089 let rows = #root::sql::raw_execute_pool(
3090 pool, &stmt.sql, stmt.params,
3091 ).await?;
3092 ::core::result::Result::Ok(rows > 0)
3093 }
3094 }
3095 } else {
3096 quote! {
3097 /// Insert this row against either backend, populating any
3098 /// `Auto<T>` PK from the auto-assigned value.
3099 ///
3100 /// # Errors
3101 /// As [`Self::insert`].
3102 pub async fn insert_pool(
3103 &mut self,
3104 pool: &#root::sql::Pool,
3105 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3106 let mut _columns: ::std::vec::Vec<&'static str> =
3107 ::std::vec::Vec::new();
3108 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3109 ::std::vec::Vec::new();
3110 #( #pushes )*
3111 let _query = #root::core::InsertQuery {
3112 model: <Self as #root::core::Model>::SCHEMA,
3113 columns: _columns,
3114 values: _values,
3115 returning: ::std::vec![ #( #returning_cols ),* ],
3116 on_conflict: ::core::option::Option::None,
3117 };
3118 let _result = #root::sql::insert_returning_pool(
3119 pool, &_query,
3120 ).await?;
3121 #root::sql::apply_auto_pk(_result, self)
3122 }
3123
3124 /// Eloquent `Model::insertOrIgnore()` — INSERT this
3125 /// row or silently skip on unique-constraint
3126 /// violation. Returns `Ok(true)` when a row was
3127 /// inserted, `Ok(false)` when a conflict caused the
3128 /// INSERT to silently skip.
3129 ///
3130 /// **Caveat on auto-PK models**: when the row is
3131 /// skipped (conflict), this instance's `Auto<T>`
3132 /// fields stay `Unset` — no PK is back-populated
3133 /// because the server didn't auto-assign one. For
3134 /// "insert then read back the PK or the existing
3135 /// row's PK", use the `upsert` family or
3136 /// `get_or_create`.
3137 ///
3138 /// # Errors
3139 /// As [`Self::insert`].
3140 pub async fn insert_or_ignore(
3141 &mut self,
3142 pool: &#root::sql::Pool,
3143 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3144 let mut _columns: ::std::vec::Vec<&'static str> =
3145 ::std::vec::Vec::new();
3146 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3147 ::std::vec::Vec::new();
3148 #( #pushes )*
3149 let _query = #root::core::InsertQuery {
3150 model: <Self as #root::core::Model>::SCHEMA,
3151 columns: _columns,
3152 values: _values,
3153 returning: ::std::vec::Vec::new(),
3154 on_conflict: ::core::option::Option::Some(
3155 #root::core::ConflictClause::DoNothing,
3156 ),
3157 };
3158 let dialect = pool.dialect();
3159 let stmt = dialect.compile_insert(&_query)?;
3160 let rows = #root::sql::raw_execute_pool(
3161 pool, &stmt.sql, stmt.params,
3162 ).await?;
3163 ::core::result::Result::Ok(rows > 0)
3164 }
3165 }
3166 }
3167 } else {
3168 let insert_columns = &fields.insert_columns;
3169 let insert_values = &fields.insert_values;
3170 quote! {
3171 /// Insert this row into its table against either backend.
3172 /// Equivalent to [`Self::insert`] but takes
3173 /// [`#root::sql::Pool`].
3174 ///
3175 /// # Errors
3176 /// As [`Self::insert`].
3177 pub async fn insert_pool(
3178 &self,
3179 pool: &#root::sql::Pool,
3180 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3181 let _query = #root::core::InsertQuery {
3182 model: <Self as #root::core::Model>::SCHEMA,
3183 columns: ::std::vec![ #( #insert_columns ),* ],
3184 values: ::std::vec![ #( #insert_values ),* ],
3185 returning: ::std::vec::Vec::new(),
3186 on_conflict: ::core::option::Option::None,
3187 };
3188 #root::sql::insert_pool(pool, &_query).await
3189 }
3190
3191 /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3192 /// or silently skip on unique-constraint violation. Maps
3193 /// to per-dialect "INSERT ... DO NOTHING on conflict":
3194 /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3195 /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL
3196 /// `INSERT IGNORE INTO …`.
3197 ///
3198 /// Returns `Ok(true)` when a row was inserted,
3199 /// `Ok(false)` when a conflict caused the INSERT to
3200 /// silently skip.
3201 ///
3202 /// Use for "create-if-absent" patterns where you don't
3203 /// need the row back. For "find-or-create with the row
3204 /// returned", use the queryset-level
3205 /// `crate::sql::get_or_create` free function.
3206 ///
3207 /// # Errors
3208 /// As [`Self::insert`], plus any dialect-specific
3209 /// translation error from the ConflictClause writer.
3210 pub async fn insert_or_ignore(
3211 &self,
3212 pool: &#root::sql::Pool,
3213 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3214 let _query = #root::core::InsertQuery {
3215 model: <Self as #root::core::Model>::SCHEMA,
3216 columns: ::std::vec![ #( #insert_columns ),* ],
3217 values: ::std::vec![ #( #insert_values ),* ],
3218 returning: ::std::vec::Vec::new(),
3219 on_conflict: ::core::option::Option::Some(
3220 #root::core::ConflictClause::DoNothing,
3221 ),
3222 };
3223 let dialect = pool.dialect();
3224 let stmt = dialect.compile_insert(&_query)?;
3225 let rows = #root::sql::raw_execute_pool(pool, &stmt.sql, stmt.params).await?;
3226 ::core::result::Result::Ok(rows > 0)
3227 }
3228 }
3229 };
3230
3231 // pool_save_method moved to after audit_pair_tokens /
3232 // audit_pk_to_string (they live ~70 lines below) — needed for
3233 // the audited branch which builds an UpdateQuery + PendingEntry
3234 // and dispatches via audit::save_one_with_audit.
3235
3236 // pool_delete_method moved to after audit_pair_tokens / audit_pk_to_string
3237 // are computed (they live ~80 lines below).
3238
3239 // Build the (column, JSON value) pair list used by every
3240 // snapshot-style audit emission. Reused across delete_on,
3241 // soft_delete_on, restore_on, and (later) bulk paths. Empty
3242 // when the model isn't audited.
3243 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3244 .map(|tracked| {
3245 tracked
3246 .iter()
3247 .map(|c| {
3248 let column_lit = c.column.as_str();
3249 let ident = &c.ident;
3250 quote! {
3251 (
3252 #column_lit,
3253 #root::__serde_json::to_value(&self.#ident)
3254 .unwrap_or(#root::__serde_json::Value::Null),
3255 )
3256 }
3257 })
3258 .collect()
3259 })
3260 .unwrap_or_default();
3261 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3262 if fields.pk_is_auto {
3263 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3264 } else {
3265 quote!(::std::format!("{}", &self.#pk_ident))
3266 }
3267 } else {
3268 quote!(::std::string::String::new())
3269 };
3270 let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
3271 if audited_fields.is_some() {
3272 let pairs = audit_pair_tokens.iter();
3273 let pk_str = audit_pk_to_string.clone();
3274 quote! {
3275 let _audit_entry = #root::audit::PendingEntry {
3276 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3277 entity_pk: #pk_str,
3278 operation: #op_path,
3279 source: #root::audit::current_source(),
3280 changes: #root::audit::snapshot_changes(&[
3281 #( #pairs ),*
3282 ]),
3283 };
3284 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3285 }
3286 } else {
3287 quote!()
3288 }
3289 };
3290 let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create));
3291 let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete));
3292 let audit_softdelete_emit = make_op_emit(quote!(#root::audit::AuditOp::SoftDelete));
3293 let audit_restore_emit = make_op_emit(quote!(#root::audit::AuditOp::Restore));
3294
3295 // `save_pool(&Pool)` — emitted for every model with a PK.
3296 // Audited Auto-PK models are deferred (the Auto::Unset →
3297 // insert_pool path needs the audited-insert flow from a future
3298 // batch). Three body shapes:
3299 // - non-audited, plain PK: build UpdateQuery + dispatch through
3300 // sql::update_pool
3301 // - non-audited, Auto-PK: same, but Auto::Unset routes to
3302 // self.insert_pool which already handles RETURNING / LAST_INSERT_ID
3303 // - audited, plain PK: build UpdateQuery + PendingEntry, dispatch
3304 // through audit::save_one_with_audit (per-backend tx wraps
3305 // UPDATE + audit emit atomically). Snapshot-style audit (post-
3306 // write field values) — diff-style audit (with pre-UPDATE
3307 // SELECT for `before` values) needs per-tracked-column codegen
3308 // that doesn't fit the runtime-helper pattern; legacy &PgPool
3309 // `save` keeps the diff for now.
3310 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3311 let pk_column_lit = pk_col.as_str();
3312 let assignments = &fields.update_assignments;
3313 if audited_fields.is_some() {
3314 if fields.pk_is_auto {
3315 // Auto-PK + audited: defer. The Auto::Unset insert
3316 // path needs a transactional INSERT + LAST_INSERT_ID
3317 // + audit emit flow — that's a follow-up batch.
3318 quote!()
3319 } else {
3320 let pairs = audit_pair_tokens.iter();
3321 let pairs2 = audit_pair_tokens.iter();
3322 let pk_str = audit_pk_to_string.clone();
3323 let pk_str2 = audit_pk_to_string.clone();
3324 quote! {
3325 /// Save (UPDATE) this row against either backend
3326 /// with audit emission inside the same transaction.
3327 /// Bi-dialect counterpart of [`Self::save`] for
3328 /// audited models with non-`Auto<T>` PKs.
3329 ///
3330 /// Captures **post-write** field state (snapshot
3331 /// audit). The legacy &PgPool [`Self::save`]
3332 /// captures BEFORE+AFTER for true diff audit;
3333 /// porting that to the &Pool path needs runtime
3334 /// per-tracked-column decoding and is deferred.
3335 ///
3336 /// # Errors
3337 /// As [`Self::save`].
3338 pub async fn save_pool(
3339 &mut self,
3340 pool: &#root::sql::Pool,
3341 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3342 let _query = #root::core::UpdateQuery {
3343 model: <Self as #root::core::Model>::SCHEMA,
3344 set: ::std::vec![ #( #assignments ),* ],
3345 where_clause: #root::core::WhereExpr::Predicate(
3346 #root::core::Filter {
3347 column: #pk_column_lit,
3348 op: #root::core::Op::Eq,
3349 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3350 ::core::clone::Clone::clone(&self.#pk_ident)
3351 ),
3352 }
3353 ),
3354 };
3355 let _audit_entry = #root::audit::PendingEntry {
3356 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3357 entity_pk: #pk_str,
3358 operation: #root::audit::AuditOp::Update,
3359 source: #root::audit::current_source(),
3360 changes: #root::audit::snapshot_changes(&[
3361 #( #pairs ),*
3362 ]),
3363 };
3364 let _affected = #root::audit::save_one_with_audit(
3365 pool, &_query, &_audit_entry,
3366 ).await?;
3367 ::core::result::Result::Ok(_affected)
3368 }
3369
3370 /// `save_pool` narrowed to a Rust-field allowlist — issue #66.
3371 /// Audit emission shrinks to the same column set so
3372 /// the audit log reflects exactly what was written.
3373 ///
3374 /// # Errors
3375 /// As [`Self::save_pool`], plus
3376 /// [`#root::core::QueryError::UnknownField`] wrapped
3377 /// in `ExecError::Query` for unknown field names.
3378 pub async fn save_partial(
3379 &mut self,
3380 fields: &[&str],
3381 pool: &#root::sql::Pool,
3382 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3383 if fields.is_empty() {
3384 #root::__tracing::warn!(
3385 target: "rustango::save_partial",
3386 model = <Self as #root::core::Model>::SCHEMA.name,
3387 "save_partial called with empty field list — no-op"
3388 );
3389 return ::core::result::Result::Ok(0);
3390 }
3391 let _schema = <Self as #root::core::Model>::SCHEMA;
3392 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3393 ::std::collections::HashSet::with_capacity(fields.len());
3394 for f in fields {
3395 match _schema.field(f) {
3396 ::core::option::Option::Some(fs) => {
3397 _wanted_cols.insert(fs.column);
3398 }
3399 ::core::option::Option::None => {
3400 return ::core::result::Result::Err(
3401 #root::sql::ExecError::Query(
3402 #root::core::QueryError::UnknownField {
3403 model: _schema.name,
3404 field: (*f).to_owned(),
3405 }
3406 )
3407 );
3408 }
3409 }
3410 }
3411 let _full: ::std::vec::Vec<#root::core::Assignment> =
3412 ::std::vec![ #( #assignments ),* ];
3413 let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3414 .into_iter()
3415 .filter(|a| _wanted_cols.contains(a.column))
3416 .collect();
3417 if _filtered.is_empty() {
3418 #root::__tracing::warn!(
3419 target: "rustango::save_partial",
3420 model = _schema.name,
3421 "save_partial: every named field maps to a non-assignable column — no-op"
3422 );
3423 return ::core::result::Result::Ok(0);
3424 }
3425 let _query = #root::core::UpdateQuery {
3426 model: _schema,
3427 set: _filtered,
3428 where_clause: #root::core::WhereExpr::Predicate(
3429 #root::core::Filter {
3430 column: #pk_column_lit,
3431 op: #root::core::Op::Eq,
3432 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3433 ::core::clone::Clone::clone(&self.#pk_ident)
3434 ),
3435 }
3436 ),
3437 };
3438 // Narrow the audit snapshot to the same column set.
3439 let _all_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3440 ::std::vec![ #( #pairs2 ),* ];
3441 let _narrowed: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3442 _all_pairs
3443 .into_iter()
3444 .filter(|(col, _)| _wanted_cols.contains(col))
3445 .collect();
3446 let _audit_entry = #root::audit::PendingEntry {
3447 entity_table: _schema.table,
3448 entity_pk: #pk_str2,
3449 operation: #root::audit::AuditOp::Update,
3450 source: #root::audit::current_source(),
3451 changes: #root::audit::snapshot_changes(&_narrowed),
3452 };
3453 let _affected = #root::audit::save_one_with_audit(
3454 pool, &_query, &_audit_entry,
3455 ).await?;
3456 ::core::result::Result::Ok(_affected)
3457 }
3458
3459 /// Typed-column counterpart of [`Self::save_partial`] —
3460 /// issue #67. `fields` is a tuple of [`Column`]
3461 /// constants whose `Model` matches `Self`; typos and
3462 /// model mismatches surface at *compile time*
3463 /// (`Author::name` inside a `Post::save_partial_typed`
3464 /// call is a type error, no runtime check).
3465 ///
3466 /// ```ignore
3467 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3468 /// ```
3469 ///
3470 /// Lowers to [`Self::save_partial`] under the hood;
3471 /// audit narrowing + every other semantic is identical.
3472 ///
3473 /// [`Column`]: #root::core::Column
3474 ///
3475 /// # Errors
3476 /// As [`Self::save_partial`].
3477 pub async fn save_partial_typed<
3478 L: #root::core::TypedFieldList<Self>,
3479 >(
3480 &mut self,
3481 fields: L,
3482 pool: &#root::sql::Pool,
3483 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3484 let _names = fields.rust_field_names();
3485 let _refs: ::std::vec::Vec<&str> =
3486 _names.iter().copied().collect();
3487 self.save_partial(&_refs, pool).await
3488 }
3489 }
3490 }
3491 } else {
3492 let dispatch_unset = if fields.pk_is_auto {
3493 quote! {
3494 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3495 return self.insert_pool(pool).await.map(|()| 1u64);
3496 }
3497 }
3498 } else {
3499 quote!()
3500 };
3501 quote! {
3502 /// Save this row to its table against either backend.
3503 /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3504 /// `UPDATE` keyed on the PK.
3505 ///
3506 /// # Errors
3507 /// As [`Self::save`].
3508 pub async fn save_pool(
3509 &mut self,
3510 pool: &#root::sql::Pool,
3511 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3512 #dispatch_unset
3513 let _query = #root::core::UpdateQuery {
3514 model: <Self as #root::core::Model>::SCHEMA,
3515 set: ::std::vec![ #( #assignments ),* ],
3516 where_clause: #root::core::WhereExpr::Predicate(
3517 #root::core::Filter {
3518 column: #pk_column_lit,
3519 op: #root::core::Op::Eq,
3520 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3521 ::core::clone::Clone::clone(&self.#pk_ident)
3522 ),
3523 }
3524 ),
3525 };
3526 let _affected = #root::sql::update_pool(pool, &_query).await?;
3527 ::core::result::Result::Ok(_affected)
3528 }
3529
3530 /// Save (UPDATE) only the listed Rust-side fields,
3531 /// leaving every other column untouched. Issue #66.
3532 ///
3533 /// `fields` are Rust-side struct field names; the macro
3534 /// resolves each to its SQL column. Unknown field
3535 /// names return [`#root::core::QueryError::UnknownField`]
3536 /// wrapped in `ExecError::Query`. An empty list is a
3537 /// no-op (returns `Ok(())` and logs a `tracing::warn!`).
3538 ///
3539 /// Use this when:
3540 /// * you only mutated a couple of fields on a wide row
3541 /// (avoid re-writing every column on every save), or
3542 /// * two writers diverged after their initial read and
3543 /// you want to preserve the other writer's changes to
3544 /// columns you didn't touch.
3545 ///
3546 /// Auto-PK models with an unset PK return
3547 /// [`#root::core::QueryError::UnknownField`] with
3548 /// field name `<pk>` — `save_partial` is an
3549 /// UPDATE-only path. Call [`Self::insert_pool`]
3550 /// (or [`Self::save_pool`] which dispatches based on
3551 /// PK state) for the INSERT case.
3552 ///
3553 /// # Errors
3554 /// As [`Self::save_pool`], plus `UnknownField` for
3555 /// unknown / empty / Auto-Unset cases.
3556 pub async fn save_partial(
3557 &mut self,
3558 fields: &[&str],
3559 pool: &#root::sql::Pool,
3560 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3561 if fields.is_empty() {
3562 #root::__tracing::warn!(
3563 target: "rustango::save_partial",
3564 model = <Self as #root::core::Model>::SCHEMA.name,
3565 "save_partial called with empty field list — no-op"
3566 );
3567 return ::core::result::Result::Ok(0);
3568 }
3569 let _schema = <Self as #root::core::Model>::SCHEMA;
3570 // Validate field names against the schema.
3571 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3572 ::std::collections::HashSet::with_capacity(fields.len());
3573 for f in fields {
3574 match _schema.field(f) {
3575 ::core::option::Option::Some(fs) => {
3576 _wanted_cols.insert(fs.column);
3577 }
3578 ::core::option::Option::None => {
3579 return ::core::result::Result::Err(
3580 #root::sql::ExecError::Query(
3581 #root::core::QueryError::UnknownField {
3582 model: _schema.name,
3583 field: (*f).to_owned(),
3584 }
3585 )
3586 );
3587 }
3588 }
3589 }
3590 // Build the full assignment vec, then keep only the
3591 // assignments whose column is in `_wanted_cols`.
3592 let _full: ::std::vec::Vec<#root::core::Assignment> =
3593 ::std::vec![ #( #assignments ),* ];
3594 let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3595 .into_iter()
3596 .filter(|a| _wanted_cols.contains(a.column))
3597 .collect();
3598 if _filtered.is_empty() {
3599 // All field names valid, but they all map to
3600 // non-assignable slots (PK column, computed/
3601 // virtual fields, relations without an
3602 // assignment). Same no-op as an empty list.
3603 #root::__tracing::warn!(
3604 target: "rustango::save_partial",
3605 model = _schema.name,
3606 "save_partial: every named field maps to a non-assignable column — no-op"
3607 );
3608 return ::core::result::Result::Ok(0);
3609 }
3610 let _query = #root::core::UpdateQuery {
3611 model: _schema,
3612 set: _filtered,
3613 where_clause: #root::core::WhereExpr::Predicate(
3614 #root::core::Filter {
3615 column: #pk_column_lit,
3616 op: #root::core::Op::Eq,
3617 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3618 ::core::clone::Clone::clone(&self.#pk_ident)
3619 ),
3620 }
3621 ),
3622 };
3623 let _affected = #root::sql::update_pool(pool, &_query).await?;
3624 ::core::result::Result::Ok(_affected)
3625 }
3626
3627 /// Typed-column counterpart of [`Self::save_partial`] —
3628 /// issue #67. `fields` is a tuple of [`Column`]
3629 /// constants whose `Model` matches `Self`; typos and
3630 /// model mismatches surface at *compile time*
3631 /// (`Author::name` inside a `Post::save_partial_typed`
3632 /// call is a type error, no runtime check).
3633 ///
3634 /// ```ignore
3635 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3636 /// ```
3637 ///
3638 /// Lowers to [`Self::save_partial`] under the hood — the
3639 /// tuple is reduced to a `&[&str]` slice of Rust-side
3640 /// field names and forwarded.
3641 ///
3642 /// [`Column`]: #root::core::Column
3643 ///
3644 /// # Errors
3645 /// As [`Self::save_partial`].
3646 pub async fn save_partial_typed<
3647 L: #root::core::TypedFieldList<Self>,
3648 >(
3649 &mut self,
3650 fields: L,
3651 pool: &#root::sql::Pool,
3652 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3653 let _names = fields.rust_field_names();
3654 let _refs: ::std::vec::Vec<&str> =
3655 _names.iter().copied().collect();
3656 self.save_partial(&_refs, pool).await
3657 }
3658 }
3659 }
3660 } else {
3661 quote!()
3662 };
3663
3664 // Audited `insert_pool` (overrides the placeholder set higher up
3665 // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3666 // audited models get insert_pool routing through
3667 // audit::insert_one_with_audit (per-backend tx wraps INSERT
3668 // + auto-PK readback + audit emit). Snapshot-style audit (the
3669 // PendingEntry's `changes` carries post-write field values).
3670 let pool_insert_method = if audited_fields.is_some() {
3671 if let Some(_) = primary_key {
3672 let pushes = if fields.has_auto {
3673 fields.insert_pushes.clone()
3674 } else {
3675 // For non-Auto-PK models, the macro normally builds
3676 // {columns, values} from fields.insert_columns +
3677 // fields.insert_values rather than insert_pushes.
3678 // Map those into the pushes shape.
3679 fields
3680 .insert_columns
3681 .iter()
3682 .zip(&fields.insert_values)
3683 .map(|(col, val)| {
3684 quote! {
3685 _columns.push(#col);
3686 _values.push(#val);
3687 }
3688 })
3689 .collect()
3690 };
3691 let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
3692 fields.returning_cols.clone()
3693 } else {
3694 // Non-Auto-PK: still need RETURNING something for the
3695 // audit helper's contract (it errors on empty
3696 // returning). Return the PK column so the audit row
3697 // can carry the assigned PK back. Some non-Auto PKs
3698 // are server-side-default (e.g. UUIDv4 default), so
3699 // RETURNING is genuinely useful.
3700 primary_key
3701 .map(|(_, col)| {
3702 let lit = col.as_str();
3703 vec![quote!(#lit)]
3704 })
3705 .unwrap_or_default()
3706 };
3707 let pairs = audit_pair_tokens.iter();
3708 let pk_str = audit_pk_to_string.clone();
3709 quote! {
3710 /// Insert this row against either backend with audit
3711 /// emission inside the same transaction. Bi-dialect
3712 /// counterpart of [`Self::insert`] for audited models.
3713 ///
3714 /// Snapshot-style audit (post-write field values).
3715 ///
3716 /// # Errors
3717 /// As [`Self::insert`].
3718 pub async fn insert_pool(
3719 &mut self,
3720 pool: &#root::sql::Pool,
3721 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3722 let mut _columns: ::std::vec::Vec<&'static str> =
3723 ::std::vec::Vec::new();
3724 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3725 ::std::vec::Vec::new();
3726 #( #pushes )*
3727 let _query = #root::core::InsertQuery {
3728 model: <Self as #root::core::Model>::SCHEMA,
3729 columns: _columns,
3730 values: _values,
3731 returning: ::std::vec![ #( #returning_cols ),* ],
3732 on_conflict: ::core::option::Option::None,
3733 };
3734 let _audit_entry = #root::audit::PendingEntry {
3735 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3736 entity_pk: #pk_str,
3737 operation: #root::audit::AuditOp::Create,
3738 source: #root::audit::current_source(),
3739 changes: #root::audit::snapshot_changes(&[
3740 #( #pairs ),*
3741 ]),
3742 };
3743 let _result = #root::audit::insert_one_with_audit(
3744 pool, &_query, &_audit_entry,
3745 ).await?;
3746 #root::sql::apply_auto_pk(_result, self)
3747 }
3748 }
3749 } else {
3750 quote!()
3751 }
3752 } else {
3753 // Keep the non-audited pool_insert_method we built earlier.
3754 pool_insert_method
3755 };
3756
3757 // Update audited save_pool: now that insert_pool is wired for
3758 // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3759 // insert_pool. Non-audited save_pool already does this.
3760 // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3761 // Replaces the snapshot-only emission with a per-backend transaction
3762 // body that:
3763 // 1. SELECTs the tracked columns by PK (typed Row::try_get per
3764 // column), capturing BEFORE values
3765 // 2. compiles the UPDATE via pool.dialect() and runs it on the tx
3766 // 3. builds AFTER pairs from &self
3767 // 4. diffs BEFORE/AFTER, emits one PendingEntry with
3768 // AuditOp::Update + diff_changes(...) on the same tx connection
3769 // 5. commits
3770 //
3771 // Per-backend arms inline the SQL string + placeholder shape, then
3772 // share the `audit_before_pair_tokens` decoder block (Row::try_get
3773 // is polymorphic over Row type — the same tokens work against
3774 // PgRow and MySqlRow as long as the field's Rust type implements
3775 // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3776 // primitives + chrono/uuid/serde_json::Value all do).
3777 let pool_save_method = if let Some(tracked) = audited_fields {
3778 if let Some((pk_ident, pk_col)) = primary_key {
3779 let pk_column_lit = pk_col.as_str();
3780 // Two iterators — quote!'s `#(#var)*` consumes the
3781 // iterator, and we need to splice the same after-pairs
3782 // sequence into both per-backend arms.
3783 let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
3784 let pk_str = audit_pk_to_string.clone();
3785 // Per-tracked-column BEFORE-pair token list. Each entry
3786 // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3787 // The Row alias resolves to PgRow / MySqlRow per call site,
3788 // so the same template generates both the PG and MySQL bodies.
3789 let mk_before_pairs =
3790 |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
3791 tracked
3792 .iter()
3793 .map(|c| {
3794 let column_lit = c.column.as_str();
3795 let value_ty = &c.value_ty;
3796 quote! {
3797 (
3798 #column_lit,
3799 match #getter::<#value_ty>(
3800 _audit_before_row, #column_lit,
3801 ) {
3802 ::core::result::Result::Ok(v) => {
3803 #root::__serde_json::to_value(&v)
3804 .unwrap_or(#root::__serde_json::Value::Null)
3805 }
3806 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3807 },
3808 )
3809 }
3810 })
3811 .collect()
3812 };
3813 let before_pairs_pg: Vec<proc_macro2::TokenStream> =
3814 mk_before_pairs(quote!(#root::sql::try_get_returning));
3815 let before_pairs_my: Vec<proc_macro2::TokenStream> =
3816 mk_before_pairs(quote!(#root::sql::try_get_returning_my));
3817 let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
3818 mk_before_pairs(quote!(#root::sql::try_get_returning_sqlite));
3819 let pg_select_cols: String = tracked
3820 .iter()
3821 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
3822 .collect::<Vec<_>>()
3823 .join(", ");
3824 let my_select_cols: String = tracked
3825 .iter()
3826 .map(|c| format!("`{}`", c.column.replace('`', "``")))
3827 .collect::<Vec<_>>()
3828 .join(", ");
3829 // SQLite uses double-quote identifier quoting (same as
3830 // Postgres in default config), so the column-list shape
3831 // matches PG.
3832 let sqlite_select_cols: String = pg_select_cols.clone();
3833 let pk_value_for_bind = if fields.pk_is_auto {
3834 quote!(self.#pk_ident.get().copied().unwrap_or_default())
3835 } else {
3836 quote!(::core::clone::Clone::clone(&self.#pk_ident))
3837 };
3838 let assignments = &fields.update_assignments;
3839 let unset_dispatch = if fields.has_auto {
3840 quote! {
3841 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3842 return self.insert_pool(pool).await.map(|()| 1u64);
3843 }
3844 }
3845 } else {
3846 quote!()
3847 };
3848 quote! {
3849 /// Save this row against either backend with audit
3850 /// emission (diff-style: BEFORE+AFTER) inside the
3851 /// same transaction. Auto::Unset PK routes to
3852 /// insert_pool. Bi-dialect counterpart of
3853 /// [`Self::save`] for audited models.
3854 ///
3855 /// The audit row's `changes` JSON contains one
3856 /// `{ "field": { "before": …, "after": … } }` entry
3857 /// per tracked column whose value actually changed
3858 /// — same shape as the existing &PgPool save() emits.
3859 ///
3860 /// # Errors
3861 /// As [`Self::save`].
3862 pub async fn save_pool(
3863 &mut self,
3864 pool: &#root::sql::Pool,
3865 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3866 #unset_dispatch
3867 let _query = #root::core::UpdateQuery {
3868 model: <Self as #root::core::Model>::SCHEMA,
3869 set: ::std::vec![ #( #assignments ),* ],
3870 where_clause: #root::core::WhereExpr::Predicate(
3871 #root::core::Filter {
3872 column: #pk_column_lit,
3873 op: #root::core::Op::Eq,
3874 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3875 ::core::clone::Clone::clone(&self.#pk_ident)
3876 ),
3877 }
3878 ),
3879 };
3880 let _after_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3881 ::std::vec![ #( #after_pairs_pg ),* ];
3882 #root::audit::save_one_with_diff(
3883 pool,
3884 &_query,
3885 #pk_column_lit,
3886 ::core::convert::Into::<#root::core::SqlValue>::into(
3887 #pk_value_for_bind,
3888 ),
3889 <Self as #root::core::Model>::SCHEMA.table,
3890 #pk_str,
3891 _after_pairs,
3892 #pg_select_cols,
3893 #my_select_cols,
3894 #sqlite_select_cols,
3895 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
3896 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
3897 |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
3898 ).await
3899 }
3900 }
3901 } else {
3902 quote!()
3903 }
3904 } else {
3905 pool_save_method
3906 };
3907
3908 // `delete_pool(&Pool)` — emitted for every model with a PK. Two
3909 // body shapes:
3910 // - non-audited: simple dispatch through `sql::delete_pool`
3911 // - audited: routes through `audit::delete_one_with_audit`,
3912 // which opens a per-backend transaction wrapping DELETE +
3913 // audit emit so the data write and audit row commit atomically.
3914 let pool_delete_method = {
3915 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
3916 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
3917 if let Some(pk_ident) = pk_ident_for_pool {
3918 if audited_fields.is_some() {
3919 let pairs = audit_pair_tokens.iter();
3920 let pk_str = audit_pk_to_string.clone();
3921 quote! {
3922 /// Delete this row against either backend with audit
3923 /// emission inside the same transaction. Bi-dialect
3924 /// counterpart of [`Self::delete`] for audited models.
3925 ///
3926 /// # Errors
3927 /// As [`Self::delete`].
3928 pub async fn delete_pool(
3929 &self,
3930 pool: &#root::sql::Pool,
3931 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3932 let _query = #root::core::DeleteQuery {
3933 model: <Self as #root::core::Model>::SCHEMA,
3934 where_clause: #root::core::WhereExpr::Predicate(
3935 #root::core::Filter {
3936 column: #pk_column_lit,
3937 op: #root::core::Op::Eq,
3938 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3939 ::core::clone::Clone::clone(&self.#pk_ident)
3940 ),
3941 }
3942 ),
3943 };
3944 let _audit_entry = #root::audit::PendingEntry {
3945 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3946 entity_pk: #pk_str,
3947 operation: #root::audit::AuditOp::Delete,
3948 source: #root::audit::current_source(),
3949 changes: #root::audit::snapshot_changes(&[
3950 #( #pairs ),*
3951 ]),
3952 };
3953 #root::audit::delete_one_with_audit(
3954 pool, &_query, &_audit_entry,
3955 ).await
3956 }
3957 }
3958 } else {
3959 quote! {
3960 /// Delete the row identified by this instance's primary key
3961 /// against either backend. Equivalent to [`Self::delete`] but
3962 /// takes [`#root::sql::Pool`] and dispatches per backend.
3963 ///
3964 /// # Errors
3965 /// As [`Self::delete`].
3966 pub async fn delete_pool(
3967 &self,
3968 pool: &#root::sql::Pool,
3969 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3970 let _query = #root::core::DeleteQuery {
3971 model: <Self as #root::core::Model>::SCHEMA,
3972 where_clause: #root::core::WhereExpr::Predicate(
3973 #root::core::Filter {
3974 column: #pk_column_lit,
3975 op: #root::core::Op::Eq,
3976 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3977 ::core::clone::Clone::clone(&self.#pk_ident)
3978 ),
3979 }
3980 ),
3981 };
3982 #root::sql::delete_pool(pool, &_query).await
3983 }
3984 }
3985 }
3986 } else {
3987 quote!()
3988 }
3989 };
3990
3991 // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
3992 // matching this instance's PK and overwrite the in-memory state
3993 // with the freshly-fetched columns.
3994 // Issue #825. Only emitted when the model declares a PK; non-PK
3995 // models can't address a specific row.
3996 //
3997 // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
3998 // every field from `self`; resets the PK to `Auto::Unset` when
3999 // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
4000 // a fresh autoincrement value. Non-Auto PKs preserve the source
4001 // PK — the caller must overwrite before insert. Pure-Rust, no
4002 // I/O, no dialect surface.
4003 let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
4004 let other_field_clones: Vec<TokenStream2> = fields
4005 .column_entries
4006 .iter()
4007 .filter(|c| &c.ident != pk_ident)
4008 .map(|c| {
4009 let ident = &c.ident;
4010 quote! {
4011 #ident: ::core::clone::Clone::clone(&self.#ident)
4012 }
4013 })
4014 .collect();
4015 let pk_clone_token = if fields.pk_is_auto {
4016 quote! { #pk_ident: #root::sql::Auto::Unset }
4017 } else {
4018 quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
4019 };
4020 let replicate_doc = if fields.pk_is_auto {
4021 quote! {
4022 /// Eloquent-style `replicate()` — return a clone of this
4023 /// row with the primary key reset to [`Auto::Unset`] so
4024 /// the copy is ready for `insert_pool` / `save_pool` to
4025 /// allocate a fresh autoincrement value. Every other
4026 /// field is `Clone`d verbatim. Issue #825.
4027 ///
4028 /// `auto_now_add` / `auto_now` timestamp fields are
4029 /// **not** reset (Eloquent's `replicate` doesn't reset
4030 /// them either) — pass them through the normal insert
4031 /// path if you want fresh values, or assign them
4032 /// explicitly after the call.
4033 }
4034 } else {
4035 quote! {
4036 /// Eloquent-style `replicate()` — clone this row
4037 /// verbatim. Because the primary key is **not** an
4038 /// `Auto<T>`, the clone keeps the source PK; the
4039 /// caller must overwrite `copy.<pk>` before inserting
4040 /// to avoid a unique-key violation. Issue #825.
4041 }
4042 };
4043 // 2026-06-07 — field-name / shortcut collision guard.
4044 //
4045 // The macro emits both `pub const <field>: <field>_col = ...`
4046 // (per-field typed-column const, used by the typed-builder
4047 // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
4048 // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
4049 // model has a field named e.g. `count`, both items would land
4050 // in the same inherent impl with the same name, and the
4051 // compiler rejects the derive with "duplicate definitions".
4052 //
4053 // Drop the conflicting shortcut for that model. Callers can
4054 // still reach the same behavior via
4055 // `QuerySet::<Model>::default().count(&pool)`.
4056 let column_names: ::std::collections::HashSet<String> = fields
4057 .column_entries
4058 .iter()
4059 .map(|c| c.ident.to_string())
4060 .collect();
4061 let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
4062 if column_names.contains(name) {
4063 quote! {}
4064 } else {
4065 tokens
4066 }
4067 };
4068 let count_method = emit_if_no_field_collision(
4069 "count",
4070 quote! {
4071 /// Count rows of this model — `SELECT COUNT(*) FROM
4072 /// <table>`. Eloquent `Model::count()` parity.
4073 ///
4074 /// Skipped on models that already declare a field named
4075 /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
4076 /// in that case.
4077 ///
4078 /// # Errors
4079 /// As [`CounterPool::count`].
4080 ///
4081 /// [`CounterPool::count`]: rustango::sql::CounterPool::count
4082 pub async fn count(
4083 pool: &#root::sql::Pool,
4084 ) -> ::core::result::Result<i64, #root::sql::ExecError> {
4085 use #root::sql::CounterPool as _;
4086 #root::query::QuerySet::<Self>::default()
4087 .count(pool)
4088 .await
4089 }
4090 },
4091 );
4092 let value_method = emit_if_no_field_collision(
4093 "value",
4094 quote! {
4095 /// Pluck a single scalar from the first row.
4096 /// Eloquent `Model::query()->value($col)` parity.
4097 ///
4098 /// Skipped on models that already declare a field named
4099 /// `value`. Drop into
4100 /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4101 ///
4102 /// # Errors
4103 /// As `ValuesFlatQuerySet::first`.
4104 pub async fn value<U>(
4105 col: &str,
4106 pool: &#root::sql::Pool,
4107 ) -> ::core::result::Result<
4108 ::core::option::Option<U>,
4109 #root::sql::ExecError,
4110 >
4111 where
4112 U: #root::sql::MaybePgScalar
4113 + #root::sql::MaybeMyScalar
4114 + #root::sql::MaybeSqliteScalar
4115 + ::core::marker::Send
4116 + ::core::marker::Unpin,
4117 {
4118 let _col_static: &'static str = Self::__resolve_col(col)?;
4119 #root::query::QuerySet::<Self>::default()
4120 .values_list_flat(_col_static)
4121 .first::<U>(pool)
4122 .await
4123 }
4124 },
4125 );
4126 let sum_method = emit_if_no_field_collision(
4127 "sum",
4128 quote! {
4129 /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4130 /// Skipped on models that already declare a field named
4131 /// `sum`.
4132 ///
4133 /// # Errors
4134 /// As [`#root::sql::fetch_aggregate_pool`].
4135 pub async fn sum<U>(
4136 col: &str,
4137 pool: &#root::sql::Pool,
4138 ) -> ::core::result::Result<
4139 ::core::option::Option<U>,
4140 #root::sql::ExecError,
4141 >
4142 where
4143 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4144 + #root::sql::MaybeMyFromRow
4145 + #root::sql::MaybeSqliteFromRow
4146 + ::core::marker::Send
4147 + ::core::marker::Unpin,
4148 {
4149 Self::__aggregate_one_pool::<U>(
4150 col,
4151 |c| #root::core::AggregateExpr::Sum(c),
4152 pool,
4153 )
4154 .await
4155 }
4156 },
4157 );
4158 let avg_method = emit_if_no_field_collision(
4159 "avg",
4160 quote! {
4161 /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4162 /// models that already declare a field named `avg`.
4163 ///
4164 /// # Errors
4165 /// As [`#root::sql::fetch_aggregate_pool`].
4166 pub async fn avg<U>(
4167 col: &str,
4168 pool: &#root::sql::Pool,
4169 ) -> ::core::result::Result<
4170 ::core::option::Option<U>,
4171 #root::sql::ExecError,
4172 >
4173 where
4174 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4175 + #root::sql::MaybeMyFromRow
4176 + #root::sql::MaybeSqliteFromRow
4177 + ::core::marker::Send
4178 + ::core::marker::Unpin,
4179 {
4180 Self::__aggregate_one_pool::<U>(
4181 col,
4182 |c| #root::core::AggregateExpr::Avg(c),
4183 pool,
4184 )
4185 .await
4186 }
4187 },
4188 );
4189 let min_method = emit_if_no_field_collision(
4190 "min",
4191 quote! {
4192 /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4193 /// models that already declare a field named `min`.
4194 ///
4195 /// # Errors
4196 /// As [`#root::sql::fetch_aggregate_pool`].
4197 pub async fn min<U>(
4198 col: &str,
4199 pool: &#root::sql::Pool,
4200 ) -> ::core::result::Result<
4201 ::core::option::Option<U>,
4202 #root::sql::ExecError,
4203 >
4204 where
4205 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4206 + #root::sql::MaybeMyFromRow
4207 + #root::sql::MaybeSqliteFromRow
4208 + ::core::marker::Send
4209 + ::core::marker::Unpin,
4210 {
4211 Self::__aggregate_one_pool::<U>(
4212 col,
4213 |c| #root::core::AggregateExpr::Min(c),
4214 pool,
4215 )
4216 .await
4217 }
4218 },
4219 );
4220 let max_method = emit_if_no_field_collision(
4221 "max",
4222 quote! {
4223 /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4224 /// models that already declare a field named `max`.
4225 ///
4226 /// # Errors
4227 /// As [`#root::sql::fetch_aggregate_pool`].
4228 pub async fn max<U>(
4229 col: &str,
4230 pool: &#root::sql::Pool,
4231 ) -> ::core::result::Result<
4232 ::core::option::Option<U>,
4233 #root::sql::ExecError,
4234 >
4235 where
4236 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4237 + #root::sql::MaybeMyFromRow
4238 + #root::sql::MaybeSqliteFromRow
4239 + ::core::marker::Send
4240 + ::core::marker::Unpin,
4241 {
4242 Self::__aggregate_one_pool::<U>(
4243 col,
4244 |c| #root::core::AggregateExpr::Max(c),
4245 pool,
4246 )
4247 .await
4248 }
4249 },
4250 );
4251 let first_method = emit_if_no_field_collision(
4252 "first",
4253 quote! {
4254 /// First row of this model. Eloquent `Model::first()`.
4255 /// Skipped on models that already declare a field named
4256 /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4257 ///
4258 /// # Errors
4259 /// As `QuerySet::first`.
4260 pub async fn first(
4261 pool: &#root::sql::Pool,
4262 ) -> ::core::result::Result<
4263 ::core::option::Option<Self>,
4264 #root::sql::ExecError,
4265 > {
4266 #root::query::QuerySet::<Self>::default()
4267 .first(pool)
4268 .await
4269 }
4270 },
4271 );
4272 let last_method = emit_if_no_field_collision(
4273 "last",
4274 quote! {
4275 /// Last row of this model by primary-key DESC.
4276 /// Eloquent `Model::query()->latest('id')->first()`
4277 /// parity — fetches the highest-PK row without
4278 /// requiring the caller to spell the PK column.
4279 /// Returns `None` on an empty table.
4280 ///
4281 /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4282 /// Skipped on models that already declare a field
4283 /// named `last`.
4284 ///
4285 /// # Errors
4286 /// As `QuerySet::last`.
4287 pub async fn last(
4288 pool: &#root::sql::Pool,
4289 ) -> ::core::result::Result<
4290 ::core::option::Option<Self>,
4291 #root::sql::ExecError,
4292 > {
4293 #root::query::QuerySet::<Self>::default()
4294 .last(pool)
4295 .await
4296 }
4297 },
4298 );
4299 quote! {
4300 /// Re-SELECT this row by its primary key and overwrite
4301 /// every in-memory field with the freshly-fetched value.
4302 /// Issue #825.
4303 ///
4304 /// Use this when the row may have been modified by another
4305 /// process / connection / job since you read it — e.g. after
4306 /// a queued task callback, or to re-sync stale UI state
4307 /// before re-saving.
4308 ///
4309 /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4310 /// when the primary key no longer matches any row (e.g.
4311 /// the row was deleted concurrently).
4312 ///
4313 /// # Errors
4314 /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4315 /// the PK no longer exists.
4316 ///
4317 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4318 pub async fn refresh_from_db(
4319 &mut self,
4320 pool: &#root::sql::Pool,
4321 ) -> ::core::result::Result<(), #root::sql::ExecError> {
4322 use #root::sql::FetcherPool as _;
4323 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4324 ::core::clone::Clone::clone(&self.#pk_ident),
4325 );
4326 let mut _rows: ::std::vec::Vec<Self> =
4327 #root::query::QuerySet::<Self>::default()
4328 .filter(::core::stringify!(#pk_ident), _pk_val)
4329 .limit(1)
4330 .fetch(pool)
4331 .await?;
4332 match _rows.into_iter().next() {
4333 ::core::option::Option::Some(_fresh) => {
4334 *self = _fresh;
4335 ::core::result::Result::Ok(())
4336 }
4337 ::core::option::Option::None => ::core::result::Result::Err(
4338 #root::sql::ExecError::Driver(
4339 #root::sql::sqlx::Error::RowNotFound,
4340 ),
4341 ),
4342 }
4343 }
4344
4345 /// Atomically increment the integer column `col` by
4346 /// `by` for this row. Equivalent to
4347 /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4348 ///
4349 /// **Doesn't mutate `self`** — the in-memory copy is now
4350 /// stale; call [`Self::refresh_from_db_pool`] /
4351 /// [`Self::fresh_pool`] to re-sync. Returns the rows-
4352 /// affected count (0 when the PK doesn't match any row,
4353 /// 1 on success).
4354 ///
4355 /// `col` is the Rust field name as a string; unknown
4356 /// fields surface as `UnknownField` at runtime. Negative
4357 /// `by` values atomically decrement (see also
4358 /// [`Self::decrement_pool`]).
4359 ///
4360 /// # Errors
4361 /// As [`UpdaterPool::execute_pool`].
4362 ///
4363 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4364 pub async fn increment(
4365 &self,
4366 col: &str,
4367 by: i64,
4368 pool: &#root::sql::Pool,
4369 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4370 Self::__increment_one(self, col, by, pool).await
4371 }
4372
4373 /// Sibling of [`Self::increment`] — atomically
4374 /// decrement this row's `col` by `by`. Eloquent
4375 /// `$model->decrement($col, $by)` parity. Equivalent to
4376 /// `self.increment(col, -by, &pool)`; the separate name
4377 /// keeps call sites readable.
4378 ///
4379 /// # Errors
4380 /// As [`Self::increment`].
4381 pub async fn decrement(
4382 &self,
4383 col: &str,
4384 by: i64,
4385 pool: &#root::sql::Pool,
4386 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4387 Self::__increment_one(self, col, -by, pool).await
4388 }
4389
4390 /// Bulk-increment: add `by` to `col` on every row of the
4391 /// table. Eloquent `Model::query()->increment($col, $by)`
4392 /// parity. Use for counters, score adjustments, view
4393 /// rollups.
4394 ///
4395 /// # Errors
4396 /// As [`UpdaterPool::execute_pool`].
4397 ///
4398 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4399 pub async fn increment_each(
4400 col: &str,
4401 by: i64,
4402 pool: &#root::sql::Pool,
4403 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4404 Self::__increment_all(col, by, pool).await
4405 }
4406
4407 /// Sibling of [`Self::increment_each`] — bulk-decrement.
4408 ///
4409 /// # Errors
4410 /// As [`Self::increment_each`].
4411 pub async fn decrement_each(
4412 col: &str,
4413 by: i64,
4414 pool: &#root::sql::Pool,
4415 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4416 Self::__increment_all(col, -by, pool).await
4417 }
4418
4419 /// Internal: forward to
4420 /// [`#root::sql::model_shortcuts::increment_one_pool`].
4421 /// One-line wrapper kept as a per-Model method so the
4422 /// macro's emitted `increment` / `decrement` instance
4423 /// calls don't have to thread `Self` through manually.
4424 #[doc(hidden)]
4425 pub async fn __increment_one(
4426 this: &Self,
4427 col: &str,
4428 by: i64,
4429 pool: &#root::sql::Pool,
4430 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4431 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4432 ::core::clone::Clone::clone(&this.#pk_ident),
4433 );
4434 #root::sql::model_shortcuts::increment_one_pool::<Self>(
4435 ::core::stringify!(#pk_ident),
4436 _pk_val,
4437 col,
4438 by,
4439 pool,
4440 )
4441 .await
4442 }
4443
4444 /// Internal: forward to
4445 /// [`#root::sql::model_shortcuts::increment_all_pool`].
4446 #[doc(hidden)]
4447 pub async fn __increment_all(
4448 col: &str,
4449 by: i64,
4450 pool: &#root::sql::Pool,
4451 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4452 #root::sql::model_shortcuts::increment_all_pool::<Self>(col, by, pool).await
4453 }
4454
4455 /// Internal: forward to
4456 /// [`#root::sql::model_shortcuts::resolve_col`].
4457 #[doc(hidden)]
4458 pub fn __resolve_col(
4459 col: &str,
4460 ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4461 #root::sql::model_shortcuts::resolve_col::<Self>(col)
4462 }
4463
4464 /// Internal: forward to
4465 /// [`#root::sql::model_shortcuts::add_signed_expr`].
4466 #[doc(hidden)]
4467 #[must_use]
4468 pub fn __add_signed_expr(
4469 col_static: &'static str,
4470 signed_by: i64,
4471 ) -> #root::core::Expr {
4472 #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4473 }
4474
4475 /// Re-SELECT this row by its primary key and return a
4476 /// **new** instance with the freshly-fetched fields.
4477 /// Eloquent `Model::fresh()` parity — non-mutating
4478 /// counterpart of [`Self::refresh_from_db_pool`].
4479 ///
4480 /// Returns `Ok(None)` when the row was deleted
4481 /// concurrently — vs [`Self::refresh_from_db_pool`]
4482 /// which surfaces that as `RowNotFound` because
4483 /// in-place mutation has nothing to write to.
4484 ///
4485 /// Useful when you want to compare the in-memory
4486 /// instance against the persisted state (audit-style
4487 /// diffs, conflict detection) without mutating the
4488 /// reference you already hold.
4489 ///
4490 /// # Errors
4491 /// As [`FetcherPool::fetch`].
4492 ///
4493 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4494 pub async fn fresh(
4495 &self,
4496 pool: &#root::sql::Pool,
4497 ) -> ::core::result::Result<
4498 ::core::option::Option<Self>,
4499 #root::sql::ExecError,
4500 > {
4501 use #root::sql::FetcherPool as _;
4502 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4503 ::core::clone::Clone::clone(&self.#pk_ident),
4504 );
4505 let _rows: ::std::vec::Vec<Self> =
4506 #root::query::QuerySet::<Self>::default()
4507 .filter(::core::stringify!(#pk_ident), _pk_val)
4508 .limit(1)
4509 .fetch(pool)
4510 .await?;
4511 ::core::result::Result::Ok(_rows.into_iter().next())
4512 }
4513
4514 #replicate_doc
4515 #[must_use]
4516 pub fn replicate(&self) -> Self {
4517 Self {
4518 #pk_clone_token,
4519 #( #other_field_clones, )*
4520 }
4521 }
4522
4523 #first_method
4524
4525 #last_method
4526
4527 /// Throwing counterpart of [`Self::first_pool`] —
4528 /// errors with `RowNotFound` when the table is empty.
4529 /// Eloquent `Model::firstOrFail()` parity.
4530 ///
4531 /// # Errors
4532 /// As [`Self::first_pool`]; additionally
4533 /// [`sqlx::Error::RowNotFound`] on empty tables.
4534 ///
4535 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4536 pub async fn first_or_fail(
4537 pool: &#root::sql::Pool,
4538 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4539 // Route through the queryset rather than `Self::first`,
4540 // which is suppressed on models with a field named
4541 // `first` (field/shortcut collision guard).
4542 match #root::query::QuerySet::<Self>::default().first(pool).await? {
4543 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4544 ::core::option::Option::None => ::core::result::Result::Err(
4545 #root::sql::ExecError::Driver(
4546 #root::sql::sqlx::Error::RowNotFound,
4547 ),
4548 ),
4549 }
4550 }
4551
4552 /// Single-column projection — `SELECT <col> FROM
4553 /// <table>`. Returns `Vec<U>` where each element is the
4554 /// decoded value of the column. Eloquent
4555 /// `Model::pluck($column)` parity.
4556 ///
4557 /// Thin wrapper over `QuerySet::<Self>::default()
4558 /// .values_list_flat(col).fetch::<U>(pool)`. `U` must
4559 /// be decodable from the column's SQL type on every
4560 /// dialect the binary targets (common picks: `i64` /
4561 /// `i32` / `String` / `bool` / `f64`).
4562 ///
4563 /// # Errors
4564 /// As `ValuesFlatQuerySet::fetch`.
4565 pub async fn pluck<U>(
4566 col: &'static str,
4567 pool: &#root::sql::Pool,
4568 ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4569 where
4570 U: #root::sql::MaybePgScalar
4571 + #root::sql::MaybeMyScalar
4572 + #root::sql::MaybeSqliteScalar
4573 + ::core::marker::Send
4574 + ::core::marker::Unpin,
4575 {
4576 #root::query::QuerySet::<Self>::default()
4577 .values_list_flat(col)
4578 .fetch::<U>(pool)
4579 .await
4580 }
4581
4582 /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4583 /// stream every row of this model in batches of `n`,
4584 /// invoking the callback once per batch. Stable PK-ASC
4585 /// ordering so the LIMIT/OFFSET pagination is
4586 /// deterministic across drivers.
4587 ///
4588 /// The callback is async — it can do further DB work
4589 /// (writes, related-row lookups, queue dispatch) per
4590 /// batch. Return `Err(...)` from the callback to abort
4591 /// the iteration early; the error bubbles up.
4592 ///
4593 /// **When to use**: bulk processing flows that can't
4594 /// fit the whole table in memory — sending newsletters,
4595 /// running data migrations, computing summary
4596 /// statistics. For small / known-bounded tables, plain
4597 /// `Self::all(&pool)` is simpler.
4598 ///
4599 /// **Caveat**: ascending OFFSET pagination is O(N²) on
4600 /// large tables. For multi-million-row scans prefer
4601 /// keyset-by-PK (the standard "WHERE id > last_seen"
4602 /// shape) over `chunk(...)`.
4603 ///
4604 /// Skipped on models without a primary key — chunking
4605 /// needs a stable order to avoid skipping / repeating
4606 /// rows across batches.
4607 pub async fn chunk<F, Fut>(
4608 n: i64,
4609 pool: &#root::sql::Pool,
4610 mut cb: F,
4611 ) -> ::core::result::Result<(), #root::sql::ExecError>
4612 where
4613 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4614 Fut: ::core::future::Future<
4615 Output = ::core::result::Result<(), #root::sql::ExecError>,
4616 >,
4617 {
4618 use #root::sql::FetcherPool as _;
4619 let pk_col = match Self::primary_key_column() {
4620 ::core::option::Option::Some(c) => c,
4621 ::core::option::Option::None => {
4622 return ::core::result::Result::Ok(());
4623 }
4624 };
4625 let mut offset: i64 = 0;
4626 loop {
4627 let rows: ::std::vec::Vec<Self> =
4628 #root::query::QuerySet::<Self>::default()
4629 .order_by(&[(pk_col, false)])
4630 .limit(n)
4631 .offset(offset)
4632 .fetch(pool)
4633 .await?;
4634 if rows.is_empty() {
4635 return ::core::result::Result::Ok(());
4636 }
4637 let len = rows.len() as i64;
4638 cb(rows).await?;
4639 if len < n {
4640 return ::core::result::Result::Ok(());
4641 }
4642 offset += n;
4643 }
4644 }
4645
4646 /// Eloquent `Model::chunkById($n, fn (...))` — same
4647 /// per-batch callback shape as [`Self::chunk`], but uses
4648 /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4649 /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4650 /// — the right choice for multi-million-row sweeps.
4651 ///
4652 /// Requires the primary key to be a signed integer type
4653 /// (`i64` / `i32`); the keyset comparison rides on
4654 /// `__rustango_pk_value()` lowering through
4655 /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4656 /// non-integer PKs (UUID, String) — those should use
4657 /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4658 /// keyset loop.
4659 ///
4660 /// Callback errors abort iteration; the error bubbles up
4661 /// unchanged. Empty table → callback invoked zero times.
4662 pub async fn chunk_by_id<F, Fut>(
4663 n: i64,
4664 pool: &#root::sql::Pool,
4665 mut cb: F,
4666 ) -> ::core::result::Result<(), #root::sql::ExecError>
4667 where
4668 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4669 Fut: ::core::future::Future<
4670 Output = ::core::result::Result<(), #root::sql::ExecError>,
4671 >,
4672 {
4673 use #root::sql::FetcherPool as _;
4674 let pk_col = match Self::primary_key_column() {
4675 ::core::option::Option::Some(c) => c,
4676 ::core::option::Option::None => {
4677 return ::core::result::Result::Ok(());
4678 }
4679 };
4680 // Track the largest PK seen so the next batch picks
4681 // up from there. `i64::MIN` as the sentinel — the
4682 // very first iteration's `> MIN` matches every row,
4683 // so the loop entry is uniform with subsequent
4684 // iterations.
4685 let mut last_seen: i64 = i64::MIN;
4686 loop {
4687 let key = ::std::format!("{}__gt", pk_col);
4688 let rows: ::std::vec::Vec<Self> =
4689 #root::query::QuerySet::<Self>::default()
4690 .filter(key.as_str(), last_seen)
4691 .order_by(&[(pk_col, false)])
4692 .limit(n)
4693 .fetch(pool)
4694 .await?;
4695 if rows.is_empty() {
4696 return ::core::result::Result::Ok(());
4697 }
4698 let len = rows.len() as i64;
4699 // Capture the last row's PK BEFORE moving rows
4700 // into the callback.
4701 let max_pk = match rows
4702 .last()
4703 .map(|r| r.__rustango_pk_value())
4704 {
4705 ::core::option::Option::Some(
4706 #root::core::SqlValue::I64(v),
4707 ) => v,
4708 ::core::option::Option::Some(
4709 #root::core::SqlValue::I32(v),
4710 ) => i64::from(v),
4711 _ => return ::core::result::Result::Ok(()),
4712 };
4713 cb(rows).await?;
4714 if len < n {
4715 return ::core::result::Result::Ok(());
4716 }
4717 last_seen = max_pk;
4718 }
4719 }
4720
4721 /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4722 /// per-row callback companion to [`Self::chunk`].
4723 /// Streams every row in keyset-paginated batches of
4724 /// `batch` size, calling `cb` once per row.
4725 ///
4726 /// Inherits the keyset-paginated scan of
4727 /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4728 /// non-integer PKs are silently a no-op).
4729 ///
4730 /// ```ignore
4731 /// Post::each(500, &pool, |p| async move {
4732 /// reindex(p).await?;
4733 /// Ok(())
4734 /// }).await?;
4735 /// ```
4736 ///
4737 /// Return `Err(...)` from the callback to abort
4738 /// iteration; the error bubbles up unchanged.
4739 pub async fn each<F, Fut>(
4740 batch: i64,
4741 pool: &#root::sql::Pool,
4742 mut cb: F,
4743 ) -> ::core::result::Result<(), #root::sql::ExecError>
4744 where
4745 F: ::core::ops::FnMut(Self) -> Fut,
4746 Fut: ::core::future::Future<
4747 Output = ::core::result::Result<(), #root::sql::ExecError>,
4748 >,
4749 {
4750 use #root::sql::FetcherPool as _;
4751 let pk_col = match Self::primary_key_column() {
4752 ::core::option::Option::Some(c) => c,
4753 ::core::option::Option::None => {
4754 return ::core::result::Result::Ok(());
4755 }
4756 };
4757 let mut last_seen: i64 = i64::MIN;
4758 loop {
4759 let key = ::std::format!("{}__gt", pk_col);
4760 let rows: ::std::vec::Vec<Self> =
4761 #root::query::QuerySet::<Self>::default()
4762 .filter(key.as_str(), last_seen)
4763 .order_by(&[(pk_col, false)])
4764 .limit(batch)
4765 .fetch(pool)
4766 .await?;
4767 if rows.is_empty() {
4768 return ::core::result::Result::Ok(());
4769 }
4770 let len = rows.len() as i64;
4771 let max_pk = match rows
4772 .last()
4773 .map(|r| r.__rustango_pk_value())
4774 {
4775 ::core::option::Option::Some(
4776 #root::core::SqlValue::I64(v),
4777 ) => v,
4778 ::core::option::Option::Some(
4779 #root::core::SqlValue::I32(v),
4780 ) => i64::from(v),
4781 _ => return ::core::result::Result::Ok(()),
4782 };
4783 for row in rows {
4784 cb(row).await?;
4785 }
4786 if len < batch {
4787 return ::core::result::Result::Ok(());
4788 }
4789 last_seen = max_pk;
4790 }
4791 }
4792
4793 /// Delete every row of this model — `TRUNCATE TABLE
4794 /// <table> RESTART IDENTITY CASCADE` on Postgres,
4795 /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4796 /// support `TRUNCATE` inside foreign-key constraints
4797 /// or — for SQLite — at all). Eloquent `Model::truncate()`
4798 /// parity.
4799 ///
4800 /// **Use only in tests / fixture-reset flows.** Production
4801 /// writes through this would silently bypass the
4802 /// `pre_delete` / `post_delete` signals (no per-row hooks
4803 /// fire on a TRUNCATE / bulk DELETE FROM) and lose every
4804 /// row's audit-log entry.
4805 ///
4806 /// # Errors
4807 /// As [`raw_execute_pool`].
4808 ///
4809 /// [`raw_execute_pool`]: rustango::sql::raw_execute_pool
4810 pub async fn truncate(
4811 pool: &#root::sql::Pool,
4812 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4813 let _table = <Self as #root::core::Model>::SCHEMA.table;
4814 let _dialect = pool.dialect();
4815 let _quoted = _dialect.quote_ident(_table);
4816 let _sql = if _dialect.name() == "postgres" {
4817 ::std::format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", _quoted)
4818 } else {
4819 ::std::format!("DELETE FROM {}", _quoted)
4820 };
4821 #root::sql::raw_execute_pool(pool, &_sql, ::std::vec::Vec::new()).await
4822 }
4823
4824 /// Bulk-delete every row whose primary key is in
4825 /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4826 /// Returns the affected row count.
4827 ///
4828 /// Eloquent `Model::destroy([1, 2, 3])` parity.
4829 /// Empty `pks` is a no-op (returns 0).
4830 ///
4831 /// Accepts any iterable whose elements are
4832 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4833 /// `[i64; N]`, etc.
4834 ///
4835 /// # Errors
4836 /// As `delete_pool`.
4837 pub async fn destroy<V>(
4838 pks: impl ::core::iter::IntoIterator<Item = V>,
4839 pool: &#root::sql::Pool,
4840 ) -> ::core::result::Result<u64, #root::sql::ExecError>
4841 where
4842 V: ::core::convert::Into<#root::core::SqlValue>,
4843 {
4844 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4845 pks.into_iter().map(::core::convert::Into::into).collect();
4846 if _values.is_empty() {
4847 return ::core::result::Result::Ok(0);
4848 }
4849 let _query = #root::core::DeleteQuery {
4850 model: <Self as #root::core::Model>::SCHEMA,
4851 where_clause: #root::core::WhereExpr::Predicate(
4852 #root::core::Filter {
4853 column: <Self as #root::core::Model>::SCHEMA
4854 .primary_key()
4855 .ok_or_else(|| {
4856 #root::sql::ExecError::Sql(
4857 #root::sql::SqlError::MissingPrimaryKey,
4858 )
4859 })?
4860 .column,
4861 op: #root::core::Op::In,
4862 value: #root::core::SqlValue::List(_values),
4863 },
4864 ),
4865 };
4866 #root::sql::delete_pool(pool, &_query).await
4867 }
4868
4869 /// Fetch every row where `<col> = <val>`. Eloquent
4870 /// `Model::where($col, $val)->get()` parity.
4871 ///
4872 /// Thin wrapper over `QuerySet::<Self>::default()
4873 /// .filter(col, val).fetch(pool)`. For one row,
4874 /// use [`Self::first_where_pool`]; for a chain that
4875 /// needs further `.filter()` / `.order_by()` /
4876 /// `.limit()`, drop down to `Self::query().filter(...)`
4877 /// directly.
4878 ///
4879 /// `val` accepts any value `Into<SqlValue>` so plain
4880 /// strings, ints, UUIDs, etc. all work.
4881 ///
4882 /// # Errors
4883 /// As [`FetcherPool::fetch`].
4884 ///
4885 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4886 pub async fn where_(
4887 col: &str,
4888 val: impl ::core::convert::Into<#root::core::SqlValue>,
4889 pool: &#root::sql::Pool,
4890 ) -> ::core::result::Result<
4891 ::std::vec::Vec<Self>,
4892 #root::sql::ExecError,
4893 > {
4894 use #root::sql::FetcherPool as _;
4895 #root::query::QuerySet::<Self>::default()
4896 .filter(col, val)
4897 .fetch(pool)
4898 .await
4899 }
4900
4901 /// Fetch every row where `<col> IN (vals)`. Eloquent
4902 /// `Model::whereIn($col, $vals)->get()` parity. Empty
4903 /// `vals` returns no rows (matches SQL's empty-IN
4904 /// semantics).
4905 ///
4906 /// `vals` accepts any iterable whose items are
4907 /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
4908 /// etc.
4909 ///
4910 /// # Errors
4911 /// As [`FetcherPool::fetch`].
4912 ///
4913 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4914 pub async fn where_in<V>(
4915 col: &str,
4916 vals: impl ::core::iter::IntoIterator<Item = V>,
4917 pool: &#root::sql::Pool,
4918 ) -> ::core::result::Result<
4919 ::std::vec::Vec<Self>,
4920 #root::sql::ExecError,
4921 >
4922 where
4923 V: ::core::convert::Into<#root::core::SqlValue>,
4924 {
4925 use #root::sql::FetcherPool as _;
4926 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4927 vals.into_iter().map(::core::convert::Into::into).collect();
4928 if _values.is_empty() {
4929 return ::core::result::Result::Ok(::std::vec::Vec::new());
4930 }
4931 let _key = ::std::format!("{}__in", col);
4932 #root::query::QuerySet::<Self>::default()
4933 .filter(&_key, #root::core::SqlValue::List(_values))
4934 .fetch(pool)
4935 .await
4936 }
4937
4938 /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
4939 /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
4940 /// `vals` returns every row (matches SQL's empty-NOT-IN
4941 /// semantics — vacuously true for every row).
4942 ///
4943 /// # Errors
4944 /// As [`FetcherPool::fetch`].
4945 ///
4946 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4947 pub async fn where_not_in<V>(
4948 col: &str,
4949 vals: impl ::core::iter::IntoIterator<Item = V>,
4950 pool: &#root::sql::Pool,
4951 ) -> ::core::result::Result<
4952 ::std::vec::Vec<Self>,
4953 #root::sql::ExecError,
4954 >
4955 where
4956 V: ::core::convert::Into<#root::core::SqlValue>,
4957 {
4958 use #root::sql::FetcherPool as _;
4959 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4960 vals.into_iter().map(::core::convert::Into::into).collect();
4961 if _values.is_empty() {
4962 return #root::query::QuerySet::<Self>::default()
4963 .fetch(pool)
4964 .await;
4965 }
4966 let _key = ::std::format!("{}__not_in", col);
4967 #root::query::QuerySet::<Self>::default()
4968 .filter(&_key, #root::core::SqlValue::List(_values))
4969 .fetch(pool)
4970 .await
4971 }
4972
4973 /// Fetch every row where `<col> IS NULL`. Eloquent
4974 /// `Model::whereNull($col)->get()` parity.
4975 ///
4976 /// # Errors
4977 /// As [`FetcherPool::fetch`].
4978 ///
4979 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4980 pub async fn where_null(
4981 col: &str,
4982 pool: &#root::sql::Pool,
4983 ) -> ::core::result::Result<
4984 ::std::vec::Vec<Self>,
4985 #root::sql::ExecError,
4986 > {
4987 use #root::sql::FetcherPool as _;
4988 let _key = ::std::format!("{}__isnull", col);
4989 #root::query::QuerySet::<Self>::default()
4990 .filter(&_key, #root::core::SqlValue::Bool(true))
4991 .fetch(pool)
4992 .await
4993 }
4994
4995 /// Fetch every row where `<col> IS NOT NULL`. Eloquent
4996 /// `Model::whereNotNull($col)->get()` parity.
4997 ///
4998 /// # Errors
4999 /// As [`FetcherPool::fetch`].
5000 ///
5001 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5002 pub async fn where_not_null(
5003 col: &str,
5004 pool: &#root::sql::Pool,
5005 ) -> ::core::result::Result<
5006 ::std::vec::Vec<Self>,
5007 #root::sql::ExecError,
5008 > {
5009 use #root::sql::FetcherPool as _;
5010 let _key = ::std::format!("{}__isnull", col);
5011 #root::query::QuerySet::<Self>::default()
5012 .filter(&_key, #root::core::SqlValue::Bool(false))
5013 .fetch(pool)
5014 .await
5015 }
5016
5017 /// Fetch up to `n` rows in random order. Eloquent
5018 /// `Model::inRandomOrder()->limit($n)->get()` /
5019 /// `Model::query()->inRandomOrder()->get()->take($n)`
5020 /// parity. **Performance caveat**: random ordering
5021 /// forces a full table scan + per-row random key sort;
5022 /// the optimizer cannot use an index. Prefer a
5023 /// `pk >= rand_offset LIMIT N` walk for huge tables.
5024 ///
5025 /// # Errors
5026 /// As [`FetcherPool::fetch`].
5027 ///
5028 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5029 pub async fn random_n(
5030 n: i64,
5031 pool: &#root::sql::Pool,
5032 ) -> ::core::result::Result<
5033 ::std::vec::Vec<Self>,
5034 #root::sql::ExecError,
5035 > {
5036 use #root::sql::FetcherPool as _;
5037 #root::query::QuerySet::<Self>::default()
5038 .order_random()
5039 .limit(n)
5040 .fetch(pool)
5041 .await
5042 }
5043
5044 /// Fetch one row in random order. Eloquent
5045 /// `Model::inRandomOrder()->first()` parity. Same
5046 /// performance caveat as [`Self::random_n_pool`].
5047 ///
5048 /// # Errors
5049 /// As [`FetcherPool::fetch`].
5050 ///
5051 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5052 pub async fn random(
5053 pool: &#root::sql::Pool,
5054 ) -> ::core::result::Result<
5055 ::core::option::Option<Self>,
5056 #root::sql::ExecError,
5057 > {
5058 ::core::result::Result::Ok(
5059 Self::random_n(1, pool).await?.into_iter().next(),
5060 )
5061 }
5062
5063 /// Fetch every row ordered ASC by `field`. Eloquent
5064 /// `Model::oldest($field)->get()` parity — the multi-row
5065 /// counterpart of [`Self::earliest_pool`].
5066 ///
5067 /// # Errors
5068 /// As [`FetcherPool::fetch`].
5069 ///
5070 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5071 pub async fn oldest(
5072 field: &str,
5073 pool: &#root::sql::Pool,
5074 ) -> ::core::result::Result<
5075 ::std::vec::Vec<Self>,
5076 #root::sql::ExecError,
5077 > {
5078 use #root::sql::FetcherPool as _;
5079 #root::query::QuerySet::<Self>::default()
5080 .order_by(&[(field, false)])
5081 .fetch(pool)
5082 .await
5083 }
5084
5085 /// Fetch every row ordered DESC by `field`. Eloquent
5086 /// `Model::latest($field)->get()` parity — the multi-row
5087 /// counterpart of [`Self::latest_pool`].
5088 ///
5089 /// # Errors
5090 /// As [`FetcherPool::fetch`].
5091 ///
5092 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5093 pub async fn newest(
5094 field: &str,
5095 pool: &#root::sql::Pool,
5096 ) -> ::core::result::Result<
5097 ::std::vec::Vec<Self>,
5098 #root::sql::ExecError,
5099 > {
5100 use #root::sql::FetcherPool as _;
5101 #root::query::QuerySet::<Self>::default()
5102 .order_by(&[(field, true)])
5103 .fetch(pool)
5104 .await
5105 }
5106
5107 /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5108 /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5109 /// Routes through the existing `__year` lookup suffix
5110 /// (issue #829).
5111 ///
5112 /// # Errors
5113 /// As [`FetcherPool::fetch`].
5114 ///
5115 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5116 pub async fn where_year(
5117 col: &str,
5118 year: i64,
5119 pool: &#root::sql::Pool,
5120 ) -> ::core::result::Result<
5121 ::std::vec::Vec<Self>,
5122 #root::sql::ExecError,
5123 > {
5124 use #root::sql::FetcherPool as _;
5125 let _key = ::std::format!("{}__year", col);
5126 #root::query::QuerySet::<Self>::default()
5127 .filter(&_key, #root::core::SqlValue::I64(year))
5128 .fetch(pool)
5129 .await
5130 }
5131
5132 /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5133 /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5134 /// `month` is 1–12.
5135 ///
5136 /// # Errors
5137 /// As [`FetcherPool::fetch`].
5138 ///
5139 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5140 pub async fn where_month(
5141 col: &str,
5142 month: i64,
5143 pool: &#root::sql::Pool,
5144 ) -> ::core::result::Result<
5145 ::std::vec::Vec<Self>,
5146 #root::sql::ExecError,
5147 > {
5148 use #root::sql::FetcherPool as _;
5149 let _key = ::std::format!("{}__month", col);
5150 #root::query::QuerySet::<Self>::default()
5151 .filter(&_key, #root::core::SqlValue::I64(month))
5152 .fetch(pool)
5153 .await
5154 }
5155
5156 /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5157 /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5158 /// `day` is 1–31.
5159 ///
5160 /// # Errors
5161 /// As [`FetcherPool::fetch`].
5162 ///
5163 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5164 pub async fn where_day(
5165 col: &str,
5166 day: i64,
5167 pool: &#root::sql::Pool,
5168 ) -> ::core::result::Result<
5169 ::std::vec::Vec<Self>,
5170 #root::sql::ExecError,
5171 > {
5172 use #root::sql::FetcherPool as _;
5173 let _key = ::std::format!("{}__day", col);
5174 #root::query::QuerySet::<Self>::default()
5175 .filter(&_key, #root::core::SqlValue::I64(day))
5176 .fetch(pool)
5177 .await
5178 }
5179
5180 /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5181 /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5182 /// `hour` is 0–23.
5183 ///
5184 /// # Errors
5185 /// As [`FetcherPool::fetch`].
5186 ///
5187 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5188 pub async fn where_hour(
5189 col: &str,
5190 hour: i64,
5191 pool: &#root::sql::Pool,
5192 ) -> ::core::result::Result<
5193 ::std::vec::Vec<Self>,
5194 #root::sql::ExecError,
5195 > {
5196 use #root::sql::FetcherPool as _;
5197 let _key = ::std::format!("{}__hour", col);
5198 #root::query::QuerySet::<Self>::default()
5199 .filter(&_key, #root::core::SqlValue::I64(hour))
5200 .fetch(pool)
5201 .await
5202 }
5203
5204 /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5205 /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5206 /// `minute` is 0–59.
5207 ///
5208 /// # Errors
5209 /// As [`FetcherPool::fetch`].
5210 ///
5211 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5212 pub async fn where_minute(
5213 col: &str,
5214 minute: i64,
5215 pool: &#root::sql::Pool,
5216 ) -> ::core::result::Result<
5217 ::std::vec::Vec<Self>,
5218 #root::sql::ExecError,
5219 > {
5220 use #root::sql::FetcherPool as _;
5221 let _key = ::std::format!("{}__minute", col);
5222 #root::query::QuerySet::<Self>::default()
5223 .filter(&_key, #root::core::SqlValue::I64(minute))
5224 .fetch(pool)
5225 .await
5226 }
5227
5228 /// Fetch every row where `<col> LIKE <pattern>` —
5229 /// caller-supplied pattern (must include `%` / `_`
5230 /// wildcards manually). Eloquent `Model::whereLike`
5231 /// parity.
5232 ///
5233 /// # Errors
5234 /// As [`FetcherPool::fetch`].
5235 ///
5236 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5237 pub async fn where_like(
5238 col: &str,
5239 pattern: impl ::core::convert::Into<::std::string::String>,
5240 pool: &#root::sql::Pool,
5241 ) -> ::core::result::Result<
5242 ::std::vec::Vec<Self>,
5243 #root::sql::ExecError,
5244 > {
5245 use #root::sql::FetcherPool as _;
5246 let _key = ::std::format!("{}__like", col);
5247 #root::query::QuerySet::<Self>::default()
5248 .filter(
5249 &_key,
5250 #root::core::SqlValue::String(pattern.into()),
5251 )
5252 .fetch(pool)
5253 .await
5254 }
5255
5256 /// Fetch every row where `<col> ILIKE <pattern>` —
5257 /// case-insensitive LIKE (PG native, MySQL/SQLite
5258 /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5259 ///
5260 /// # Errors
5261 /// As [`FetcherPool::fetch`].
5262 ///
5263 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5264 pub async fn where_ilike(
5265 col: &str,
5266 pattern: impl ::core::convert::Into<::std::string::String>,
5267 pool: &#root::sql::Pool,
5268 ) -> ::core::result::Result<
5269 ::std::vec::Vec<Self>,
5270 #root::sql::ExecError,
5271 > {
5272 use #root::sql::FetcherPool as _;
5273 let _key = ::std::format!("{}__ilike", col);
5274 #root::query::QuerySet::<Self>::default()
5275 .filter(
5276 &_key,
5277 #root::core::SqlValue::String(pattern.into()),
5278 )
5279 .fetch(pool)
5280 .await
5281 }
5282
5283 /// Fetch every row where `<col>` starts with `prefix`
5284 /// (auto-appends `%`). Eloquent
5285 /// `whereLike("col", "$prefix%")` parity.
5286 ///
5287 /// # Errors
5288 /// As [`FetcherPool::fetch`].
5289 ///
5290 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5291 pub async fn where_starts_with(
5292 col: &str,
5293 prefix: impl ::core::convert::Into<::std::string::String>,
5294 pool: &#root::sql::Pool,
5295 ) -> ::core::result::Result<
5296 ::std::vec::Vec<Self>,
5297 #root::sql::ExecError,
5298 > {
5299 use #root::sql::FetcherPool as _;
5300 let _key = ::std::format!("{}__startswith", col);
5301 #root::query::QuerySet::<Self>::default()
5302 .filter(
5303 &_key,
5304 #root::core::SqlValue::String(prefix.into()),
5305 )
5306 .fetch(pool)
5307 .await
5308 }
5309
5310 /// Fetch every row where `<col>` ends with `suffix`
5311 /// (auto-prepends `%`). Eloquent
5312 /// `whereLike("col", "%$suffix")` parity.
5313 ///
5314 /// # Errors
5315 /// As [`FetcherPool::fetch`].
5316 ///
5317 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5318 pub async fn where_ends_with(
5319 col: &str,
5320 suffix: impl ::core::convert::Into<::std::string::String>,
5321 pool: &#root::sql::Pool,
5322 ) -> ::core::result::Result<
5323 ::std::vec::Vec<Self>,
5324 #root::sql::ExecError,
5325 > {
5326 use #root::sql::FetcherPool as _;
5327 let _key = ::std::format!("{}__endswith", col);
5328 #root::query::QuerySet::<Self>::default()
5329 .filter(
5330 &_key,
5331 #root::core::SqlValue::String(suffix.into()),
5332 )
5333 .fetch(pool)
5334 .await
5335 }
5336
5337 /// Fetch every row where `<col>` contains `substr`
5338 /// (auto-wraps with `%`).
5339 /// Eloquent `whereLike("col", "%$substr%")` parity.
5340 ///
5341 /// # Errors
5342 /// As [`FetcherPool::fetch`].
5343 ///
5344 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5345 pub async fn where_contains(
5346 col: &str,
5347 substr: impl ::core::convert::Into<::std::string::String>,
5348 pool: &#root::sql::Pool,
5349 ) -> ::core::result::Result<
5350 ::std::vec::Vec<Self>,
5351 #root::sql::ExecError,
5352 > {
5353 use #root::sql::FetcherPool as _;
5354 let _key = ::std::format!("{}__contains", col);
5355 #root::query::QuerySet::<Self>::default()
5356 .filter(
5357 &_key,
5358 #root::core::SqlValue::String(substr.into()),
5359 )
5360 .fetch(pool)
5361 .await
5362 }
5363
5364 /// Fetch every row where `<col> > val`. Eloquent
5365 /// `Model::where($col, ">", $val)->get()` parity.
5366 ///
5367 /// # Errors
5368 /// As [`FetcherPool::fetch`].
5369 ///
5370 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5371 pub async fn where_gt(
5372 col: &str,
5373 val: impl ::core::convert::Into<#root::core::SqlValue>,
5374 pool: &#root::sql::Pool,
5375 ) -> ::core::result::Result<
5376 ::std::vec::Vec<Self>,
5377 #root::sql::ExecError,
5378 > {
5379 use #root::sql::FetcherPool as _;
5380 let _key = ::std::format!("{}__gt", col);
5381 #root::query::QuerySet::<Self>::default()
5382 .filter(&_key, val)
5383 .fetch(pool)
5384 .await
5385 }
5386
5387 /// Fetch every row where `<col> >= val`. Eloquent
5388 /// `Model::where($col, ">=", $val)->get()` parity.
5389 ///
5390 /// # Errors
5391 /// As [`FetcherPool::fetch`].
5392 ///
5393 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5394 pub async fn where_gte(
5395 col: &str,
5396 val: impl ::core::convert::Into<#root::core::SqlValue>,
5397 pool: &#root::sql::Pool,
5398 ) -> ::core::result::Result<
5399 ::std::vec::Vec<Self>,
5400 #root::sql::ExecError,
5401 > {
5402 use #root::sql::FetcherPool as _;
5403 let _key = ::std::format!("{}__gte", col);
5404 #root::query::QuerySet::<Self>::default()
5405 .filter(&_key, val)
5406 .fetch(pool)
5407 .await
5408 }
5409
5410 /// Fetch every row where `<col> < val`. Eloquent
5411 /// `Model::where($col, "<", $val)->get()` parity.
5412 ///
5413 /// # Errors
5414 /// As [`FetcherPool::fetch`].
5415 ///
5416 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5417 pub async fn where_lt(
5418 col: &str,
5419 val: impl ::core::convert::Into<#root::core::SqlValue>,
5420 pool: &#root::sql::Pool,
5421 ) -> ::core::result::Result<
5422 ::std::vec::Vec<Self>,
5423 #root::sql::ExecError,
5424 > {
5425 use #root::sql::FetcherPool as _;
5426 let _key = ::std::format!("{}__lt", col);
5427 #root::query::QuerySet::<Self>::default()
5428 .filter(&_key, val)
5429 .fetch(pool)
5430 .await
5431 }
5432
5433 /// Fetch every row where `<col> <= val`. Eloquent
5434 /// `Model::where($col, "<=", $val)->get()` parity.
5435 ///
5436 /// # Errors
5437 /// As [`FetcherPool::fetch`].
5438 ///
5439 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5440 pub async fn where_lte(
5441 col: &str,
5442 val: impl ::core::convert::Into<#root::core::SqlValue>,
5443 pool: &#root::sql::Pool,
5444 ) -> ::core::result::Result<
5445 ::std::vec::Vec<Self>,
5446 #root::sql::ExecError,
5447 > {
5448 use #root::sql::FetcherPool as _;
5449 let _key = ::std::format!("{}__lte", col);
5450 #root::query::QuerySet::<Self>::default()
5451 .filter(&_key, val)
5452 .fetch(pool)
5453 .await
5454 }
5455
5456 /// Fetch every row where `<col> <> val`. Eloquent
5457 /// `Model::where($col, "!=", $val)->get()` parity.
5458 ///
5459 /// # Errors
5460 /// As [`FetcherPool::fetch`].
5461 ///
5462 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5463 pub async fn where_ne(
5464 col: &str,
5465 val: impl ::core::convert::Into<#root::core::SqlValue>,
5466 pool: &#root::sql::Pool,
5467 ) -> ::core::result::Result<
5468 ::std::vec::Vec<Self>,
5469 #root::sql::ExecError,
5470 > {
5471 use #root::sql::FetcherPool as _;
5472 let _key = ::std::format!("{}__ne", col);
5473 #root::query::QuerySet::<Self>::default()
5474 .filter(&_key, val)
5475 .fetch(pool)
5476 .await
5477 }
5478
5479 /// Fetch every row matching `<col> = val` for ANY of the
5480 /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5481 /// parity. Empty `cols` returns no rows.
5482 ///
5483 /// Resolves each `&str` column to its SCHEMA-registered
5484 /// `&'static str` once and builds a single OR-composed
5485 /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5486 ///
5487 /// # Errors
5488 /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5489 /// when any column is not declared on the model.
5490 ///
5491 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5492 pub async fn where_any(
5493 cols: &[&str],
5494 val: impl ::core::convert::Into<#root::core::SqlValue>,
5495 pool: &#root::sql::Pool,
5496 ) -> ::core::result::Result<
5497 ::std::vec::Vec<Self>,
5498 #root::sql::ExecError,
5499 > {
5500 Self::__where_multi(cols, val, false, pool).await
5501 }
5502
5503 /// Fetch every row matching `<col> = val` for ALL listed
5504 /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5505 /// Empty `cols` returns every row (vacuous AND).
5506 ///
5507 /// # Errors
5508 /// As [`Self::where_any`].
5509 pub async fn where_all(
5510 cols: &[&str],
5511 val: impl ::core::convert::Into<#root::core::SqlValue>,
5512 pool: &#root::sql::Pool,
5513 ) -> ::core::result::Result<
5514 ::std::vec::Vec<Self>,
5515 #root::sql::ExecError,
5516 > {
5517 Self::__where_multi(cols, val, true, pool).await
5518 }
5519
5520 /// Internal: build a Q expression composing `cols` via
5521 /// AND (`all`) or OR (`!all`), then fetch. Backs
5522 /// `where_any` / `where_all`.
5523 #[doc(hidden)]
5524 pub async fn __where_multi(
5525 cols: &[&str],
5526 val: impl ::core::convert::Into<#root::core::SqlValue>,
5527 all: bool,
5528 pool: &#root::sql::Pool,
5529 ) -> ::core::result::Result<
5530 ::std::vec::Vec<Self>,
5531 #root::sql::ExecError,
5532 > {
5533 #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5534 .await
5535 }
5536
5537 /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5538 /// parity. PK-ordered
5539 /// is NOT guaranteed without an explicit `order_by` —
5540 /// drop into `Self::query()` for that.
5541 ///
5542 /// # Errors
5543 /// As [`FetcherPool::fetch`].
5544 ///
5545 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5546 pub async fn take(
5547 n: i64,
5548 pool: &#root::sql::Pool,
5549 ) -> ::core::result::Result<
5550 ::std::vec::Vec<Self>,
5551 #root::sql::ExecError,
5552 > {
5553 use #root::sql::FetcherPool as _;
5554 #root::query::QuerySet::<Self>::default()
5555 .limit(n)
5556 .fetch(pool)
5557 .await
5558 }
5559
5560 /// Fetch the page-th window of `per_page` rows
5561 /// (1-indexed). Eloquent
5562 /// `Model::query()->forPage($page, $perPage)->get()`
5563 /// parity. The DB scans an `OFFSET (page - 1) * per_page
5564 /// LIMIT per_page`; for large offsets this is O(N) —
5565 /// prefer keyset pagination via PK on hot paths.
5566 ///
5567 /// # Errors
5568 /// As [`FetcherPool::fetch`].
5569 ///
5570 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5571 pub async fn for_page(
5572 page: i64,
5573 per_page: i64,
5574 pool: &#root::sql::Pool,
5575 ) -> ::core::result::Result<
5576 ::std::vec::Vec<Self>,
5577 #root::sql::ExecError,
5578 > {
5579 use #root::sql::FetcherPool as _;
5580 let _offset = if page > 1 { (page - 1) * per_page } else { 0 };
5581 #root::query::QuerySet::<Self>::default()
5582 .limit(per_page)
5583 .offset(_offset)
5584 .fetch(pool)
5585 .await
5586 }
5587
5588 /// Eloquent `Model::paginate($per_page, $page)` — fetch
5589 /// one page of rows AND the total row count in one
5590 /// call. Returns `(rows, total)`. Two queries: a
5591 /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5592 /// for the total.
5593 ///
5594 /// Useful for paginated UIs that need both the visible
5595 /// rows AND a "Page X of Y" / total-count widget. For
5596 /// large tables prefer keyset pagination + cached count;
5597 /// every call to `paginate` re-counts the full table.
5598 ///
5599 /// Same 1-indexed `page` convention as [`Self::for_page`].
5600 ///
5601 /// # Errors
5602 /// As [`Self::for_page`] and [`Self::count`].
5603 pub async fn paginate(
5604 page: i64,
5605 per_page: i64,
5606 pool: &#root::sql::Pool,
5607 ) -> ::core::result::Result<
5608 (::std::vec::Vec<Self>, i64),
5609 #root::sql::ExecError,
5610 > {
5611 // Route through the queryset rather than `Self::count`:
5612 // the `count` inherent method is suppressed on models
5613 // that declare a field named `count` (field/shortcut
5614 // collision guard), and `paginate` must still compile
5615 // for those models.
5616 let total = {
5617 use #root::sql::CounterPool as _;
5618 #root::query::QuerySet::<Self>::default()
5619 .count(pool)
5620 .await?
5621 };
5622 let rows = Self::for_page(page, per_page, pool).await?;
5623 ::core::result::Result::Ok((rows, total))
5624 }
5625
5626 /// Bulk-update — set `set_col = set_val` on every row
5627 /// matching `where_col = where_val`. Returns affected row
5628 /// count. Eloquent
5629 /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5630 /// parity.
5631 ///
5632 /// For multi-column updates drop into the queryset
5633 /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5634 ///
5635 /// # Errors
5636 /// As [`UpdaterPool::execute_pool`].
5637 ///
5638 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5639 pub async fn update_where(
5640 where_col: &str,
5641 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5642 set_col: &str,
5643 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5644 pool: &#root::sql::Pool,
5645 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5646 use #root::sql::UpdaterPool as _;
5647 #root::query::QuerySet::<Self>::default()
5648 .filter(where_col, where_val)
5649 .update()
5650 .set(set_col, set_val)
5651 .execute_pool(pool)
5652 .await
5653 }
5654
5655 /// Bulk-delete — remove every row matching
5656 /// `where_col = where_val`. Returns affected row count.
5657 /// Eloquent
5658 /// `Model::where($where_col, $where_val)->delete()` parity.
5659 ///
5660 /// For more complex filters drop into the queryset
5661 /// builder + `Self::query().filter(...).delete().execute_pool(&pool)`.
5662 ///
5663 /// # Errors
5664 /// As [`#root::sql::delete_pool`].
5665 pub async fn delete_where(
5666 where_col: &str,
5667 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5668 pool: &#root::sql::Pool,
5669 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5670 let _query = #root::core::DeleteQuery {
5671 model: <Self as #root::core::Model>::SCHEMA,
5672 where_clause: #root::core::WhereExpr::Predicate(
5673 #root::core::Filter {
5674 column: <Self as #root::core::Model>::SCHEMA
5675 .field(where_col)
5676 .ok_or_else(|| {
5677 #root::sql::ExecError::Query(
5678 #root::core::QueryError::UnknownField {
5679 model: <Self as #root::core::Model>::SCHEMA.name,
5680 field: ::std::string::ToString::to_string(where_col),
5681 },
5682 )
5683 })?
5684 .column,
5685 op: #root::core::Op::Eq,
5686 value: ::core::convert::Into::into(where_val),
5687 },
5688 ),
5689 };
5690 #root::sql::delete_pool(pool, &_query).await
5691 }
5692
5693 /// Bulk-update — set `set_col = set_val` on EVERY row of
5694 /// this model's table. **No WHERE clause** — use with
5695 /// care. Eloquent `Model::query()->update([$col => $val])`
5696 /// parity.
5697 ///
5698 /// Use for backfills, one-shot reset flows, etc. The
5699 /// returned count is rows affected.
5700 ///
5701 /// # Errors
5702 /// As [`UpdaterPool::execute_pool`].
5703 ///
5704 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5705 pub async fn update_all(
5706 set_col: &str,
5707 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5708 pool: &#root::sql::Pool,
5709 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5710 use #root::sql::UpdaterPool as _;
5711 #root::query::QuerySet::<Self>::default()
5712 .update()
5713 .set(set_col, set_val)
5714 .execute_pool(pool)
5715 .await
5716 }
5717
5718 /// Fetch every row where `<col> NOT LIKE <pattern>`.
5719 /// Eloquent `Model::whereNotLike` parity. Pattern is
5720 /// passed verbatim — caller controls `%` / `_`.
5721 ///
5722 /// # Errors
5723 /// As [`FetcherPool::fetch`].
5724 ///
5725 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5726 pub async fn where_not_like(
5727 col: &str,
5728 pattern: impl ::core::convert::Into<::std::string::String>,
5729 pool: &#root::sql::Pool,
5730 ) -> ::core::result::Result<
5731 ::std::vec::Vec<Self>,
5732 #root::sql::ExecError,
5733 > {
5734 use #root::sql::FetcherPool as _;
5735 let _key = ::std::format!("{}__not_like", col);
5736 #root::query::QuerySet::<Self>::default()
5737 .filter(
5738 &_key,
5739 #root::core::SqlValue::String(pattern.into()),
5740 )
5741 .fetch(pool)
5742 .await
5743 }
5744
5745 /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5746 /// case-insensitive `NOT LIKE` (PG native, MySQL /
5747 /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5748 ///
5749 /// # Errors
5750 /// As [`FetcherPool::fetch`].
5751 ///
5752 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5753 pub async fn where_not_ilike(
5754 col: &str,
5755 pattern: impl ::core::convert::Into<::std::string::String>,
5756 pool: &#root::sql::Pool,
5757 ) -> ::core::result::Result<
5758 ::std::vec::Vec<Self>,
5759 #root::sql::ExecError,
5760 > {
5761 use #root::sql::FetcherPool as _;
5762 let _key = ::std::format!("{}__not_ilike", col);
5763 #root::query::QuerySet::<Self>::default()
5764 .filter(
5765 &_key,
5766 #root::core::SqlValue::String(pattern.into()),
5767 )
5768 .fetch(pool)
5769 .await
5770 }
5771
5772 /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5773 /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5774 /// parity.
5775 ///
5776 /// # Errors
5777 /// As [`FetcherPool::fetch`].
5778 ///
5779 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5780 pub async fn where_not_between(
5781 col: &str,
5782 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5783 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5784 pool: &#root::sql::Pool,
5785 ) -> ::core::result::Result<
5786 ::std::vec::Vec<Self>,
5787 #root::sql::ExecError,
5788 > {
5789 use #root::sql::FetcherPool as _;
5790 let _key = ::std::format!("{}__not_between", col);
5791 let _vals = #root::core::SqlValue::List(::std::vec![
5792 ::core::convert::Into::into(lo),
5793 ::core::convert::Into::into(hi),
5794 ]);
5795 #root::query::QuerySet::<Self>::default()
5796 .filter(&_key, _vals)
5797 .fetch(pool)
5798 .await
5799 }
5800
5801 /// Returns the SQL table name for this model. Eloquent
5802 /// `$model->getTable()` parity.
5803 #[must_use]
5804 pub fn table_name() -> &'static str {
5805 <Self as #root::core::Model>::SCHEMA.table
5806 }
5807
5808 /// Returns the SQL column name of this model's primary
5809 /// key, or `None` when the model has no
5810 /// `#[rustango(primary_key)]`. Eloquent
5811 /// `$model->getKeyName()` parity.
5812 #[must_use]
5813 pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5814 <Self as #root::core::Model>::SCHEMA
5815 .primary_key()
5816 .map(|f| f.column)
5817 }
5818
5819 /// Fetch every row where `<col> BETWEEN lo AND hi`
5820 /// (inclusive on both ends — same as SQL). Eloquent
5821 /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5822 ///
5823 /// # Errors
5824 /// As [`FetcherPool::fetch`].
5825 ///
5826 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5827 pub async fn where_between(
5828 col: &str,
5829 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5830 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5831 pool: &#root::sql::Pool,
5832 ) -> ::core::result::Result<
5833 ::std::vec::Vec<Self>,
5834 #root::sql::ExecError,
5835 > {
5836 use #root::sql::FetcherPool as _;
5837 let _key = ::std::format!("{}__between", col);
5838 let _vals = #root::core::SqlValue::List(::std::vec![
5839 ::core::convert::Into::into(lo),
5840 ::core::convert::Into::into(hi),
5841 ]);
5842 #root::query::QuerySet::<Self>::default()
5843 .filter(&_key, _vals)
5844 .fetch(pool)
5845 .await
5846 }
5847
5848 /// Fetch the first row where `<col> = <val>`. Returns
5849 /// `Ok(None)` when no row matches. Eloquent
5850 /// `Model::firstWhere($col, $val)` parity.
5851 ///
5852 /// Thin wrapper over `QuerySet::<Self>::default()
5853 /// .filter(col, val).first(pool)`. Use this when you
5854 /// want one row identified by a non-PK column (e.g.
5855 /// `User::first_where_pool("email", "x@y.com", &pool)`).
5856 ///
5857 /// `val` accepts any value `Into<SqlValue>` so plain
5858 /// strings, ints, UUIDs, etc. all work.
5859 ///
5860 /// # Errors
5861 /// As `QuerySet::first`.
5862 pub async fn first_where(
5863 col: &str,
5864 val: impl ::core::convert::Into<#root::core::SqlValue>,
5865 pool: &#root::sql::Pool,
5866 ) -> ::core::result::Result<
5867 ::core::option::Option<Self>,
5868 #root::sql::ExecError,
5869 > {
5870 #root::query::QuerySet::<Self>::default()
5871 .filter(col, val)
5872 .first(pool)
5873 .await
5874 }
5875
5876 #value_method
5877
5878 /// Fetch the row with the largest `field` value —
5879 /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
5880 /// `Ok(None)` for an empty table. Eloquent
5881 /// `Model::latest($field)->first()` parity.
5882 /// Thin wrapper over `QuerySet::<Self>::default()
5883 /// .latest(field, pool)`.
5884 ///
5885 /// **Field name** is the Rust field ident as a string
5886 /// (not the SQL column). Unknown fields surface as
5887 /// `ExecError::Query(QueryError::UnknownField)` at
5888 /// compile time.
5889 ///
5890 /// # Errors
5891 /// As `QuerySet::latest`.
5892 pub async fn latest(
5893 field: &str,
5894 pool: &#root::sql::Pool,
5895 ) -> ::core::result::Result<
5896 ::core::option::Option<Self>,
5897 #root::sql::ExecError,
5898 > {
5899 #root::query::QuerySet::<Self>::default()
5900 .latest(field, pool)
5901 .await
5902 }
5903
5904 /// Sibling of [`Self::latest_pool`] — fetches the row
5905 /// with the smallest `field` value (`ORDER BY <field>
5906 /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
5907 /// ->first()` parity.
5908 ///
5909 /// # Errors
5910 /// As [`Self::latest_pool`].
5911 pub async fn earliest(
5912 field: &str,
5913 pool: &#root::sql::Pool,
5914 ) -> ::core::result::Result<
5915 ::core::option::Option<Self>,
5916 #root::sql::ExecError,
5917 > {
5918 #root::query::QuerySet::<Self>::default()
5919 .earliest(field, pool)
5920 .await
5921 }
5922
5923 #count_method
5924
5925 /// `true` when the table contains at least one row.
5926 /// Eloquent `Model::query()->exists()` parity. Thin wrapper
5927 /// over `QuerySet::<Self>::default().exists(pool)`.
5928 ///
5929 /// # Errors
5930 /// As [`ExistsPool::exists`].
5931 ///
5932 /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
5933 pub async fn exists(
5934 pool: &#root::sql::Pool,
5935 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5936 use #root::sql::ExistsPool as _;
5937 #root::query::QuerySet::<Self>::default()
5938 .exists(pool)
5939 .await
5940 }
5941
5942 /// Inverse of [`Self::exists`] — returns `true` when
5943 /// the table has zero rows. Eloquent
5944 /// `Model::doesntExist()` parity.
5945 ///
5946 /// # Errors
5947 /// As [`Self::exists`].
5948 pub async fn doesnt_exist(
5949 pool: &#root::sql::Pool,
5950 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5951 Self::exists(pool).await.map(|e| !e)
5952 }
5953
5954 /// Eloquent `Model::query()->whereKey($pk)->exists()` —
5955 /// `true` when a row with primary key `pk` exists in
5956 /// the table. Sugar over
5957 /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
5958 ///
5959 /// Differs from [`Self::exists`] (which checks "any row
5960 /// in the table") by checking a specific PK existence.
5961 /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
5962 /// because the row is never materialized — the SQL is
5963 /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
5964 ///
5965 /// # Errors
5966 /// As [`#root::sql::ExistsPool::contains_pk`].
5967 pub async fn contains_pk(
5968 pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
5969 pool: &#root::sql::Pool,
5970 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5971 use #root::sql::ExistsPool as _;
5972 #root::query::QuerySet::<Self>::default()
5973 .contains_pk(pool, pk)
5974 .await
5975 }
5976
5977 #sum_method
5978 #avg_method
5979 #min_method
5980 #max_method
5981
5982 /// Internal: forward to
5983 /// [`#root::sql::model_shortcuts::aggregate_one_pool`].
5984 /// Backs `sum` / `avg` / `min` / `max`.
5985 #[doc(hidden)]
5986 pub async fn __aggregate_one_pool<U>(
5987 col: &str,
5988 build: fn(&'static str) -> #root::core::AggregateExpr,
5989 pool: &#root::sql::Pool,
5990 ) -> ::core::result::Result<
5991 ::core::option::Option<U>,
5992 #root::sql::ExecError,
5993 >
5994 where
5995 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
5996 + #root::sql::MaybeMyFromRow
5997 + #root::sql::MaybeSqliteFromRow
5998 + ::core::marker::Send
5999 + ::core::marker::Unpin,
6000 {
6001 #root::sql::model_shortcuts::aggregate_one_pool::<Self, U>(col, build, pool)
6002 .await
6003 }
6004
6005 /// Fetch every row of this model from `pool`. Eloquent
6006 /// `Model::all()` parity — a thin wrapper over
6007 /// `QuerySet::<Self>::default().fetch(pool)`.
6008 ///
6009 /// **Use with care on large tables** — there's no
6010 /// pagination or limit; the entire table is materialized
6011 /// into memory. For anything beyond fixture / lookup
6012 /// tables, page through `QuerySet::<Self>::default()
6013 /// .order_by(...).limit(N).offset(M).fetch(pool)`
6014 /// or stream via `.iterator(chunk_size)`.
6015 ///
6016 /// # Errors
6017 /// As [`FetcherPool::fetch`].
6018 ///
6019 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6020 pub async fn all(
6021 pool: &#root::sql::Pool,
6022 ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
6023 {
6024 use #root::sql::FetcherPool as _;
6025 #root::query::QuerySet::<Self>::default()
6026 .fetch(pool)
6027 .await
6028 }
6029
6030 /// Look up every row whose primary key is in `pks`.
6031 /// Returns the matching rows in **inventory** order — NOT
6032 /// the order of `pks`. Empty `pks` returns an empty
6033 /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
6034 /// with a list) parity.
6035 ///
6036 /// Thin wrapper over `QuerySet::<Self>::default()
6037 /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
6038 /// Caller-supplied PKs that don't match a row are
6039 /// silently skipped (the returned vec is shorter than
6040 /// the input list). For an order-preserving / "fail
6041 /// when any missing" variant, build the queryset
6042 /// explicitly with `in_bulk` instead.
6043 ///
6044 /// Accepts any iterable whose elements are
6045 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
6046 /// `Vec<Uuid>`, etc.
6047 ///
6048 /// # Errors
6049 /// As [`FetcherPool::fetch`].
6050 ///
6051 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6052 pub async fn find_many<V>(
6053 pks: impl ::core::iter::IntoIterator<Item = V>,
6054 pool: &#root::sql::Pool,
6055 ) -> ::core::result::Result<
6056 ::std::vec::Vec<Self>,
6057 #root::sql::ExecError,
6058 >
6059 where
6060 V: ::core::convert::Into<#root::core::SqlValue>,
6061 {
6062 use #root::sql::FetcherPool as _;
6063 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6064 pks.into_iter().map(::core::convert::Into::into).collect();
6065 if _values.is_empty() {
6066 return ::core::result::Result::Ok(::std::vec::Vec::new());
6067 }
6068 let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
6069 #root::query::QuerySet::<Self>::default()
6070 .filter(&_key, #root::core::SqlValue::List(_values))
6071 .fetch(pool)
6072 .await
6073 }
6074
6075 /// Look up the row whose primary key equals `pk`. Returns
6076 /// `Ok(None)` when no row matches, rather than erroring.
6077 /// Eloquent `Model::find`
6078 /// shape — accepts any value `Into<SqlValue>`.
6079 ///
6080 /// One-liner shortcut for the common
6081 /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6082 /// .limit(1).fetch(pool).await?.into_iter().next()`
6083 /// dance.
6084 ///
6085 /// # Errors
6086 /// As [`FetcherPool::fetch`].
6087 ///
6088 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6089 pub async fn find(
6090 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6091 pool: &#root::sql::Pool,
6092 ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6093 {
6094 use #root::sql::FetcherPool as _;
6095 let _pk_val: #root::core::SqlValue = pk.into();
6096 let mut _rows: ::std::vec::Vec<Self> =
6097 #root::query::QuerySet::<Self>::default()
6098 .filter(::core::stringify!(#pk_ident), _pk_val)
6099 .limit(1)
6100 .fetch(pool)
6101 .await?;
6102 ::core::result::Result::Ok(_rows.into_iter().next())
6103 }
6104
6105 /// Look up the row whose primary key equals `pk`. Errors
6106 /// when no row matches — the throwing counterpart of
6107 /// [`Self::find_pool`]. Eloquent `Model::findOrFail`
6108 /// parity.
6109 ///
6110 /// Translates the miss into
6111 /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6112 /// so callers can `?`-bubble straight through the typical
6113 /// `ExecError` error chain.
6114 ///
6115 /// # Errors
6116 /// As [`Self::find_pool`]; additionally
6117 /// [`sqlx::Error::RowNotFound`] when no row matches.
6118 ///
6119 /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6120 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6121 pub async fn find_or_fail(
6122 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6123 pool: &#root::sql::Pool,
6124 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6125 match Self::find(pk, pool).await? {
6126 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6127 ::core::option::Option::None => ::core::result::Result::Err(
6128 #root::sql::ExecError::Driver(
6129 #root::sql::sqlx::Error::RowNotFound,
6130 ),
6131 ),
6132 }
6133 }
6134
6135 /// Look up multiple rows by primary key, **failing** if
6136 /// any requested PK is missing. Eloquent
6137 /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6138 /// inventory order (NOT request order — same as
6139 /// [`Self::find_many`]).
6140 ///
6141 /// Empty `pks` returns an empty `Vec` (no rows requested
6142 /// → nothing to fail on).
6143 ///
6144 /// Differs from [`Self::find_many`] in that this method
6145 /// requires every PK to resolve. If even one is missing,
6146 /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6147 /// in `ExecError::Driver`). Rows are deduped at the SQL
6148 /// layer, so passing the same PK twice in `pks` counts as
6149 /// one expected row.
6150 ///
6151 /// # Errors
6152 /// As [`Self::find_many`], plus `RowNotFound` when the
6153 /// returned row count is less than the count of distinct
6154 /// requested PKs.
6155 pub async fn find_many_or_fail<V>(
6156 pks: impl ::core::iter::IntoIterator<Item = V>,
6157 pool: &#root::sql::Pool,
6158 ) -> ::core::result::Result<
6159 ::std::vec::Vec<Self>,
6160 #root::sql::ExecError,
6161 >
6162 where
6163 V: ::core::convert::Into<#root::core::SqlValue>,
6164 {
6165 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6166 pks.into_iter().map(::core::convert::Into::into).collect();
6167 if _values.is_empty() {
6168 return ::core::result::Result::Ok(::std::vec::Vec::new());
6169 }
6170 // Dedup the requested PK list before counting so
6171 // duplicate-PK requests don't false-fail (the SQL
6172 // `IN (...)` clause naturally dedups too).
6173 let mut _seen: ::std::collections::HashSet<
6174 ::std::string::String,
6175 > = ::std::collections::HashSet::new();
6176 for v in &_values {
6177 _seen.insert(v.to_display_string());
6178 }
6179 let _expected = _seen.len();
6180 let _rows = Self::find_many(_values, pool).await?;
6181 if _rows.len() < _expected {
6182 return ::core::result::Result::Err(
6183 #root::sql::ExecError::Driver(
6184 #root::sql::sqlx::Error::RowNotFound,
6185 ),
6186 );
6187 }
6188 ::core::result::Result::Ok(_rows)
6189 }
6190
6191 /// Find by primary key or run `fallback` to produce a
6192 /// default row to return. Eloquent
6193 /// `Model::findOr($pk, fn() => …)` parity.
6194 ///
6195 /// Unlike [`Self::find_or_fail_pool`] (which raises on
6196 /// miss), this is the "give me something sensible"
6197 /// branch: typical use is "fetch the user's row, else
6198 /// fall back to an anonymous/guest stub".
6199 ///
6200 /// `fallback` runs only when no row matches — the DB
6201 /// round-trip happens unconditionally.
6202 ///
6203 /// # Errors
6204 /// As [`Self::find_pool`].
6205 pub async fn find_or<F>(
6206 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6207 pool: &#root::sql::Pool,
6208 fallback: F,
6209 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6210 where
6211 F: ::core::ops::FnOnce() -> Self,
6212 {
6213 ::core::result::Result::Ok(
6214 Self::find(pk, pool).await?.unwrap_or_else(fallback),
6215 )
6216 }
6217
6218 /// Same as [`Self::find_or`] but also returns a `bool`
6219 /// indicating whether the row was found in the DB
6220 /// (`true`) or freshly built from `fallback` (`false`).
6221 /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6222 /// in PHP the returned model also exposes `->exists`;
6223 /// here that's surfaced as the second tuple element.
6224 ///
6225 /// Useful for edit-or-create form handlers where the
6226 /// caller needs to know whether to PATCH or POST when
6227 /// the user submits the form.
6228 ///
6229 /// # Errors
6230 /// As [`Self::find`].
6231 pub async fn find_or_new<F>(
6232 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6233 pool: &#root::sql::Pool,
6234 fallback: F,
6235 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6236 where
6237 F: ::core::ops::FnOnce() -> Self,
6238 {
6239 match Self::find(pk, pool).await? {
6240 ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6241 ::core::option::Option::None => {
6242 ::core::result::Result::Ok((fallback(), false))
6243 }
6244 }
6245 }
6246
6247 /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6248 /// [`Self::find_or_new`] but **persists** the new row
6249 /// when the PK isn't found. Returns `(row, exists: bool)`
6250 /// — `exists=true` when the PK was found,
6251 /// `false` when a fresh row was inserted.
6252 ///
6253 /// ```ignore
6254 /// let (post, found) = Post::find_or_insert(
6255 /// pk,
6256 /// &pool,
6257 /// || Post { id: Auto::default(), title: "new".into() },
6258 /// ).await?;
6259 /// // `found` true → returned existing row;
6260 /// // `found` false → fallback was inserted, `post.id` populated.
6261 /// ```
6262 ///
6263 /// **Caveat**: two concurrent calls that both miss the
6264 /// find can both INSERT, violating uniqueness. Wrap in a
6265 /// transaction or rely on a UNIQUE constraint + handle
6266 /// the conflict error if you need race-free semantics.
6267 ///
6268 /// # Errors
6269 /// As [`Self::find`] and [`Self::save_pool`].
6270 pub async fn find_or_insert<F>(
6271 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6272 pool: &#root::sql::Pool,
6273 fallback: F,
6274 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6275 where
6276 F: ::core::ops::FnOnce() -> Self,
6277 {
6278 if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6279 return ::core::result::Result::Ok((_row, true));
6280 }
6281 let mut _new = fallback();
6282 _new.save_pool(pool).await?;
6283 ::core::result::Result::Ok((_new, false))
6284 }
6285
6286 /// Fetch the first row of the table, or run `fallback`
6287 /// when the table is empty. Eloquent
6288 /// `Model::firstOr(fn() => …)` parity.
6289 ///
6290 /// # Errors
6291 /// As [`Self::first_pool`].
6292 pub async fn first_or<F>(
6293 pool: &#root::sql::Pool,
6294 fallback: F,
6295 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6296 where
6297 F: ::core::ops::FnOnce() -> Self,
6298 {
6299 // Route through the queryset rather than `Self::first`,
6300 // which is suppressed on models with a field named
6301 // `first` (field/shortcut collision guard).
6302 ::core::result::Result::Ok(
6303 #root::query::QuerySet::<Self>::default()
6304 .first(pool)
6305 .await?
6306 .unwrap_or_else(fallback),
6307 )
6308 }
6309
6310 /// Fetch exactly one row matching `<col> = <val>`. Errors
6311 /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6312 /// or more than one matches
6313 /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6314 /// Eloquent `Model::sole($col, $val)` parity.
6315 ///
6316 /// # Errors
6317 /// As [`Self::where_pool`] plus the explicit
6318 /// `RowNotFound` / `MultipleRowsReturned` cases above.
6319 pub async fn sole(
6320 col: &str,
6321 val: impl ::core::convert::Into<#root::core::SqlValue>,
6322 pool: &#root::sql::Pool,
6323 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6324 let mut _rows = Self::where_(col, val, pool).await?;
6325 match _rows.len() {
6326 0 => ::core::result::Result::Err(
6327 #root::sql::ExecError::Driver(
6328 #root::sql::sqlx::Error::RowNotFound,
6329 ),
6330 ),
6331 1 => ::core::result::Result::Ok(_rows.remove(0)),
6332 n => ::core::result::Result::Err(
6333 #root::sql::ExecError::MultipleRowsReturned {
6334 op: "sole",
6335 table: <Self as #root::core::Model>::SCHEMA.name,
6336 count: n,
6337 },
6338 ),
6339 }
6340 }
6341 }
6342 } else {
6343 quote!()
6344 };
6345
6346 // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6347 // the non-audited `_pool` methods but execute against an open
6348 // `PoolTx` so the writes participate in the caller's transaction.
6349 // Auditing inside TX is deferred; these always use the plain
6350 // executor primitives regardless of whether the model is audited.
6351 let tx_insert_method = if fields.has_auto {
6352 let pushes = &fields.insert_pushes;
6353 let returning_cols = &fields.returning_cols;
6354 quote! {
6355 /// Insert this row inside an open transaction, populating
6356 /// any `Auto<T>` PK from the auto-assigned value. Works
6357 /// against any backend that `tx` wraps.
6358 ///
6359 /// # Errors
6360 /// As [`Self::insert_pool`].
6361 pub async fn insert_tx(
6362 &mut self,
6363 tx: &mut #root::sql::PoolTx<'_>,
6364 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6365 let mut _columns: ::std::vec::Vec<&'static str> =
6366 ::std::vec::Vec::new();
6367 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6368 ::std::vec::Vec::new();
6369 #( #pushes )*
6370 let _query = #root::core::InsertQuery {
6371 model: <Self as #root::core::Model>::SCHEMA,
6372 columns: _columns,
6373 values: _values,
6374 returning: ::std::vec![ #( #returning_cols ),* ],
6375 on_conflict: ::core::option::Option::None,
6376 };
6377 let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6378 #root::sql::apply_auto_pk(_result, self)
6379 }
6380 }
6381 } else {
6382 let insert_columns = &fields.insert_columns;
6383 let insert_values = &fields.insert_values;
6384 quote! {
6385 /// Insert this row inside an open transaction.
6386 ///
6387 /// # Errors
6388 /// As [`Self::insert_pool`].
6389 pub async fn insert_tx(
6390 &self,
6391 tx: &mut #root::sql::PoolTx<'_>,
6392 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6393 let _query = #root::core::InsertQuery {
6394 model: <Self as #root::core::Model>::SCHEMA,
6395 columns: ::std::vec![ #( #insert_columns ),* ],
6396 values: ::std::vec![ #( #insert_values ),* ],
6397 returning: ::std::vec::Vec::new(),
6398 on_conflict: ::core::option::Option::None,
6399 };
6400 #root::sql::insert_tx(tx, &_query).await
6401 }
6402 }
6403 };
6404
6405 let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6406 let pk_column_lit = pk_col.as_str();
6407 let assignments = &fields.update_assignments;
6408 let dispatch_unset = if fields.pk_is_auto {
6409 quote! {
6410 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6411 return self.insert_tx(tx).await.map(|()| 1u64);
6412 }
6413 }
6414 } else {
6415 quote!()
6416 };
6417 quote! {
6418 /// Save this row inside an open transaction. `INSERT` when
6419 /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6420 /// PK. Works against any backend that `tx` wraps.
6421 ///
6422 /// # Errors
6423 /// As [`Self::save_pool`].
6424 pub async fn save_tx(
6425 &mut self,
6426 tx: &mut #root::sql::PoolTx<'_>,
6427 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6428 #dispatch_unset
6429 let _query = #root::core::UpdateQuery {
6430 model: <Self as #root::core::Model>::SCHEMA,
6431 set: ::std::vec![ #( #assignments ),* ],
6432 where_clause: #root::core::WhereExpr::Predicate(
6433 #root::core::Filter {
6434 column: #pk_column_lit,
6435 op: #root::core::Op::Eq,
6436 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6437 ::core::clone::Clone::clone(&self.#pk_ident)
6438 ),
6439 }
6440 ),
6441 };
6442 let _affected = #root::sql::update_tx(tx, &_query).await?;
6443 ::core::result::Result::Ok(_affected)
6444 }
6445 }
6446 } else {
6447 quote!()
6448 };
6449
6450 let tx_delete_method = {
6451 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6452 let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6453 if let Some(pk_ident) = pk_ident_for_tx {
6454 quote! {
6455 /// Delete the row identified by this instance's PK
6456 /// inside an open transaction. Works against any backend
6457 /// that `tx` wraps.
6458 ///
6459 /// # Errors
6460 /// As [`Self::delete_pool`].
6461 pub async fn delete_tx(
6462 &self,
6463 tx: &mut #root::sql::PoolTx<'_>,
6464 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6465 let _query = #root::core::DeleteQuery {
6466 model: <Self as #root::core::Model>::SCHEMA,
6467 where_clause: #root::core::WhereExpr::Predicate(
6468 #root::core::Filter {
6469 column: #pk_column_lit,
6470 op: #root::core::Op::Eq,
6471 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6472 ::core::clone::Clone::clone(&self.#pk_ident)
6473 ),
6474 }
6475 ),
6476 };
6477 #root::sql::delete_tx(tx, &_query).await
6478 }
6479 }
6480 } else {
6481 quote!()
6482 }
6483 };
6484
6485 // Update emission captures both BEFORE and AFTER state — runs an
6486 // extra SELECT against `_executor` BEFORE the UPDATE, captures
6487 // each tracked field's prior value, then after the UPDATE diffs
6488 // against the in-memory `&self`. `diff_changes` drops unchanged
6489 // columns so the JSON only contains the actual delta.
6490 //
6491 // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6492 // and binds `_audit_before_pairs`; `audit_update_post` runs
6493 // after the UPDATE and emits the PendingEntry.
6494 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6495 audited_fields
6496 {
6497 if tracked.is_empty() {
6498 (quote!(), quote!())
6499 } else {
6500 let select_cols: String = tracked
6501 .iter()
6502 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
6503 .collect::<Vec<_>>()
6504 .join(", ");
6505 let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6506 let select_cols_lit = select_cols;
6507 let pk_column_lit_for_select = pk_column_for_select;
6508 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6509 if fields.pk_is_auto {
6510 quote!(self.#pk_ident.get().copied().unwrap_or_default())
6511 } else {
6512 quote!(::core::clone::Clone::clone(&self.#pk_ident))
6513 }
6514 } else {
6515 quote!(0_i64)
6516 };
6517 let before_pairs = tracked.iter().map(|c| {
6518 let column_lit = c.column.as_str();
6519 let value_ty = &c.value_ty;
6520 quote! {
6521 (
6522 #column_lit,
6523 match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6524 &_audit_before_row, #column_lit,
6525 ) {
6526 ::core::result::Result::Ok(v) => {
6527 #root::__serde_json::to_value(&v)
6528 .unwrap_or(#root::__serde_json::Value::Null)
6529 }
6530 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6531 },
6532 )
6533 }
6534 });
6535 let after_pairs = tracked.iter().map(|c| {
6536 let column_lit = c.column.as_str();
6537 let ident = &c.ident;
6538 quote! {
6539 (
6540 #column_lit,
6541 #root::__serde_json::to_value(&self.#ident)
6542 .unwrap_or(#root::__serde_json::Value::Null),
6543 )
6544 }
6545 });
6546 let pk_str = audit_pk_to_string.clone();
6547 let pre = quote! {
6548 let _audit_select_sql = ::std::format!(
6549 r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
6550 #select_cols_lit,
6551 <Self as #root::core::Model>::SCHEMA.table,
6552 #pk_column_lit_for_select,
6553 );
6554 let _audit_before_pairs:
6555 ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6556 match #root::sql::sqlx::query(&_audit_select_sql)
6557 .bind(#pk_value_for_bind)
6558 .fetch_optional(&mut *_executor)
6559 .await
6560 {
6561 ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
6562 ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
6563 }
6564 _ => ::core::option::Option::None,
6565 };
6566 };
6567 let post = quote! {
6568 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6569 let _audit_after:
6570 ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
6571 ::std::vec![ #( #after_pairs ),* ];
6572 let _audit_entry = #root::audit::PendingEntry {
6573 entity_table: <Self as #root::core::Model>::SCHEMA.table,
6574 entity_pk: #pk_str,
6575 operation: #root::audit::AuditOp::Update,
6576 source: #root::audit::current_source(),
6577 changes: #root::audit::diff_changes(
6578 &_audit_before,
6579 &_audit_after,
6580 ),
6581 };
6582 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6583 }
6584 };
6585 (pre, post)
6586 }
6587 } else {
6588 (quote!(), quote!())
6589 };
6590
6591 // Bulk-insert audit: capture every row's tracked fields after the
6592 // RETURNING populates each PK, then push one batched INSERT INTO
6593 // audit_log via `emit_many`. One round-trip regardless of N rows.
6594 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6595 let row_pk_str = if let Some((pk_ident, _)) = primary_key {
6596 if fields.pk_is_auto {
6597 quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
6598 } else {
6599 quote!(::std::format!("{}", &_row.#pk_ident))
6600 }
6601 } else {
6602 quote!(::std::string::String::new())
6603 };
6604 let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
6605 let column_lit = c.column.as_str();
6606 let ident = &c.ident;
6607 quote! {
6608 (
6609 #column_lit,
6610 #root::__serde_json::to_value(&_row.#ident)
6611 .unwrap_or(#root::__serde_json::Value::Null),
6612 )
6613 }
6614 });
6615 quote! {
6616 let _audit_source = #root::audit::current_source();
6617 let mut _audit_entries:
6618 ::std::vec::Vec<#root::audit::PendingEntry> =
6619 ::std::vec::Vec::with_capacity(rows.len());
6620 for _row in rows.iter() {
6621 _audit_entries.push(#root::audit::PendingEntry {
6622 entity_table: <Self as #root::core::Model>::SCHEMA.table,
6623 entity_pk: #row_pk_str,
6624 operation: #root::audit::AuditOp::Create,
6625 source: _audit_source.clone(),
6626 changes: #root::audit::snapshot_changes(&[
6627 #( #row_pairs ),*
6628 ]),
6629 });
6630 }
6631 #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6632 }
6633 } else {
6634 quote!()
6635 };
6636
6637 let save_method = if fields.pk_is_auto {
6638 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6639 let pk_column_lit = pk_column.as_str();
6640 let assignments = &fields.update_assignments;
6641 let upsert_cols = &fields.upsert_update_columns;
6642 let upsert_pushes = &fields.insert_pushes;
6643 let upsert_returning = &fields.returning_cols;
6644 let upsert_auto_assigns = &fields.auto_assigns;
6645 // Conflict target: prefer the first declared `unique_together`
6646 // when it exists. Plain `Auto<T>` PKs are server-assigned via
6647 // `BIGSERIAL` and never collide on insert, so a PK-only target
6648 // would silently turn `upsert()` into "always-insert" for
6649 // surrogate-PK models with composite UNIQUE constraints — see
6650 // `RolePermission` / `UserRole` / `UserPermission` in the
6651 // tenancy permission engine. When no `unique_together` is
6652 // declared we keep the PK target (the original behaviour).
6653 let upsert_target_columns: Vec<String> = indexes
6654 .iter()
6655 .find(|i| i.unique && !i.columns.is_empty())
6656 .map(|i| i.columns.clone())
6657 .unwrap_or_else(|| vec![pk_column.clone()]);
6658 let upsert_target_lits = upsert_target_columns
6659 .iter()
6660 .map(String::as_str)
6661 .collect::<Vec<_>>();
6662 let conflict_clause = if fields.upsert_update_columns.is_empty() {
6663 quote!(#root::core::ConflictClause::DoNothing)
6664 } else {
6665 quote!(#root::core::ConflictClause::DoUpdate {
6666 target: ::std::vec![ #( #upsert_target_lits ),* ],
6667 update_columns: ::std::vec![ #( #upsert_cols ),* ],
6668 })
6669 };
6670 Some(quote! {
6671 /// Insert this row if its `Auto<T>` primary key is
6672 /// `Unset`, otherwise update the existing row matching the
6673 /// PK. The caller doesn't need to pick `insert` vs the
6674 /// bulk-update path manually.
6675 ///
6676 /// On the insert branch, populates the PK from `RETURNING`
6677 /// (same behavior as `insert`). On the update branch,
6678 /// writes every non-PK column back; if no row matches the
6679 /// PK, returns `Ok(())` silently.
6680 ///
6681 /// Only generated when the primary key is declared as
6682 /// `Auto<T>`. Models with a manually-managed PK must use
6683 /// `insert` or the QuerySet update builder.
6684 ///
6685 /// **Postgres only — prefer [`Self::save_pool`].** This takes a
6686 /// driver-specific `PgPool`, so it does not exist at all on a
6687 /// build without the `postgres` feature: selecting `sqlite`
6688 /// makes the method vanish rather than fail with anything that
6689 /// names the cause.
6690 ///
6691 /// [`Self::save_pool`] takes `rustango::sql::Pool` and works on
6692 /// all three backends. The naming is inverted — the short name
6693 /// is the narrow one — and that is tracked in #1293.
6694 ///
6695 /// # Errors
6696 /// Returns [`#root::sql::ExecError`] for SQL-writing
6697 /// or driver failures.
6698 #[cfg(feature = "postgres")]
6699 pub async fn save(
6700 &mut self,
6701 pool: &#root::sql::sqlx::PgPool,
6702 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6703 #pool_to_save_on
6704 }
6705
6706 /// Like [`Self::save`] but accepts any sqlx executor —
6707 /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6708 /// escape hatch for tenant-scoped writes: schema-mode
6709 /// tenants share the registry pool but rely on a per-
6710 /// checkout `SET search_path`, so passing `&PgPool` would
6711 /// silently hit the wrong schema. Acquire a connection
6712 /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6713 ///
6714 /// # Errors
6715 /// As [`Self::save`].
6716 #[cfg(feature = "postgres")]
6717 pub async fn save_on #executor_generics (
6718 &mut self,
6719 #executor_param,
6720 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6721 #executor_where
6722 {
6723 // #1029 — INSERT writes exactly one row → 1; UPDATE returns
6724 // the rows-affected count (0 when the PK no longer exists).
6725 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6726 return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
6727 }
6728 #audit_update_pre
6729 let _query = #root::core::UpdateQuery {
6730 model: <Self as #root::core::Model>::SCHEMA,
6731 set: ::std::vec![ #( #assignments ),* ],
6732 where_clause: #root::core::WhereExpr::Predicate(
6733 #root::core::Filter {
6734 column: #pk_column_lit,
6735 op: #root::core::Op::Eq,
6736 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6737 ::core::clone::Clone::clone(&self.#pk_ident)
6738 ),
6739 }
6740 ),
6741 };
6742 let _affected = #root::sql::__macro_internals::update_on(
6743 #executor_passes_to_data_write,
6744 &_query,
6745 ).await?;
6746 #audit_update_post
6747 ::core::result::Result::Ok(_affected)
6748 }
6749
6750 /// Per-call override for the audit source. Runs
6751 /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6752 /// scope so the resulting audit entry records `source`
6753 /// instead of the task-local default. Useful for seed
6754 /// scripts and one-off CLI tools that don't sit inside an
6755 /// admin handler. The override applies only to this call;
6756 /// no global state changes.
6757 ///
6758 /// # Errors
6759 /// As [`Self::save_on`].
6760 #[cfg(feature = "postgres")]
6761 pub async fn save_on_with #executor_generics (
6762 &mut self,
6763 #executor_param,
6764 source: #root::audit::AuditSource,
6765 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6766 #executor_where
6767 {
6768 #root::audit::with_source(source, self.save_on(_executor)).await
6769 }
6770
6771 /// Insert this row or update it in-place if the primary key already
6772 /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6773 ///
6774 /// With `Auto::Unset` PK the server assigns a new key and no conflict
6775 /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6776 /// inserted if absent or all non-PK columns are overwritten if present.
6777 ///
6778 /// # Errors
6779 /// As [`Self::insert_on`].
6780 #[cfg(feature = "postgres")]
6781 pub async fn upsert(
6782 &mut self,
6783 pool: &#root::sql::sqlx::PgPool,
6784 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6785 #pool_to_upsert_on
6786 }
6787
6788 /// Like [`Self::upsert`] but accepts any sqlx executor.
6789 /// See [`Self::save_on`] for tenancy-scoped rationale.
6790 ///
6791 /// # Errors
6792 /// As [`Self::upsert`].
6793 #[cfg(feature = "postgres")]
6794 pub async fn upsert_on #executor_generics (
6795 &mut self,
6796 #executor_param,
6797 ) -> ::core::result::Result<(), #root::sql::ExecError>
6798 #executor_where
6799 {
6800 let mut _columns: ::std::vec::Vec<&'static str> =
6801 ::std::vec::Vec::new();
6802 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6803 ::std::vec::Vec::new();
6804 #( #upsert_pushes )*
6805 let query = #root::core::InsertQuery {
6806 model: <Self as #root::core::Model>::SCHEMA,
6807 columns: _columns,
6808 values: _values,
6809 returning: ::std::vec![ #( #upsert_returning ),* ],
6810 on_conflict: ::core::option::Option::Some(#conflict_clause),
6811 };
6812 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
6813 #executor_passes_to_data_write,
6814 &query,
6815 ).await?;
6816 let _returning_row = &_returning_row_v;
6817 #( #upsert_auto_assigns )*
6818 ::core::result::Result::Ok(())
6819 }
6820 })
6821 } else {
6822 None
6823 };
6824
6825 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6826 let pk_column_lit = pk_column.as_str();
6827 // Optional `soft_delete_on` / `restore_on` companions when the
6828 // model has a `#[rustango(soft_delete)]` column. They land
6829 // alongside the regular `delete_on` so callers have both
6830 // options — a hard delete (audit-tracked as a real DELETE) and
6831 // a logical delete (audit-tracked as an UPDATE setting the
6832 // deleted_at column to NOW()).
6833 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6834 let col_lit = col;
6835 let sd_field_ident = fields
6836 .soft_delete_field_ident
6837 .clone()
6838 .expect("soft_delete_column without ident");
6839 quote! {
6840 /// Soft-delete this row by setting its
6841 /// `#[rustango(soft_delete)]` column to `NOW()`.
6842 /// The row stays in the table; query helpers can
6843 /// filter it out by checking the column for `IS NOT
6844 /// NULL`.
6845 ///
6846 /// # Errors
6847 /// As [`Self::delete`].
6848 #[cfg(feature = "postgres")]
6849 pub async fn soft_delete_on #executor_generics (
6850 &self,
6851 #executor_param,
6852 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6853 #executor_where
6854 {
6855 let _query = #root::core::UpdateQuery {
6856 model: <Self as #root::core::Model>::SCHEMA,
6857 set: ::std::vec![
6858 #root::core::Assignment {
6859 column: #col_lit,
6860 value: ::core::convert::Into::<#root::core::Expr>::into(
6861 ::core::convert::Into::<#root::core::SqlValue>::into(
6862 #root::__chrono::Utc::now()
6863 )
6864 ),
6865 },
6866 ],
6867 where_clause: #root::core::WhereExpr::Predicate(
6868 #root::core::Filter {
6869 column: #pk_column_lit,
6870 op: #root::core::Op::Eq,
6871 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6872 ::core::clone::Clone::clone(&self.#pk_ident)
6873 ),
6874 }
6875 ),
6876 };
6877 let _affected = #root::sql::__macro_internals::update_on(
6878 #executor_passes_to_data_write,
6879 &_query,
6880 ).await?;
6881 #audit_softdelete_emit
6882 ::core::result::Result::Ok(_affected)
6883 }
6884
6885 /// Inverse of [`Self::soft_delete_on`] — clears the
6886 /// soft-delete column back to NULL so the row is
6887 /// considered live again.
6888 ///
6889 /// # Errors
6890 /// As [`Self::delete`].
6891 #[cfg(feature = "postgres")]
6892 pub async fn restore_on #executor_generics (
6893 &self,
6894 #executor_param,
6895 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6896 #executor_where
6897 {
6898 let _query = #root::core::UpdateQuery {
6899 model: <Self as #root::core::Model>::SCHEMA,
6900 set: ::std::vec![
6901 #root::core::Assignment {
6902 column: #col_lit,
6903 value: ::core::convert::Into::<#root::core::Expr>::into(
6904 #root::core::SqlValue::Null
6905 ),
6906 },
6907 ],
6908 where_clause: #root::core::WhereExpr::Predicate(
6909 #root::core::Filter {
6910 column: #pk_column_lit,
6911 op: #root::core::Op::Eq,
6912 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6913 ::core::clone::Clone::clone(&self.#pk_ident)
6914 ),
6915 }
6916 ),
6917 };
6918 let _affected = #root::sql::__macro_internals::update_on(
6919 #executor_passes_to_data_write,
6920 &_query,
6921 ).await?;
6922 #audit_restore_emit
6923 ::core::result::Result::Ok(_affected)
6924 }
6925
6926 /// Tri-dialect counterpart of [`Self::soft_delete_on`]
6927 /// — takes [`#root::sql::Pool`] and dispatches per
6928 /// backend. Eloquent `Model::delete()` semantics on
6929 /// soft-delete-enabled models (closes #821 partial).
6930 ///
6931 /// Sets the `#[rustango(soft_delete)]` column to
6932 /// `NOW()` on every backend. Query helpers
6933 /// (`QuerySet::active()` / `only_trashed()`,
6934 /// `soft_delete::active_filter` /
6935 /// `compose_with_active`) filter trashed rows out by
6936 /// reading `IS NULL` on the same column.
6937 ///
6938 /// # Errors
6939 /// As [`#root::sql::update_pool`].
6940 pub async fn soft_delete(
6941 &self,
6942 pool: &#root::sql::Pool,
6943 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6944 let _query = #root::core::UpdateQuery {
6945 model: <Self as #root::core::Model>::SCHEMA,
6946 set: ::std::vec![
6947 #root::core::Assignment {
6948 column: #col_lit,
6949 value: ::core::convert::Into::<#root::core::Expr>::into(
6950 ::core::convert::Into::<#root::core::SqlValue>::into(
6951 #root::__chrono::Utc::now()
6952 )
6953 ),
6954 },
6955 ],
6956 where_clause: #root::core::WhereExpr::Predicate(
6957 #root::core::Filter {
6958 column: #pk_column_lit,
6959 op: #root::core::Op::Eq,
6960 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6961 ::core::clone::Clone::clone(&self.#pk_ident)
6962 ),
6963 }
6964 ),
6965 };
6966 #root::sql::update_pool(pool, &_query).await
6967 }
6968
6969 /// Tri-dialect counterpart of [`Self::restore_on`].
6970 /// Clears the `#[rustango(soft_delete)]` column back
6971 /// to `NULL`, marking the row live again. Eloquent
6972 /// `Model::restore()` parity.
6973 ///
6974 /// # Errors
6975 /// As [`#root::sql::update_pool`].
6976 pub async fn restore(
6977 &self,
6978 pool: &#root::sql::Pool,
6979 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6980 let _query = #root::core::UpdateQuery {
6981 model: <Self as #root::core::Model>::SCHEMA,
6982 set: ::std::vec![
6983 #root::core::Assignment {
6984 column: #col_lit,
6985 value: ::core::convert::Into::<#root::core::Expr>::into(
6986 #root::core::SqlValue::Null
6987 ),
6988 },
6989 ],
6990 where_clause: #root::core::WhereExpr::Predicate(
6991 #root::core::Filter {
6992 column: #pk_column_lit,
6993 op: #root::core::Op::Eq,
6994 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6995 ::core::clone::Clone::clone(&self.#pk_ident)
6996 ),
6997 }
6998 ),
6999 };
7000 #root::sql::update_pool(pool, &_query).await
7001 }
7002
7003 /// Hard-delete this row, ignoring the soft-delete
7004 /// column. Eloquent `Model::forceDelete()` parity —
7005 /// the escape hatch when you need to actually purge
7006 /// data (GDPR, fixture cleanup, etc.).
7007 ///
7008 /// Equivalent to [`Self::delete_pool`] (the framework's
7009 /// non-soft delete) — exposed under the Eloquent name
7010 /// for muscle-memory + so soft-delete-enabled models
7011 /// have all three operations (soft / restore / force)
7012 /// in one place.
7013 ///
7014 /// # Errors
7015 /// As [`Self::delete_pool`].
7016 pub async fn force_delete(
7017 &self,
7018 pool: &#root::sql::Pool,
7019 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7020 Self::delete_pool(self, pool).await
7021 }
7022
7023 /// Returns `true` when this row is soft-deleted (its
7024 /// `#[rustango(soft_delete)]` column is currently
7025 /// set — Eloquent `$model->trashed()` parity).
7026 ///
7027 /// Pure in-memory predicate over `&self`; does not
7028 /// hit the database. Useful in admin/template code
7029 /// like `{% if post.trashed() %}…{% endif %}` and in
7030 /// guard clauses on restore/force-delete flows.
7031 pub fn trashed(&self) -> bool {
7032 ::core::option::Option::is_some(&self.#sd_field_ident)
7033 }
7034
7035 /// Fetch every row whose `#[rustango(soft_delete)]`
7036 /// column is `NULL` (a.k.a. the live, non-trashed
7037 /// rows). Eloquent's default `Model::all()` behavior on
7038 /// a soft-delete model (Eloquent auto-scopes trashed
7039 /// rows out; rustango doesn't have auto-scoping yet —
7040 /// see issue #820 — so this is the explicit shortcut).
7041 ///
7042 /// One-liner over `QuerySet::<Self>::default()
7043 /// .active().fetch(pool)`. Closes #821 partial.
7044 ///
7045 /// # Errors
7046 /// As [`#root::sql::FetcherPool::fetch`].
7047 pub async fn active(
7048 pool: &#root::sql::Pool,
7049 ) -> ::core::result::Result<
7050 ::std::vec::Vec<Self>,
7051 #root::sql::ExecError,
7052 > {
7053 use #root::sql::FetcherPool as _;
7054 #root::query::QuerySet::<Self>::default()
7055 .active()
7056 .fetch(pool)
7057 .await
7058 }
7059
7060 /// Fetch ONLY soft-deleted rows. Eloquent
7061 /// `Model::onlyTrashed()->get()` parity — drives the
7062 /// admin "Trash" page, restore flows, GDPR purge
7063 /// scans, etc.
7064 ///
7065 /// One-liner over `QuerySet::<Self>::default()
7066 /// .only_trashed().fetch(pool)`. Closes #821
7067 /// partial.
7068 ///
7069 /// # Errors
7070 /// As [`#root::sql::FetcherPool::fetch`].
7071 pub async fn only_trashed(
7072 pool: &#root::sql::Pool,
7073 ) -> ::core::result::Result<
7074 ::std::vec::Vec<Self>,
7075 #root::sql::ExecError,
7076 > {
7077 use #root::sql::FetcherPool as _;
7078 #root::query::QuerySet::<Self>::default()
7079 .only_trashed()
7080 .fetch(pool)
7081 .await
7082 }
7083
7084 /// Fetch every row, both live and soft-deleted.
7085 /// Eloquent `Model::withTrashed()->get()` parity.
7086 ///
7087 /// Today every queryset already includes trashed rows
7088 /// (rustango has no global-scope tracking yet — issue
7089 /// #820), so this is functionally equivalent to
7090 /// [`Self::all_pool`]. Exposed as a named shortcut so
7091 /// soft-delete-aware code reads `Model::with_trashed_pool`
7092 /// rather than `Model::all_pool` — keeps intent visible
7093 /// in callers and stays correct when auto-scoping lands.
7094 ///
7095 /// Closes #821 partial.
7096 ///
7097 /// # Errors
7098 /// As [`#root::sql::FetcherPool::fetch`].
7099 pub async fn with_trashed(
7100 pool: &#root::sql::Pool,
7101 ) -> ::core::result::Result<
7102 ::std::vec::Vec<Self>,
7103 #root::sql::ExecError,
7104 > {
7105 use #root::sql::FetcherPool as _;
7106 #root::query::QuerySet::<Self>::default()
7107 .with_trashed()
7108 .fetch(pool)
7109 .await
7110 }
7111
7112 }
7113 } else {
7114 quote!()
7115 };
7116 quote! {
7117 /// Delete the row identified by this instance's primary key.
7118 ///
7119 /// Returns the number of rows affected (0 or 1).
7120 ///
7121 /// **Postgres only — prefer [`Self::delete_pool`].** This takes
7122 /// a driver-specific `PgPool`, so it does not exist on a build
7123 /// without the `postgres` feature. [`Self::delete_pool`] takes
7124 /// `rustango::sql::Pool` and works on all three. The naming is
7125 /// inverted; tracked in #1293.
7126 ///
7127 /// # Errors
7128 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7129 /// driver failures.
7130 #[cfg(feature = "postgres")]
7131 pub async fn delete(
7132 &self,
7133 pool: &#root::sql::sqlx::PgPool,
7134 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7135 #pool_to_delete_on
7136 }
7137
7138 /// Like [`Self::delete`] but accepts any sqlx executor —
7139 /// for tenant-scoped deletes against an explicitly-acquired
7140 /// connection. See [`Self::save_on`] for the rationale.
7141 ///
7142 /// # Errors
7143 /// As [`Self::delete`].
7144 #[cfg(feature = "postgres")]
7145 pub async fn delete_on #executor_generics (
7146 &self,
7147 #executor_param,
7148 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7149 #executor_where
7150 {
7151 let query = #root::core::DeleteQuery {
7152 model: <Self as #root::core::Model>::SCHEMA,
7153 where_clause: #root::core::WhereExpr::Predicate(
7154 #root::core::Filter {
7155 column: #pk_column_lit,
7156 op: #root::core::Op::Eq,
7157 value: ::core::convert::Into::<#root::core::SqlValue>::into(
7158 ::core::clone::Clone::clone(&self.#pk_ident)
7159 ),
7160 }
7161 ),
7162 };
7163 let _affected = #root::sql::__macro_internals::delete_on(
7164 #executor_passes_to_data_write,
7165 &query,
7166 ).await?;
7167 #audit_delete_emit
7168 ::core::result::Result::Ok(_affected)
7169 }
7170
7171 /// Per-call audit-source override for [`Self::delete_on`].
7172 /// See [`Self::save_on_with`] for shape rationale.
7173 ///
7174 /// # Errors
7175 /// As [`Self::delete_on`].
7176 #[cfg(feature = "postgres")]
7177 pub async fn delete_on_with #executor_generics (
7178 &self,
7179 #executor_param,
7180 source: #root::audit::AuditSource,
7181 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7182 #executor_where
7183 {
7184 #root::audit::with_source(source, self.delete_on(_executor)).await
7185 }
7186 #pool_delete_method
7187 #pool_insert_method
7188 #pool_save_method
7189 #refresh_replicate_methods
7190 #tx_delete_method
7191 #tx_insert_method
7192 #tx_save_method
7193 #soft_delete_methods
7194
7195 /// Returns `true` when `other` represents the same DB
7196 /// row as `self` — i.e. their primary keys compare
7197 /// equal. Eloquent `$model->is($other)` parity.
7198 ///
7199 /// Because both arguments are typed `&Self`, the
7200 /// model/table check is automatic — `Post::is` cannot
7201 /// be invoked against a `Comment` at compile time. Only
7202 /// the PK has to be compared at runtime.
7203 pub fn is(&self, other: &Self) -> bool {
7204 self.#pk_ident == other.#pk_ident
7205 }
7206
7207 /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7208 /// parity.
7209 pub fn is_not(&self, other: &Self) -> bool {
7210 self.#pk_ident != other.#pk_ident
7211 }
7212
7213 /// Returns this row's primary-key value as an
7214 /// [`#root::core::SqlValue`]. Eloquent
7215 /// `$model->getKey()` parity.
7216 ///
7217 /// Useful when you need to thread the PK through a
7218 /// generic `Into<SqlValue>`-bound API without knowing the
7219 /// concrete PK type (`i64` vs `Uuid` vs `String`).
7220 #[must_use]
7221 pub fn get_key(&self) -> #root::core::SqlValue {
7222 ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7223 }
7224
7225 }
7226 });
7227
7228 let insert_method = if fields.has_auto {
7229 let pushes = &fields.insert_pushes;
7230 let returning_cols = &fields.returning_cols;
7231 let auto_assigns = &fields.auto_assigns;
7232 quote! {
7233 /// Insert this row into its table. Skips columns whose
7234 /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7235 /// sequence fills them in, then reads each `Auto` column
7236 /// back via `RETURNING` and stores it on `self`.
7237 ///
7238 /// **Postgres only — prefer [`Self::insert_pool`].** This takes
7239 /// a driver-specific `PgPool`, so it does not exist on a build
7240 /// without the `postgres` feature. [`Self::insert_pool`] takes
7241 /// `rustango::sql::Pool` and works on all three. The naming is
7242 /// inverted; tracked in #1293.
7243 ///
7244 /// # Errors
7245 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7246 /// driver failures.
7247 #[cfg(feature = "postgres")]
7248 pub async fn insert(
7249 &mut self,
7250 pool: &#root::sql::sqlx::PgPool,
7251 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7252 #pool_to_insert_on
7253 }
7254
7255 /// Like [`Self::insert`] but accepts any sqlx executor.
7256 /// See [`Self::save_on`] for tenancy-scoped rationale.
7257 ///
7258 /// # Errors
7259 /// As [`Self::insert`].
7260 #[cfg(feature = "postgres")]
7261 pub async fn insert_on #executor_generics (
7262 &mut self,
7263 #executor_param,
7264 ) -> ::core::result::Result<(), #root::sql::ExecError>
7265 #executor_where
7266 {
7267 let mut _columns: ::std::vec::Vec<&'static str> =
7268 ::std::vec::Vec::new();
7269 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7270 ::std::vec::Vec::new();
7271 #( #pushes )*
7272 let query = #root::core::InsertQuery {
7273 model: <Self as #root::core::Model>::SCHEMA,
7274 columns: _columns,
7275 values: _values,
7276 returning: ::std::vec![ #( #returning_cols ),* ],
7277 on_conflict: ::core::option::Option::None,
7278 };
7279 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7280 #executor_passes_to_data_write,
7281 &query,
7282 ).await?;
7283 let _returning_row = &_returning_row_v;
7284 #( #auto_assigns )*
7285 #audit_insert_emit
7286 ::core::result::Result::Ok(())
7287 }
7288
7289 /// Per-call audit-source override for [`Self::insert_on`].
7290 /// See [`Self::save_on_with`] for shape rationale.
7291 ///
7292 /// # Errors
7293 /// As [`Self::insert_on`].
7294 #[cfg(feature = "postgres")]
7295 pub async fn insert_on_with #executor_generics (
7296 &mut self,
7297 #executor_param,
7298 source: #root::audit::AuditSource,
7299 ) -> ::core::result::Result<(), #root::sql::ExecError>
7300 #executor_where
7301 {
7302 #root::audit::with_source(source, self.insert_on(_executor)).await
7303 }
7304 }
7305 } else {
7306 let insert_columns = &fields.insert_columns;
7307 let insert_values = &fields.insert_values;
7308 quote! {
7309 /// Insert this row into its table.
7310 ///
7311 /// # Errors
7312 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7313 /// driver failures.
7314 #[cfg(feature = "postgres")]
7315 pub async fn insert(
7316 &self,
7317 pool: &#root::sql::sqlx::PgPool,
7318 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7319 self.insert_on(pool).await
7320 }
7321
7322 /// Like [`Self::insert`] but accepts any sqlx executor.
7323 /// See [`Self::save_on`] for tenancy-scoped rationale.
7324 ///
7325 /// # Errors
7326 /// As [`Self::insert`].
7327 #[cfg(feature = "postgres")]
7328 pub async fn insert_on<'_c, _E>(
7329 &self,
7330 _executor: _E,
7331 ) -> ::core::result::Result<(), #root::sql::ExecError>
7332 where
7333 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7334 {
7335 let query = #root::core::InsertQuery {
7336 model: <Self as #root::core::Model>::SCHEMA,
7337 columns: ::std::vec![ #( #insert_columns ),* ],
7338 values: ::std::vec![ #( #insert_values ),* ],
7339 returning: ::std::vec::Vec::new(),
7340 on_conflict: ::core::option::Option::None,
7341 };
7342 #root::sql::__macro_internals::insert_on(_executor, &query).await
7343 }
7344 }
7345 };
7346
7347 let bulk_insert_method = if fields.has_auto {
7348 let cols_no_auto = &fields.bulk_columns_no_auto;
7349 let cols_all = &fields.bulk_columns_all;
7350 let pushes_no_auto = &fields.bulk_pushes_no_auto;
7351 let pushes_all = &fields.bulk_pushes_all;
7352 let returning_cols = &fields.returning_cols;
7353 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7354 let uniformity = &fields.bulk_auto_uniformity;
7355 let first_auto_ident = fields
7356 .first_auto_ident
7357 .as_ref()
7358 .expect("has_auto implies first_auto_ident is Some");
7359 quote! {
7360 /// Bulk-insert `rows` in a single round-trip. Every row's
7361 /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7362 /// (sequence fills them in) or uniformly `Auto::Set(_)`
7363 /// (caller-supplied values). Mixed Set/Unset is rejected
7364 /// — call `insert` per row for that case.
7365 ///
7366 /// Empty slice is a no-op. Each row's `Auto` fields are
7367 /// populated from the `RETURNING` clause in input order
7368 /// before this returns.
7369 ///
7370 /// # Errors
7371 /// Returns [`#root::sql::ExecError`] for validation,
7372 /// SQL-writing, mixed-Auto rejection, or driver failures.
7373 #[cfg(feature = "postgres")]
7374 pub async fn bulk_insert(
7375 rows: &mut [Self],
7376 pool: &#root::sql::sqlx::PgPool,
7377 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7378 #pool_to_bulk_insert_on
7379 }
7380
7381 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7382 /// See [`Self::save_on`] for tenancy-scoped rationale.
7383 ///
7384 /// # Errors
7385 /// As [`Self::bulk_insert`].
7386 #[cfg(feature = "postgres")]
7387 pub async fn bulk_insert_on #executor_generics (
7388 rows: &mut [Self],
7389 #executor_param,
7390 ) -> ::core::result::Result<(), #root::sql::ExecError>
7391 #executor_where
7392 {
7393 if rows.is_empty() {
7394 return ::core::result::Result::Ok(());
7395 }
7396 let _first_unset = matches!(
7397 rows[0].#first_auto_ident,
7398 #root::sql::Auto::Unset
7399 );
7400 #( #uniformity )*
7401
7402 let mut _all_rows: ::std::vec::Vec<
7403 ::std::vec::Vec<#root::core::SqlValue>,
7404 > = ::std::vec::Vec::with_capacity(rows.len());
7405 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7406 for _row in rows.iter() {
7407 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7408 ::std::vec::Vec::new();
7409 #( #pushes_no_auto )*
7410 _all_rows.push(_row_vals);
7411 }
7412 ::std::vec![ #( #cols_no_auto ),* ]
7413 } else {
7414 for _row in rows.iter() {
7415 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7416 ::std::vec::Vec::new();
7417 #( #pushes_all )*
7418 _all_rows.push(_row_vals);
7419 }
7420 ::std::vec![ #( #cols_all ),* ]
7421 };
7422
7423 let _query = #root::core::BulkInsertQuery {
7424 model: <Self as #root::core::Model>::SCHEMA,
7425 columns: _columns,
7426 rows: _all_rows,
7427 returning: ::std::vec![ #( #returning_cols ),* ],
7428 on_conflict: ::core::option::Option::None,
7429 };
7430 let _returned = #root::sql::__macro_internals::bulk_insert_on(
7431 #executor_passes_to_data_write,
7432 &_query,
7433 ).await?;
7434 if _returned.len() != rows.len() {
7435 return ::core::result::Result::Err(
7436 #root::sql::ExecError::Sql(
7437 #root::sql::SqlError::BulkInsertReturningMismatch {
7438 expected: rows.len(),
7439 actual: _returned.len(),
7440 }
7441 )
7442 );
7443 }
7444 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7445 #auto_assigns_for_row
7446 }
7447 #audit_bulk_insert_emit
7448 ::core::result::Result::Ok(())
7449 }
7450 }
7451 } else {
7452 let cols_all = &fields.bulk_columns_all;
7453 let pushes_all = &fields.bulk_pushes_all;
7454 quote! {
7455 /// Bulk-insert `rows` in a single round-trip. Every row's
7456 /// fields are written verbatim — there are no `Auto<T>`
7457 /// fields on this model.
7458 ///
7459 /// Empty slice is a no-op.
7460 ///
7461 /// # Errors
7462 /// Returns [`#root::sql::ExecError`] for validation,
7463 /// SQL-writing, or driver failures.
7464 #[cfg(feature = "postgres")]
7465 pub async fn bulk_insert(
7466 rows: &[Self],
7467 pool: &#root::sql::sqlx::PgPool,
7468 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7469 Self::bulk_insert_on(rows, pool).await
7470 }
7471
7472 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7473 /// See [`Self::save_on`] for tenancy-scoped rationale.
7474 ///
7475 /// # Errors
7476 /// As [`Self::bulk_insert`].
7477 #[cfg(feature = "postgres")]
7478 pub async fn bulk_insert_on<'_c, _E>(
7479 rows: &[Self],
7480 _executor: _E,
7481 ) -> ::core::result::Result<(), #root::sql::ExecError>
7482 where
7483 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7484 {
7485 if rows.is_empty() {
7486 return ::core::result::Result::Ok(());
7487 }
7488 let mut _all_rows: ::std::vec::Vec<
7489 ::std::vec::Vec<#root::core::SqlValue>,
7490 > = ::std::vec::Vec::with_capacity(rows.len());
7491 for _row in rows.iter() {
7492 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7493 ::std::vec::Vec::new();
7494 #( #pushes_all )*
7495 _all_rows.push(_row_vals);
7496 }
7497 let _query = #root::core::BulkInsertQuery {
7498 model: <Self as #root::core::Model>::SCHEMA,
7499 columns: ::std::vec![ #( #cols_all ),* ],
7500 rows: _all_rows,
7501 returning: ::std::vec::Vec::new(),
7502 on_conflict: ::core::option::Option::None,
7503 };
7504 let _ = #root::sql::__macro_internals::bulk_insert_on(_executor, &_query).await?;
7505 ::core::result::Result::Ok(())
7506 }
7507 }
7508 };
7509
7510 // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7511 // (no postgres-feature gate); routes through the existing
7512 // `bulk_insert_pool` + per-dialect conflict writer.
7513 //
7514 // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7515 // sequence picks the PK for fresh inserts; the UPDATE branch never
7516 // touches the Auto column.
7517 let bulk_upsert_pool_method = {
7518 // Pick the "no Auto" columns when the model has Auto fields,
7519 // else every column.
7520 let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7521 (
7522 fields.bulk_columns_no_auto.clone(),
7523 fields.bulk_pushes_no_auto.clone(),
7524 )
7525 } else {
7526 (
7527 fields.bulk_columns_all.clone(),
7528 fields.bulk_pushes_all.clone(),
7529 )
7530 };
7531 quote! {
7532 /// Tri-dialect bulk insert that updates on conflict — the
7533 /// "import a batch idempotently" shape. Issue #267 / T1.5.
7534 ///
7535 /// Per-row values are extracted and lowered into a
7536 /// [`#root::core::BulkInsertQuery`] with
7537 /// `on_conflict = DoUpdate { target, update_columns }`. The
7538 /// writer dispatches per-dialect:
7539 /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7540 /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)` (target ignored — MySQL matches every UNIQUE index)
7541 ///
7542 /// `target` names the column(s) whose unique constraint
7543 /// defines the conflict (typically a `unique` or
7544 /// `unique_together` natural-key column, NOT the `Auto<T>`
7545 /// PK). `update_cols` names the columns to overwrite on
7546 /// conflict — every other column is left untouched on the
7547 /// existing row.
7548 ///
7549 /// Auto-PK rows must all have `Auto::Unset` (the sequence
7550 /// picks the PK on insert; the update path never touches
7551 /// the Auto column). Auto-set rows trigger a hard error.
7552 /// Empty slice is a no-op.
7553 ///
7554 /// # Errors
7555 /// Returns [`#root::sql::ExecError`] for validation,
7556 /// SQL-writing, or driver failures.
7557 pub async fn bulk_upsert_pool(
7558 rows: &[Self],
7559 target: &[&'static str],
7560 update_cols: &[&'static str],
7561 pool: &#root::sql::Pool,
7562 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7563 if rows.is_empty() {
7564 return ::core::result::Result::Ok(());
7565 }
7566 let mut _all_rows: ::std::vec::Vec<
7567 ::std::vec::Vec<#root::core::SqlValue>,
7568 > = ::std::vec::Vec::with_capacity(rows.len());
7569 for _row in rows.iter() {
7570 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7571 ::std::vec::Vec::new();
7572 #( #upsert_pushes )*
7573 _all_rows.push(_row_vals);
7574 }
7575 let _query = #root::core::BulkInsertQuery {
7576 model: <Self as #root::core::Model>::SCHEMA,
7577 columns: ::std::vec![ #( #upsert_cols ),* ],
7578 rows: _all_rows,
7579 returning: ::std::vec::Vec::new(),
7580 on_conflict: ::core::option::Option::Some(
7581 #root::core::ConflictClause::DoUpdate {
7582 target: target.to_vec(),
7583 update_columns: update_cols.to_vec(),
7584 }
7585 ),
7586 };
7587 #root::sql::bulk_insert_pool(pool, &_query).await
7588 }
7589
7590 /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7591 /// skip rows that would violate a unique constraint. Issue
7592 /// #267 / T1.5. Same per-dialect dispatch as
7593 /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7594 /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7595 /// <pivot> = <pivot>` (MySQL no-op write).
7596 ///
7597 /// # Errors
7598 /// As [`Self::bulk_upsert_pool`].
7599 pub async fn bulk_insert_or_ignore_pool(
7600 rows: &[Self],
7601 pool: &#root::sql::Pool,
7602 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7603 if rows.is_empty() {
7604 return ::core::result::Result::Ok(());
7605 }
7606 let mut _all_rows: ::std::vec::Vec<
7607 ::std::vec::Vec<#root::core::SqlValue>,
7608 > = ::std::vec::Vec::with_capacity(rows.len());
7609 for _row in rows.iter() {
7610 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7611 ::std::vec::Vec::new();
7612 #( #upsert_pushes )*
7613 _all_rows.push(_row_vals);
7614 }
7615 let _query = #root::core::BulkInsertQuery {
7616 model: <Self as #root::core::Model>::SCHEMA,
7617 columns: ::std::vec![ #( #upsert_cols ),* ],
7618 rows: _all_rows,
7619 returning: ::std::vec::Vec::new(),
7620 on_conflict: ::core::option::Option::Some(
7621 #root::core::ConflictClause::DoNothing
7622 ),
7623 };
7624 #root::sql::bulk_insert_pool(pool, &_query).await
7625 }
7626 }
7627 };
7628
7629 // Ergonomic `Model::bulk_update(objs, fields)`.
7630 // The SQL/IR/executor stack
7631 // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7632 // `write_bulk_update_*` writers) already existed; what was missing
7633 // was the per-model constructor that maps `&[Self]` + a runtime
7634 // column list into rows of `[pk, col_vals…]` so callers don't
7635 // hand-build the IR. Emitted only when the model has a primary key
7636 // (the PK is the join key and can't itself be updated).
7637 let bulk_update_method = match &fields.primary_key {
7638 None => quote! {},
7639 Some((pk_ident, pk_col)) => {
7640 // One pair of match arms per non-PK column: a validation arm
7641 // resolving the runtime name to its `&'static str` column,
7642 // and a value arm pushing that field off the row.
7643 let mut col_arms: Vec<TokenStream2> = Vec::new();
7644 let mut val_arms: Vec<TokenStream2> = Vec::new();
7645 for entry in &fields.column_entries {
7646 if &entry.column == pk_col {
7647 continue;
7648 }
7649 let col = &entry.column;
7650 let ident = &entry.ident;
7651 col_arms.push(quote! { #col => #col, });
7652 val_arms.push(quote! {
7653 #col => _row_vals.push(
7654 ::core::convert::Into::<#root::core::SqlValue>::into(
7655 ::core::clone::Clone::clone(&_o.#ident)
7656 )
7657 ),
7658 });
7659 }
7660 quote! {
7661 /// Write per-row-different values for the named `fields`
7662 /// across every object in `objs` in a single statement,
7663 /// matched by primary key.
7664 ///
7665 /// `fields` names the **columns** to update. The primary
7666 /// key identifies each row and cannot itself be updated
7667 /// (pass it and you get
7668 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7669 /// Empty `objs` or `fields` is a no-op returning `0`.
7670 /// Objects whose PK matches no row are simply not updated.
7671 /// Returns the number of rows affected.
7672 ///
7673 /// Tri-dialect: lowers to one
7674 /// [`#root::core::BulkUpdateQuery`] and dispatches
7675 /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7676 /// a CTE + correlated subquery on SQLite, an inner
7677 /// `JOIN (VALUES …)` on MySQL.
7678 ///
7679 /// # Errors
7680 /// [`#root::core::QueryError::UnknownField`] for a name
7681 /// that isn't a column on this model,
7682 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7683 /// `fields` names the PK, or [`#root::sql::ExecError`] for
7684 /// SQL-writing / driver failures.
7685 pub async fn bulk_update(
7686 objs: &[Self],
7687 fields: &[&str],
7688 pool: &#root::sql::Pool,
7689 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7690 if objs.is_empty() || fields.is_empty() {
7691 return ::core::result::Result::Ok(0);
7692 }
7693 let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7694 let mut _update_columns: ::std::vec::Vec<&'static str> =
7695 ::std::vec::Vec::with_capacity(fields.len());
7696 for &_f in fields {
7697 let _col: &'static str = match _f {
7698 #pk_col => {
7699 return ::core::result::Result::Err(
7700 ::core::convert::Into::into(
7701 #root::core::QueryError::BulkUpdatePrimaryKey {
7702 model: _model_name,
7703 field: ::std::string::ToString::to_string(_f),
7704 }
7705 )
7706 );
7707 }
7708 #( #col_arms )*
7709 _ => {
7710 return ::core::result::Result::Err(
7711 ::core::convert::Into::into(
7712 #root::core::QueryError::UnknownField {
7713 model: _model_name,
7714 field: ::std::string::ToString::to_string(_f),
7715 }
7716 )
7717 );
7718 }
7719 };
7720 // A model whose only field is the primary key emits no
7721 // non-PK arms, so every arm above diverges and this push
7722 // is genuinely unreachable — correct, since such a model
7723 // has nothing to bulk-update. Scoped to the statement
7724 // rather than the fn so a real unreachability elsewhere
7725 // in this body still warns. Without it every PK-only
7726 // model warns in the *user's* crate, which they cannot
7727 // act on (#1290).
7728 #[allow(unreachable_code)]
7729 _update_columns.push(_col);
7730 }
7731 let mut _rows: ::std::vec::Vec<
7732 ::std::vec::Vec<#root::core::SqlValue>,
7733 > = ::std::vec::Vec::with_capacity(objs.len());
7734 for _o in objs.iter() {
7735 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7736 ::std::vec::Vec::with_capacity(fields.len() + 1);
7737 // PK first — the writers expect `[pk, …update cols]`.
7738 _row_vals.push(
7739 ::core::convert::Into::<#root::core::SqlValue>::into(
7740 ::core::clone::Clone::clone(&_o.#pk_ident)
7741 )
7742 );
7743 for &_f in fields {
7744 match _f {
7745 #( #val_arms )*
7746 // Unreachable: every name was validated
7747 // against the same arm set above.
7748 _ => {}
7749 }
7750 }
7751 _rows.push(_row_vals);
7752 }
7753 let _query = #root::core::BulkUpdateQuery {
7754 model: <Self as #root::core::Model>::SCHEMA,
7755 update_columns: _update_columns,
7756 rows: _rows,
7757 };
7758 #root::sql::bulk_update_pool(pool, &_query).await
7759 }
7760 }
7761 }
7762 };
7763
7764 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7765 quote! {
7766 /// Hidden runtime accessor for the primary-key value as a
7767 /// [`SqlValue`]. Used by reverse-relation helpers
7768 /// (`<parent>::<child>_set`) emitted from sibling models'
7769 /// FK fields. Not part of the public API.
7770 #[doc(hidden)]
7771 pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7772 ::core::convert::Into::<#root::core::SqlValue>::into(
7773 ::core::clone::Clone::clone(&self.#pk_ident)
7774 )
7775 }
7776 }
7777 });
7778
7779 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7780 quote! {
7781 impl #root::sql::HasPkValue for #struct_name {
7782 fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7783 ::core::convert::Into::<#root::core::SqlValue>::into(
7784 ::core::clone::Clone::clone(&self.#pk_ident)
7785 )
7786 }
7787 }
7788 }
7789 });
7790
7791 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7792
7793 // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7794 // dispatch to the right per-backend body without the macro emitting
7795 // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7796 // so audited models with non-Auto PKs (which still go through
7797 // `insert_one_with_audit` → `apply_auto_pk`) link.
7798 let assign_auto_pk_pool_impl = {
7799 let auto_assigns = &fields.auto_assigns;
7800 // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7801 // so the body is structurally identical to `auto_assigns` —
7802 // only the helper name swaps from `try_get_returning` to
7803 // `try_get_returning_sqlite` so the closure typechecks against
7804 // a `SqliteRow` instead of a `PgRow`.
7805 let auto_assigns_sqlite: Vec<TokenStream2> = fields
7806 .auto_field_idents
7807 .iter()
7808 .map(|(ident, column)| {
7809 quote! {
7810 self.#ident = #root::sql::try_get_returning_sqlite(
7811 _returning_row, #column
7812 )?;
7813 }
7814 })
7815 .collect();
7816 // #1028 — decode each `generated_as` column from the same
7817 // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7818 // own type drives the decode (no `Auto<T>` wrapper).
7819 let generated_assigns: Vec<TokenStream2> = fields
7820 .generated_field_idents
7821 .iter()
7822 .map(|(ident, column)| {
7823 quote! {
7824 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7825 }
7826 })
7827 .collect();
7828 let generated_assigns_sqlite: Vec<TokenStream2> = fields
7829 .generated_field_idents
7830 .iter()
7831 .map(|(ident, column)| {
7832 quote! {
7833 self.#ident = #root::sql::try_get_returning_sqlite(
7834 _returning_row, #column
7835 )?;
7836 }
7837 })
7838 .collect();
7839 let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
7840 // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7841 // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7842 // Auto<Uuid>/etc. fail to link against MySQL (intended —
7843 // those models can't use AUTO_INCREMENT). The trait is only
7844 // touched on the MySQL arm at runtime, so PG-only consumers
7845 // never see the bound failure.
7846 //
7847 // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7848 // Auto<i64> PK + auto_now_add timestamp) errored hard at
7849 // runtime with "multi-column RETURNING". MySQL has no
7850 // multi-column RETURNING semantic and a follow-up SELECT
7851 // would need cross-trait plumbing. Pragmatic shape: succeed
7852 // with the FIRST Auto field populated from LAST_INSERT_ID();
7853 // any other Auto fields stay `Auto::Unset`. Callers that
7854 // need the DB-defaulted timestamp / UUID can re-fetch the
7855 // row by PK after `save_pool`. Fixes the cookbook chapter
7856 // 12 dialect divergence.
7857 let value_ty = fields
7858 .first_auto_value_ty
7859 .as_ref()
7860 .expect("first_auto_value_ty set whenever first_auto_ident is");
7861 quote! {
7862 let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7863 ::rustango_from_mysql_auto_id(_id)?;
7864 self.#first = #root::sql::Auto::Set(_converted);
7865 ::core::result::Result::Ok(())
7866 }
7867 } else {
7868 quote! {
7869 let _ = _id;
7870 ::core::result::Result::Ok(())
7871 }
7872 };
7873 quote! {
7874 impl #root::sql::AssignAutoPkPool for #struct_name {
7875 fn __rustango_assign_from_pg_row(
7876 &mut self,
7877 _returning_row: &#root::sql::PgReturningRow,
7878 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7879 #( #auto_assigns )*
7880 #( #generated_assigns )*
7881 ::core::result::Result::Ok(())
7882 }
7883 fn __rustango_assign_from_mysql_id(
7884 &mut self,
7885 _id: i64,
7886 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7887 #mysql_body
7888 }
7889 fn __rustango_assign_from_sqlite_row(
7890 &mut self,
7891 _returning_row: &#root::sql::SqliteReturningRow,
7892 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7893 #( #auto_assigns_sqlite )*
7894 #( #generated_assigns_sqlite )*
7895 ::core::result::Result::Ok(())
7896 }
7897 }
7898 }
7899 };
7900
7901 let from_aliased_row_inits = &fields.from_aliased_row_inits;
7902 let aliased_row_helper = quote! {
7903 /// Decode a row's aliased target columns (produced by
7904 /// `select_related`'s LEFT JOIN) into a fresh instance of
7905 /// this model. Reads each column via
7906 /// `format!("{prefix}__{col}")`, matching the alias the
7907 /// SELECT writer emitted. Slice 9.0d.
7908 #[doc(hidden)]
7909 #[cfg(feature = "postgres")]
7910 pub fn __rustango_from_aliased_row(
7911 row: &#root::sql::sqlx::postgres::PgRow,
7912 prefix: &str,
7913 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7914 ::core::result::Result::Ok(Self {
7915 #( #from_aliased_row_inits ),*
7916 })
7917 }
7918 };
7919 // v0.23.0-batch8 — MySQL counterpart, gated through the
7920 // cfg-aware macro_rules so PG-only builds expand to nothing.
7921 let aliased_row_helper_my = quote! {
7922 #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7923 #( #from_aliased_row_inits ),*
7924 });
7925 };
7926
7927 // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7928 // pattern + cfg gate on the `sqlite` feature.
7929 let aliased_row_helper_sqlite = quote! {
7930 #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7931 #( #from_aliased_row_inits ),*
7932 });
7933 };
7934
7935 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7936 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7937 let load_related_impl_sqlite =
7938 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7939
7940 // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7941 // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7942 // default `Self::objects()`. Each accessor returns a fresh
7943 // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7944 // methods the user defined.
7945 let extra_manager_fns: Vec<TokenStream2> = manager_fns
7946 .iter()
7947 .map(|fn_ident| {
7948 let model_name_str = struct_name.to_string();
7949 let fn_name_str = fn_ident.to_string();
7950 let doc = format!(
7951 "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7952 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7953 equivalent to `Self::objects()`. Chains with any \
7954 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7955 extension methods."
7956 );
7957 quote! {
7958 #[doc = #doc]
7959 #[must_use]
7960 pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7961 #root::query::QuerySet::new()
7962 }
7963 }
7964 })
7965 .collect();
7966
7967 quote! {
7968 impl #struct_name {
7969 /// Start a new `QuerySet` over this model.
7970 #[must_use]
7971 pub fn objects() -> #root::query::QuerySet<#struct_name> {
7972 #root::query::QuerySet::new()
7973 }
7974
7975 /// Eloquent-shape alias of [`Self::objects`]. Returns
7976 /// a fresh `QuerySet<Self>` ready for `.filter()` /
7977 /// `.where_()` / etc. Matches Laravel muscle-memory:
7978 ///
7979 /// ```ignore
7980 /// // Eloquent: Post::query()->where('published', true)
7981 /// // rustango: Post::query().filter("published", true)
7982 /// // or: Post::objects().filter("published", true)
7983 /// ```
7984 ///
7985 /// Both names point at the same underlying constructor;
7986 /// neither is preferred.
7987 #[must_use]
7988 pub fn query() -> #root::query::QuerySet<#struct_name> {
7989 #root::query::QuerySet::new()
7990 }
7991
7992 #( #extra_manager_fns )*
7993
7994 #insert_method
7995
7996 #bulk_insert_method
7997
7998 #bulk_upsert_pool_method
7999
8000 #bulk_update_method
8001
8002 #save_method
8003
8004 #pk_methods
8005
8006 #pk_value_helper
8007
8008 #aliased_row_helper
8009
8010 #column_consts
8011 }
8012
8013 #aliased_row_helper_my
8014
8015 #aliased_row_helper_sqlite
8016
8017 #load_related_impl
8018
8019 #load_related_impl_my
8020
8021 #load_related_impl_sqlite
8022
8023 #has_pk_value_impl
8024
8025 #fk_pk_access_impl
8026
8027 #assign_auto_pk_pool_impl
8028 }
8029}
8030
8031/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
8032/// `auto_assigns` but reading from `_returning_row` and writing to
8033/// `_row_mut` instead of `self`.
8034fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
8035 let root = rustango_root();
8036 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
8037 let col_lit = column.as_str();
8038 quote! {
8039 _row_mut.#ident = #root::sql::sqlx::Row::try_get(
8040 _returning_row,
8041 #col_lit,
8042 )?;
8043 }
8044 });
8045 quote! { #( #lines )* }
8046}
8047
8048/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
8049fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
8050 let lines = entries.iter().map(|e| {
8051 let ident = &e.ident;
8052 let col_ty = column_type_ident(ident);
8053 quote! {
8054 #[allow(non_upper_case_globals)]
8055 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
8056 }
8057 });
8058 quote! { #(#lines)* }
8059}
8060
8061/// Emit a hidden per-model module carrying one zero-sized type per field,
8062/// each with a `Column` impl pointing back at the model.
8063fn column_module_tokens(
8064 module_ident: &syn::Ident,
8065 struct_name: &syn::Ident,
8066 entries: &[ColumnEntry],
8067) -> TokenStream2 {
8068 let root = rustango_root();
8069 let items = entries.iter().map(|e| {
8070 let col_ty = column_type_ident(&e.ident);
8071 let value_ty = &e.value_ty;
8072 let name = &e.name;
8073 let column = &e.column;
8074 let field_type_tokens = &e.field_type_tokens;
8075 quote! {
8076 #[derive(::core::clone::Clone, ::core::marker::Copy)]
8077 pub struct #col_ty;
8078
8079 impl #root::core::Column for #col_ty {
8080 type Model = super::#struct_name;
8081 type Value = #value_ty;
8082 const NAME: &'static str = #name;
8083 const COLUMN: &'static str = #column;
8084 const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
8085 }
8086 }
8087 });
8088 quote! {
8089 #[doc(hidden)]
8090 #[allow(non_camel_case_types, non_snake_case)]
8091 pub mod #module_ident {
8092 // Re-import the parent scope so field types referencing
8093 // sibling models (e.g. `ForeignKey<Author>`) resolve
8094 // inside this submodule. Without this we'd hit
8095 // `proc_macro_derive_resolution_fallback` warnings.
8096 #[allow(unused_imports)]
8097 use super::*;
8098 #(#items)*
8099 }
8100 }
8101}
8102
8103fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
8104 syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
8105}
8106
8107fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8108 syn::Ident::new(
8109 &format!("__rustango_cols_{struct_name}"),
8110 struct_name.span(),
8111 )
8112}
8113
8114fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
8115 let root = rustango_root();
8116 // The Postgres impl is always emitted — every rustango build pulls in
8117 // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8118 // routed through `#root::__impl_my_from_row!`, a cfg-gated
8119 // macro_rules whose body collapses to nothing when rustango is built
8120 // without the `mysql` feature. No user-facing feature shim required.
8121 //
8122 // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8123 // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8124 // back into a comma-separated list inside square brackets. Since each
8125 // entry is already in `field: expr` shape, the existing tokens slot in.
8126 quote! {
8127 #[cfg(feature = "postgres")]
8128 impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8129 for #struct_name
8130 {
8131 fn from_row(
8132 row: &'r #root::sql::sqlx::postgres::PgRow,
8133 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8134 ::core::result::Result::Ok(Self {
8135 #( #from_row_inits ),*
8136 })
8137 }
8138 }
8139
8140 #root::__impl_my_from_row!(#struct_name, |row| {
8141 #( #from_row_inits ),*
8142 });
8143
8144 #root::__impl_sqlite_from_row!(#struct_name, |row| {
8145 #( #from_row_inits ),*
8146 });
8147 }
8148}
8149
8150struct ContainerAttrs {
8151 table: Option<String>,
8152 display: Option<(String, proc_macro2::Span)>,
8153 /// Explicit app label from `#[rustango(app = "blog")]`.
8154 /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8155 /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8156 /// at runtime — this attribute is the override for cases where
8157 /// the inference is wrong (e.g. a model that conceptually belongs
8158 /// to one app but is physically in another module).
8159 app: Option<String>,
8160 /// Per-model admin knobs from
8161 /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8162 /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8163 /// admin code falls back to `AdminConfig::DEFAULT`.
8164 admin: Option<AdminAttrs>,
8165 /// Per-model audit configuration from `#[rustango(audit(...))]`.
8166 /// `None` when the model isn't audited — write paths emit no
8167 /// audit entries. When present, single-row writes capture
8168 /// before/after for the listed fields and bulk writes batch
8169 /// snapshots into one INSERT into `rustango_audit_log`.
8170 audit: Option<AuditAttrs>,
8171 /// `true` when `#[rustango(permissions)]` is present. Signals that
8172 /// `auto_create_permissions` should seed the four CRUD codenames for
8173 /// this model.
8174 permissions: bool,
8175 /// Many-to-many relations declared via
8176 /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8177 /// src = "post_id", dst = "tag_id"))]`.
8178 m2m: Vec<M2MAttr>,
8179 /// Polymorphic M2M relations declared via
8180 /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8181 /// pk_column = "taggable_id", ct_column = "taggable_type",
8182 /// related_column = "tag_id"))]` (issue #818).
8183 generic_m2m: Vec<GenericM2MAttr>,
8184 /// Composite indexes declared via
8185 /// `#[rustango(index("col1, col2"))]` or
8186 /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8187 /// Single-column indexes from `#[rustango(index)]` on fields are
8188 /// accumulated here during field collection.
8189 indexes: Vec<IndexAttr>,
8190 /// Table-level CHECK constraints declared via
8191 /// `#[rustango(check(name = "…", expr = "…"))]`.
8192 checks: Vec<CheckAttr>,
8193 /// Table-level PG `EXCLUDE` constraints declared via
8194 /// `#[rustango(exclude(name = "…", using = "gist", elements =
8195 /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8196 /// migration writer renders them on Postgres and skips with a
8197 /// warning on MySQL/SQLite. Issue #319.
8198 excludes: Vec<ExcludeAttr>,
8199 /// Composite (multi-column) FKs declared via
8200 /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8201 /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8202 composite_fks: Vec<CompositeFkAttr>,
8203 /// Generic ("any model") FKs declared via
8204 /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8205 /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8206 generic_fks: Vec<GenericFkAttr>,
8207 /// Where this model lives in a tenancy deployment, declared via
8208 /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8209 /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8210 /// to partition diff output between registry-scoped and
8211 /// tenant-scoped migration files.
8212 scope: Option<String>,
8213 /// Custom-Manager extension-trait name from
8214 /// `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8215 /// When set, the macro emits an empty `pub trait <name>: Sized {}`
8216 /// adjacent to the model so users can write
8217 /// `impl FooManagerExt for QuerySet<Foo> { fn published(self) -> Self ... }`
8218 /// and discover the convention from the model definition.
8219 manager_ext: Option<syn::Ident>,
8220 /// Extra QuerySet accessor names from
8221 /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8222 /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8223 /// the default `Self::objects()`. Multiple attributes allowed.
8224 manager_fns: Vec<syn::Ident>,
8225 /// Default ordering declared via `#[rustango(default_order =
8226 /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8227 /// `(column_name, desc_flag, span_for_error_reporting)` — the
8228 /// `-` prefix means descending; the `+` prefix or no prefix means
8229 /// ascending.
8230 default_order: Vec<(String, bool, proc_macro2::Span)>,
8231 /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8232 /// Routes the emitted schema's `is_view = true` so the migration
8233 /// snapshot skips this model (its underlying SQL view is operator-
8234 /// managed, not rustango-managed).
8235 is_view: bool,
8236 /// `#[rustango(managed = false)]` — who owns the table.
8237 /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8238 /// the migration snapshot skips this model so `makemigrations` /
8239 /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8240 /// TABLE` against it (operator-managed schema).
8241 managed: bool,
8242 /// Name of the model's base manager, from
8243 /// `#[rustango(base_manager_name = "...")]`. Threaded into
8244 /// `ModelSchema::base_manager_name`. Declarative-only today.
8245 base_manager_name: Option<String>,
8246 /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8247 /// field this model's instances are ordered relative to.
8248 /// Declarative-only today; threaded onto
8249 /// `ModelSchema::order_with_respect_to`.
8250 order_with_respect_to: Option<String>,
8251 /// `#[rustango(proxy)]` / `#[rustango(proxy = true)]`.
8252 /// Marks the model as a proxy that
8253 /// shares its DB table with another struct. Threaded into
8254 /// `ModelSchema::proxy` so future codegen can skip table-owning
8255 /// behavior for proxies.
8256 proxy: bool,
8257 /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8258 /// Each comma-separated capability token surfaces on
8259 /// `ModelSchema::required_db_features` so `manage check --deploy`
8260 /// can warn when the active dialect lacks one.
8261 required_db_features: Vec<String>,
8262 /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8263 /// Normalized to the dialect name `manage check --deploy`
8264 /// compares against `Settings.database.backend`. Aliases
8265 /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8266 /// stored under the canonical name.
8267 required_db_vendor: Option<String>,
8268 /// `#[rustango(default_related_name = "...")]`. Threaded into
8269 /// `ModelSchema::default_related_name`. Reverse-relation accessor
8270 /// name to use when an FK / M2M field doesn't override it.
8271 /// Today rustango doesn't auto-emit reverse managers; the
8272 /// metadata is the foundation for that work.
8273 default_related_name: Option<String>,
8274 /// `#[rustango(db_table_comment = "...")]`. Threaded into
8275 /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8276 /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8277 /// inline `COMMENT='...'`, SQLite: no-op).
8278 db_table_comment: Option<String>,
8279 /// `#[rustango(get_latest_by = "created_at")]` /
8280 /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8281 /// `(column, descending)` where `descending = true` when the
8282 /// attribute value starts with `-`. Threaded into
8283 /// `ModelSchema::get_latest_by`.
8284 get_latest_by: Option<(String, bool)>,
8285 /// Extra `(codename, label)` permissions
8286 /// from `#[rustango(extra_permissions = "approve:Can approve,
8287 /// archive:Can archive")]`. Comma-separated `codename:label`
8288 /// pairs. Threaded into `ModelSchema::extra_permissions`.
8289 extra_permissions: Vec<(String, String)>,
8290 /// Which CRUD codenames (`"add"` / `"change"` / `"delete"` /
8291 /// `"view"`) the framework
8292 /// auto-creates. Empty `Vec` (default) means **all four**. Set via
8293 /// `#[rustango(default_permissions = "view,change")]` to opt out.
8294 /// Validated at parse time; unknown actions fail with a span-pointing
8295 /// error.
8296 default_permissions: Vec<String>,
8297 /// `#[rustango(verbose_name = "blog post")]` — the
8298 /// human-readable singular label for the model. Threaded into
8299 /// `ModelSchema::verbose_name` so admin section headers /
8300 /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8301 /// over the Rust struct identifier.
8302 verbose_name: Option<String>,
8303 /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8304 /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8305 /// When unset, `display_label_plural()` auto-suffixes `s`.
8306 verbose_name_plural: Option<String>,
8307 /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8308 /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8309 /// a name (used by `QuerySet::without_global_scope`) with a
8310 /// `fn() -> WhereExpr` path that the macro emits into
8311 /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8312 global_scopes: Vec<GlobalScopeAttr>,
8313 /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8314 /// `#[rustango(through(name, far, far_fk_column, intermediate,
8315 /// intermediate_fk_column, intermediate_pk_column))]` — issue
8316 /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8317 /// entry emits an inherent `<name>_through(&self)` accessor that
8318 /// returns a `QuerySet<Far>` filtered via a correlated subquery
8319 /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8320 /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8321 through_relations: Vec<ThroughAttr>,
8322 /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8323 /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8324 /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8325 /// Each entry emits two associated functions on the parent —
8326 /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8327 /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8328 /// over a correlated subquery against the child table. Users
8329 /// drop the result into `QuerySet::where_raw(...)`.
8330 reverse_has_relations: Vec<ReverseHasAttr>,
8331 /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8332 /// issue #830. Each emits a `Model::generic_reverse_relations()`
8333 /// entry so the relation-existence family resolves polymorphic
8334 /// children by name.
8335 generic_has_relations: Vec<GenericHasAttr>,
8336}
8337
8338/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8339/// declaration. Each entry becomes one `core::GlobalScope` in the
8340/// emitted schema literal; `apply` resolves at macro-expand time
8341/// against the consumer's scope so the function must be in scope at
8342/// the use site. Issue #820.
8343struct GlobalScopeAttr {
8344 name: String,
8345 apply: syn::Path,
8346}
8347
8348/// Parsed `#[rustango(through(...))]` declaration. Issue
8349/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8350/// `hasManyThrough` / `hasOneThrough` parity.
8351///
8352/// `Country hasManyThrough Post via User` declares as:
8353///
8354/// ```ignore
8355/// #[rustango(through(
8356/// name = "posts",
8357/// far = "Post",
8358/// far_fk_column = "author_id",
8359/// intermediate = "User",
8360/// intermediate_fk_column = "country_id",
8361/// ))]
8362/// struct Country { ... }
8363/// ```
8364///
8365/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8366/// which returns a queryset filtered via a correlated subquery —
8367/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8368/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8369/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8370/// because the subquery lives inside a `WhereExpr::InSubquery` node
8371/// and the outer queryset's pending list stays empty.
8372///
8373/// All four required arguments use **SQL column / table names**
8374/// (not Rust field names) to sidestep the multi-hop-filter substrate
8375/// gap. Once that substrate lands, a higher-level Rust-field-name
8376/// shorthand can be added without breaking this surface.
8377struct ThroughAttr {
8378 /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8379 name: String,
8380 /// Far model type identifier. `far = "Post"` → returns
8381 /// `QuerySet<Post>`. Resolved verbatim against the scope where
8382 /// the derive expands.
8383 far: syn::Ident,
8384 /// SQL column on the far model's table that references the
8385 /// intermediate model's primary key. For `Post`'s
8386 /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8387 /// default `<field>_id` convention) or whatever the user
8388 /// declared via `#[rustango(db_column = "...")]`.
8389 far_fk_column: String,
8390 /// Intermediate model type identifier. Needed to look up its
8391 /// `SCHEMA` so the subquery's `FROM` clause points at the
8392 /// intermediate table. `intermediate = "User"`.
8393 intermediate: syn::Ident,
8394 /// SQL column on the intermediate's table that references the
8395 /// source (this) model's primary key. For `User`'s
8396 /// `country: ForeignKey<Country>` the column is `"country_id"`.
8397 intermediate_fk_column: String,
8398 /// SQL primary-key column on the intermediate's table — the
8399 /// column the subquery projects. Optional; defaults to `"id"`
8400 /// (rustango's default PK column name). Override when the
8401 /// intermediate declares a custom PK column.
8402 intermediate_pk_column: String,
8403}
8404
8405/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8406/// child_fk_column = "..."))]` declaration. Issue
8407/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8408/// `whereHas` / `whereDoesntHave` parity.
8409///
8410/// `Post hasMany Comment` declares as:
8411///
8412/// ```ignore
8413/// #[rustango(reverse_has(
8414/// name = "comments",
8415/// child = "Comment",
8416/// child_fk_column = "post_id",
8417/// ))]
8418/// struct Post { ... }
8419/// ```
8420///
8421/// The macro emits two associated functions on `Post`:
8422///
8423/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8424/// `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8425/// <outer>.<self_pk_column>)`.
8426/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8427/// but `NOT EXISTS`, the `whereDoesntHave` analog.
8428///
8429/// User code:
8430///
8431/// ```ignore
8432/// // Posts with at least one comment:
8433/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8434/// // Posts with no comments:
8435/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8436/// ```
8437///
8438/// As with #817, all column / table identifiers are **SQL names**
8439/// (not Rust field names) so the substrate is independent of the
8440/// outstanding multi-hop filter resolver gap. The emitted
8441/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8442/// SQL-emit time via the writer's scope stack.
8443struct ReverseHasAttr {
8444 /// Accessor name. `name = "comments"` → emits
8445 /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8446 name: String,
8447 /// Child model type identifier. `child = "Comment"` — needed to
8448 /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8449 /// points at the child table.
8450 child: syn::Ident,
8451 /// SQL column on the child's table that references this model's
8452 /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8453 /// column is `"post_id"`.
8454 child_fk_column: String,
8455 /// SQL primary-key column on **this** model's table — the column
8456 /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8457 /// (rustango's default PK column name).
8458 self_pk_column: String,
8459}
8460
8461/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8462/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8463/// relation-existence family (issue #830). The child is a polymorphic,
8464/// content-type-discriminated model; emits a `Model::
8465/// generic_reverse_relations()` entry the queryset resolves by name.
8466struct GenericHasAttr {
8467 /// Accessor name, e.g. `name = "tags"`.
8468 name: String,
8469 /// Child model type identifier (`child = "Tag"`) — looked up for its
8470 /// `SCHEMA` so the subquery's `FROM` points at the child table.
8471 child: syn::Ident,
8472 /// Column on the child table holding the parent's content-type id.
8473 /// Optional; defaults to `"content_type_id"`.
8474 ct_column: String,
8475 /// Column on the child table holding the parent's PK value.
8476 /// Optional; defaults to `"object_pk"`.
8477 pk_column: String,
8478 /// SQL primary-key column on **this** (parent) model's table.
8479 /// Optional; defaults to `"id"`.
8480 self_pk_column: String,
8481}
8482
8483/// Parsed form of one index declaration (field-level or container-level).
8484struct IndexAttr {
8485 /// Index name; auto-derived when `None` at parse time.
8486 name: Option<String>,
8487 /// Column names in the index.
8488 columns: Vec<String>,
8489 /// `true` for `CREATE UNIQUE INDEX`.
8490 unique: bool,
8491 /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8492 /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8493 /// `"btree"` when the attribute is absent — the DDL writer omits
8494 /// the `USING` clause and the backend uses its own default
8495 /// (btree on every supported dialect).
8496 method: String,
8497 /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8498 /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8499 /// condition = "...", name = "..."))]`. `None` for plain indexes.
8500 where_clause: Option<String>,
8501 /// Covering-index columns (PG 11+ `INCLUDE (...)` clause).
8502 /// Empty `Vec` (the default) means "no covering columns".
8503 include: Vec<String>,
8504}
8505
8506/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8507struct CheckAttr {
8508 name: String,
8509 expr: String,
8510}
8511
8512/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8513/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8514/// PG-only — surfaced on every backend in the macro emit; the migration
8515/// writer skips the op on MySQL/SQLite. Issue #319.
8516struct ExcludeAttr {
8517 /// Constraint name (free-form Rust identifier).
8518 name: String,
8519 /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8520 using: String,
8521 /// Comma-separated `(column, operator)` pairs, in declaration
8522 /// order. Parsed from `"col WITH op, col WITH op"`.
8523 elements: Vec<(String, String)>,
8524 /// Optional `WHERE` predicate (raw SQL).
8525 where_clause: Option<String>,
8526}
8527
8528/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8529/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8530/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8531/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8532/// the model, not the field.
8533struct CompositeFkAttr {
8534 /// Logical relation name (free-form Rust identifier).
8535 name: String,
8536 /// SQL table name of the target.
8537 to: String,
8538 /// Source-side column names, in declaration order.
8539 from: Vec<String>,
8540 /// Target-side column names, same length / order as `from`.
8541 on: Vec<String>,
8542}
8543
8544/// Parsed form of one `#[rustango(generic_fk(name = "target",
8545/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8546/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8547/// generic ("any model") FKs live on the model, not the field.
8548struct GenericFkAttr {
8549 /// Logical relation name (free-form Rust identifier).
8550 name: String,
8551 /// Source-side column carrying the `content_type_id` value.
8552 ct_column: String,
8553 /// Source-side column carrying the target row's primary key.
8554 pk_column: String,
8555}
8556
8557/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8558struct M2MAttr {
8559 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8560 name: String,
8561 /// Target table (e.g. `"app_tags"`).
8562 to: String,
8563 /// Junction table (e.g. `"post_tags"`).
8564 through: String,
8565 /// Source FK column in the junction table (e.g. `"post_id"`).
8566 src: String,
8567 /// Destination FK column in the junction table (e.g. `"tag_id"`).
8568 dst: String,
8569 /// Whether the migration writer should auto-create the junction
8570 /// table. Default `true`. Set `auto_create = false` (#324) when
8571 /// the operator declares the through table as its own
8572 /// `#[derive(Model)]` struct with extra columns.
8573 auto_create: bool,
8574}
8575
8576/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8577/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8578/// junction carries a ContentType discriminator so unrelated models
8579/// share one pivot + related set.
8580struct GenericM2MAttr {
8581 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8582 name: String,
8583 /// Polymorphic junction table (e.g. `"taggables"`).
8584 through: String,
8585 /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8586 pk_column: String,
8587 /// Junction column holding the owning model's `content_type_id`
8588 /// discriminator (e.g. `"taggable_type"`).
8589 ct_column: String,
8590 /// Junction column holding the related model PK (e.g. `"tag_id"`).
8591 related_column: String,
8592}
8593
8594/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8595/// "user"))]`. `track` is a comma-separated list of field names whose
8596/// before/after values land in the JSONB `changes` column. `source`
8597/// is informational only — it pins a default source when the model
8598/// is written outside any `audit::with_source(...)` scope (rare).
8599#[derive(Default)]
8600struct AuditAttrs {
8601 /// Field names to capture in the `changes` JSONB. Validated
8602 /// against declared scalar fields at compile time. Empty means
8603 /// "track every scalar field".
8604 track: Option<(Vec<String>, proc_macro2::Span)>,
8605}
8606
8607/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8608/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8609/// comma-separated strings; we validate each ident against the model's
8610/// declared fields at compile time.
8611#[derive(Default)]
8612struct AdminAttrs {
8613 list_display: Option<(Vec<String>, proc_macro2::Span)>,
8614 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8615 list_per_page: Option<usize>,
8616 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8617 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8618 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8619 /// Bulk action names. No field-validation against model fields —
8620 /// these are action handlers, not column references.
8621 actions: Option<(Vec<String>, proc_macro2::Span)>,
8622 /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8623 /// sections, comma-separated fields per section, optional
8624 /// `Title:` prefix. Empty title omits the `<legend>`.
8625 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8626 /// `admin(list_display_links = "title")`. Names
8627 /// from `list_display` whose cells should link to detail/edit.
8628 /// Issue #350.
8629 list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8630 /// `admin(search_help_text = "...")`. Short
8631 /// caption rendered beside the admin list view's search box.
8632 /// Issue #353.
8633 search_help_text: Option<String>,
8634 /// `admin(actions_on_top = false)`. Hides the
8635 /// action-bar above the table. Default `true`. Issue #354.
8636 actions_on_top: Option<bool>,
8637 /// `admin(actions_on_bottom = true)`. Renders an
8638 /// additional action-bar below the table. Default `false`.
8639 /// Issue #354.
8640 actions_on_bottom: Option<bool>,
8641 /// `admin(date_hierarchy = "created_at")`. Name of
8642 /// a date / datetime field whose values render as a clickable
8643 /// year / month / day drill-down strip above the list table.
8644 /// Empty / unset disables the strip. Issue #355.
8645 date_hierarchy: Option<String>,
8646 /// `admin(prepopulated_fields = "slug:title")`.
8647 /// Each entry is `target:source[+source2]`; multiple entries are
8648 /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8649 /// The admin change-form emits JS that slugifies the source values
8650 /// into the target field on every keystroke. Issue #356.
8651 prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8652 /// `admin(raw_id_fields = "parent, owner")`. Names
8653 /// of FK fields whose change-form widget renders a lookup link
8654 /// next to the input. Issue #357.
8655 raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8656 /// `admin(autocomplete_fields = "author_id")`.
8657 /// Names of FK fields whose change-form widget renders an
8658 /// Ajax-driven typeahead populated from a `__autocomplete`
8659 /// endpoint on the target model. Issue #358.
8660 autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8661 /// `admin(list_select_related = "all" | "none" | "author, …")`
8662 /// — tunes the admin list view's FK auto-JOIN
8663 /// policy. Default `"all"` matches rustango's join-everything
8664 /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8665 list_select_related: Option<String>,
8666 /// `admin(formfield_overrides = "field:widget, field2:widget2")`.
8667 /// Each entry is `field_name:widget_name`; multiple
8668 /// entries comma-separated. Empty / unset → no overrides. The
8669 /// list of widget names supported is documented on
8670 /// `AdminConfig::formfield_overrides`. Issue #359.
8671 formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8672}
8673
8674fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8675 let mut out = ContainerAttrs {
8676 table: None,
8677 display: None,
8678 app: None,
8679 admin: None,
8680 audit: None,
8681 // Default `permissions = true` so every `#[derive(Model)]`
8682 // gets the four CRUD codenames seeded by `auto_create_permissions`
8683 // and is visible to non-superusers in the tenant admin without
8684 // manual per-model annotation. Models that intentionally don't
8685 // want permission rows (registry-internal types, framework
8686 // tables operators shouldn't manage directly) opt out via
8687 // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8688 // out-of-the-box admin invisibility regression (#62).
8689 permissions: true,
8690 m2m: Vec::new(),
8691 generic_m2m: Vec::new(),
8692 indexes: Vec::new(),
8693 checks: Vec::new(),
8694 excludes: Vec::new(),
8695 composite_fks: Vec::new(),
8696 generic_fks: Vec::new(),
8697 scope: None,
8698 manager_ext: None,
8699 manager_fns: Vec::new(),
8700 default_order: Vec::new(),
8701 is_view: false,
8702 managed: true,
8703 verbose_name: None,
8704 verbose_name_plural: None,
8705 base_manager_name: None,
8706 order_with_respect_to: None,
8707 proxy: false,
8708 required_db_features: Vec::new(),
8709 required_db_vendor: None,
8710 default_related_name: None,
8711 db_table_comment: None,
8712 get_latest_by: None,
8713 extra_permissions: Vec::new(),
8714 default_permissions: Vec::new(),
8715 global_scopes: Vec::new(),
8716 through_relations: Vec::new(),
8717 reverse_has_relations: Vec::new(),
8718 generic_has_relations: Vec::new(),
8719 };
8720 for attr in &input.attrs {
8721 if !attr.path().is_ident("rustango") {
8722 continue;
8723 }
8724 attr.parse_nested_meta(|meta| {
8725 if meta.path.is_ident("table") {
8726 let s: LitStr = meta.value()?.parse()?;
8727 let name = s.value();
8728 // v0.27.3 (#65) — macro-time guard against table names
8729 // that compile but break SQL downstream. Hyphens are
8730 // the common footgun: PostgreSQL accepts a quoted
8731 // `"intermediate-region"` in CREATE TABLE, but the
8732 // FK / index name derivation in `migrate::ddl`
8733 // emits `intermediate-region_field_fkey` unquoted,
8734 // which then fails the SQL parser. Same shape rule
8735 // as Postgres regular identifiers so the safe path
8736 // is the only path.
8737 validate_table_name(&name, s.span())?;
8738 out.table = Some(name);
8739 return Ok(());
8740 }
8741 if meta.path.is_ident("display") {
8742 let s: LitStr = meta.value()?.parse()?;
8743 out.display = Some((s.value(), s.span()));
8744 return Ok(());
8745 }
8746 if meta.path.is_ident("app") {
8747 let s: LitStr = meta.value()?.parse()?;
8748 out.app = Some(s.value());
8749 return Ok(());
8750 }
8751 if meta.path.is_ident("scope") {
8752 let s: LitStr = meta.value()?.parse()?;
8753 let val = s.value();
8754 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8755 return Err(meta.error(format!(
8756 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8757 )));
8758 }
8759 out.scope = Some(val);
8760 return Ok(());
8761 }
8762 if meta.path.is_ident("admin") {
8763 let mut admin = AdminAttrs::default();
8764 meta.parse_nested_meta(|inner| {
8765 if inner.path.is_ident("list_display") {
8766 let s: LitStr = inner.value()?.parse()?;
8767 admin.list_display =
8768 Some((split_field_list(&s.value()), s.span()));
8769 return Ok(());
8770 }
8771 if inner.path.is_ident("search_fields") {
8772 let s: LitStr = inner.value()?.parse()?;
8773 admin.search_fields =
8774 Some((split_field_list(&s.value()), s.span()));
8775 return Ok(());
8776 }
8777 if inner.path.is_ident("readonly_fields") {
8778 let s: LitStr = inner.value()?.parse()?;
8779 admin.readonly_fields =
8780 Some((split_field_list(&s.value()), s.span()));
8781 return Ok(());
8782 }
8783 if inner.path.is_ident("list_per_page") {
8784 let lit: syn::LitInt = inner.value()?.parse()?;
8785 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8786 return Ok(());
8787 }
8788 if inner.path.is_ident("ordering") {
8789 let s: LitStr = inner.value()?.parse()?;
8790 admin.ordering = Some((
8791 parse_ordering_list(&s.value()),
8792 s.span(),
8793 ));
8794 return Ok(());
8795 }
8796 if inner.path.is_ident("list_filter") {
8797 let s: LitStr = inner.value()?.parse()?;
8798 admin.list_filter =
8799 Some((split_field_list(&s.value()), s.span()));
8800 return Ok(());
8801 }
8802 if inner.path.is_ident("actions") {
8803 let s: LitStr = inner.value()?.parse()?;
8804 admin.actions =
8805 Some((split_field_list(&s.value()), s.span()));
8806 return Ok(());
8807 }
8808 if inner.path.is_ident("fieldsets") {
8809 let s: LitStr = inner.value()?.parse()?;
8810 admin.fieldsets =
8811 Some((parse_fieldset_list(&s.value()), s.span()));
8812 return Ok(());
8813 }
8814 if inner.path.is_ident("list_display_links") {
8815 let s: LitStr = inner.value()?.parse()?;
8816 admin.list_display_links =
8817 Some((split_field_list(&s.value()), s.span()));
8818 return Ok(());
8819 }
8820 if inner.path.is_ident("search_help_text") {
8821 let s: LitStr = inner.value()?.parse()?;
8822 admin.search_help_text = Some(s.value());
8823 return Ok(());
8824 }
8825 if inner.path.is_ident("actions_on_top") {
8826 let lit: syn::LitBool = inner.value()?.parse()?;
8827 admin.actions_on_top = Some(lit.value);
8828 return Ok(());
8829 }
8830 if inner.path.is_ident("actions_on_bottom") {
8831 let lit: syn::LitBool = inner.value()?.parse()?;
8832 admin.actions_on_bottom = Some(lit.value);
8833 return Ok(());
8834 }
8835 if inner.path.is_ident("date_hierarchy") {
8836 let s: LitStr = inner.value()?.parse()?;
8837 admin.date_hierarchy = Some(s.value());
8838 return Ok(());
8839 }
8840 if inner.path.is_ident("prepopulated_fields") {
8841 let s: LitStr = inner.value()?.parse()?;
8842 admin.prepopulated_fields =
8843 Some((parse_prepopulated_list(&s.value()), s.span()));
8844 return Ok(());
8845 }
8846 if inner.path.is_ident("raw_id_fields") {
8847 let s: LitStr = inner.value()?.parse()?;
8848 admin.raw_id_fields =
8849 Some((split_field_list(&s.value()), s.span()));
8850 return Ok(());
8851 }
8852 if inner.path.is_ident("autocomplete_fields") {
8853 let s: LitStr = inner.value()?.parse()?;
8854 admin.autocomplete_fields =
8855 Some((split_field_list(&s.value()), s.span()));
8856 return Ok(());
8857 }
8858 if inner.path.is_ident("list_select_related") {
8859 let s: LitStr = inner.value()?.parse()?;
8860 admin.list_select_related = Some(s.value());
8861 return Ok(());
8862 }
8863 if inner.path.is_ident("formfield_overrides") {
8864 let s: LitStr = inner.value()?.parse()?;
8865 admin.formfield_overrides =
8866 Some((parse_formfield_overrides(&s.value()), s.span()));
8867 return Ok(());
8868 }
8869 Err(inner.error(
8870 "unknown admin attribute (supported: \
8871 `list_display`, `list_display_links`, \
8872 `search_fields`, `search_help_text`, \
8873 `readonly_fields`, \
8874 `list_filter`, `list_per_page`, `ordering`, `actions`, \
8875 `actions_on_top`, `actions_on_bottom`, \
8876 `date_hierarchy`, \
8877 `prepopulated_fields`, \
8878 `raw_id_fields`, \
8879 `autocomplete_fields`, \
8880 `list_select_related`, \
8881 `formfield_overrides`, \
8882 `fieldsets`)",
8883 ))
8884 })?;
8885 out.admin = Some(admin);
8886 return Ok(());
8887 }
8888 if meta.path.is_ident("manager") {
8889 // `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8890 // Stretch `from_queryset = "..."` is left as a
8891 // follow-up — the issue's primary acceptance is the
8892 // `ext = ...` trait emission.
8893 meta.parse_nested_meta(|inner| {
8894 if inner.path.is_ident("ext") {
8895 let s: LitStr = inner.value()?.parse()?;
8896 let name = s.value();
8897 if name.is_empty() {
8898 return Err(inner.error("manager(ext = \"...\") cannot be empty"));
8899 }
8900 out.manager_ext =
8901 Some(syn::Ident::new(&name, s.span()));
8902 return Ok(());
8903 }
8904 Err(inner.error(
8905 "unknown manager attribute (supported: `ext = \"TraitName\"`)",
8906 ))
8907 })?;
8908 return Ok(());
8909 }
8910 if meta.path.is_ident("manager_fn") {
8911 // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8912 // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8913 // next to the default `Self::objects()`. Multiple
8914 // attributes accumulate.
8915 let s: LitStr = meta.value()?.parse()?;
8916 let name = s.value();
8917 if name.is_empty() {
8918 return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8919 }
8920 if name == "objects" {
8921 return Err(meta.error(
8922 "`manager_fn = \"objects\"` collides with the default \
8923 accessor — pick a different name",
8924 ));
8925 }
8926 let ident = syn::Ident::new(&name, s.span());
8927 if out.manager_fns.iter().any(|prev| *prev == ident) {
8928 return Err(meta.error(format!(
8929 "duplicate `manager_fn = \"{name}\"`"
8930 )));
8931 }
8932 out.manager_fns.push(ident);
8933 return Ok(());
8934 }
8935 if meta.path.is_ident("default_order") {
8936 // `#[rustango(default_order = "-created_at, status")]`
8937 // — issue #291 / T2.5. Comma-separated list; `-prefix`
8938 // means descending, `+prefix` or bare name means ascending.
8939 // Per-query opt-in via `QuerySet::with_default_order()`.
8940 let s: LitStr = meta.value()?.parse()?;
8941 let raw = s.value();
8942 let span = s.span();
8943 let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
8944 Vec::new();
8945 for entry in raw.split(',') {
8946 let trimmed = entry.trim();
8947 if trimmed.is_empty() {
8948 return Err(syn::Error::new(
8949 span,
8950 "`default_order = \"...\"` has an empty entry — \
8951 check for a stray comma",
8952 ));
8953 }
8954 let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
8955 (true, rest.trim().to_owned())
8956 } else if let Some(rest) = trimmed.strip_prefix('+') {
8957 (false, rest.trim().to_owned())
8958 } else {
8959 (false, trimmed.to_owned())
8960 };
8961 if name.is_empty() {
8962 return Err(syn::Error::new(
8963 span,
8964 "`default_order` entry has no column name after the prefix",
8965 ));
8966 }
8967 if parsed.iter().any(|(n, _, _)| *n == name) {
8968 return Err(syn::Error::new(
8969 span,
8970 format!("duplicate column `{name}` in `default_order`"),
8971 ));
8972 }
8973 parsed.push((name, desc, span));
8974 }
8975 if parsed.is_empty() {
8976 return Err(syn::Error::new(
8977 span,
8978 "`default_order = \"...\"` cannot be empty",
8979 ));
8980 }
8981 out.default_order = parsed;
8982 return Ok(());
8983 }
8984 if meta.path.is_ident("global_scope") {
8985 // `#[rustango(global_scope(name = "active", apply =
8986 // path::to::fn))]` — issue #820. The apply function
8987 // path resolves at macro-expand time in the consumer's
8988 // scope; the macro re-emits it verbatim into the
8989 // `ModelSchema::global_scopes` slice literal.
8990 let span = meta.path.span();
8991 let mut scope_name: Option<String> = None;
8992 let mut apply_path: Option<syn::Path> = None;
8993 meta.parse_nested_meta(|inner| {
8994 if inner.path.is_ident("name") {
8995 let s: LitStr = inner.value()?.parse()?;
8996 let raw = s.value();
8997 if raw.trim().is_empty() {
8998 return Err(syn::Error::new(
8999 s.span(),
9000 "`global_scope(name = \"...\")` must not be empty",
9001 ));
9002 }
9003 scope_name = Some(raw);
9004 return Ok(());
9005 }
9006 if inner.path.is_ident("apply") {
9007 let p: syn::Path = inner.value()?.parse()?;
9008 apply_path = Some(p);
9009 return Ok(());
9010 }
9011 Err(inner.error(
9012 "unknown `global_scope` attribute (supported: \
9013 `name`, `apply`)",
9014 ))
9015 })?;
9016 let Some(name) = scope_name else {
9017 return Err(syn::Error::new(
9018 span,
9019 "`global_scope` requires `name = \"...\"`",
9020 ));
9021 };
9022 let Some(apply) = apply_path else {
9023 return Err(syn::Error::new(
9024 span,
9025 "`global_scope` requires `apply = fn_path`",
9026 ));
9027 };
9028 if out.global_scopes.iter().any(|s| s.name == name) {
9029 return Err(syn::Error::new(
9030 span,
9031 format!(
9032 "duplicate global scope name `{name}` — \
9033 pick a unique identifier so \
9034 `QuerySet::without_global_scope(\"{name}\")` \
9035 is unambiguous"
9036 ),
9037 ));
9038 }
9039 out.global_scopes.push(GlobalScopeAttr { name, apply });
9040 return Ok(());
9041 }
9042 if meta.path.is_ident("through") {
9043 // `#[rustango(through(name = "posts", far = "Post",
9044 // far_fk_column = "author_id", intermediate = "User",
9045 // intermediate_fk_column = "country_id"
9046 // [, intermediate_pk_column = "id"]))]` — issue #817.
9047 let span = meta.path.span();
9048 let mut accessor_name: Option<String> = None;
9049 let mut far_ident: Option<syn::Ident> = None;
9050 let mut far_fk_column: Option<String> = None;
9051 let mut intermediate_ident: Option<syn::Ident> = None;
9052 let mut intermediate_fk_column: Option<String> = None;
9053 let mut intermediate_pk_column: Option<String> = None;
9054 fn parse_nonempty_string(
9055 inner: &syn::meta::ParseNestedMeta<'_>,
9056 field: &str,
9057 ) -> syn::Result<String> {
9058 let s: LitStr = inner.value()?.parse()?;
9059 let raw = s.value();
9060 let trimmed = raw.trim();
9061 if trimmed.is_empty() {
9062 return Err(syn::Error::new(
9063 s.span(),
9064 format!("`through({field} = \"...\")` must not be empty"),
9065 ));
9066 }
9067 Ok(trimmed.to_owned())
9068 }
9069 meta.parse_nested_meta(|inner| {
9070 if inner.path.is_ident("name") {
9071 accessor_name = Some(parse_nonempty_string(&inner, "name")?);
9072 return Ok(());
9073 }
9074 if inner.path.is_ident("far") {
9075 // Far model name accepted as a string literal
9076 // so the attribute fits inside the existing
9077 // `parse_nested_meta` shape; parsed back into a
9078 // `syn::Ident` so the emitted accessor can
9079 // reference the type directly.
9080 let s: LitStr = inner.value()?.parse()?;
9081 let raw = s.value();
9082 let trimmed = raw.trim();
9083 if trimmed.is_empty() {
9084 return Err(syn::Error::new(
9085 s.span(),
9086 "`through(far = \"...\")` must not be empty",
9087 ));
9088 }
9089 far_ident = Some(syn::Ident::new(trimmed, s.span()));
9090 return Ok(());
9091 }
9092 if inner.path.is_ident("far_fk_column") {
9093 far_fk_column =
9094 Some(parse_nonempty_string(&inner, "far_fk_column")?);
9095 return Ok(());
9096 }
9097 if inner.path.is_ident("intermediate") {
9098 let s: LitStr = inner.value()?.parse()?;
9099 let raw = s.value();
9100 let trimmed = raw.trim();
9101 if trimmed.is_empty() {
9102 return Err(syn::Error::new(
9103 s.span(),
9104 "`through(intermediate = \"...\")` must not be empty",
9105 ));
9106 }
9107 intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9108 return Ok(());
9109 }
9110 if inner.path.is_ident("intermediate_fk_column") {
9111 intermediate_fk_column =
9112 Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9113 return Ok(());
9114 }
9115 if inner.path.is_ident("intermediate_pk_column") {
9116 intermediate_pk_column =
9117 Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9118 return Ok(());
9119 }
9120 Err(inner.error(
9121 "unknown `through` attribute (supported: \
9122 `name`, `far`, `far_fk_column`, \
9123 `intermediate`, `intermediate_fk_column`, \
9124 `intermediate_pk_column`)",
9125 ))
9126 })?;
9127 let Some(name) = accessor_name else {
9128 return Err(syn::Error::new(
9129 span,
9130 "`through` requires `name = \"...\"`",
9131 ));
9132 };
9133 let Some(far) = far_ident else {
9134 return Err(syn::Error::new(
9135 span,
9136 "`through` requires `far = \"FarModelType\"`",
9137 ));
9138 };
9139 let Some(far_fk_column) = far_fk_column else {
9140 return Err(syn::Error::new(
9141 span,
9142 "`through` requires `far_fk_column = \"<column>\"`",
9143 ));
9144 };
9145 let Some(intermediate) = intermediate_ident else {
9146 return Err(syn::Error::new(
9147 span,
9148 "`through` requires `intermediate = \"IntermediateModelType\"`",
9149 ));
9150 };
9151 let Some(intermediate_fk_column) = intermediate_fk_column else {
9152 return Err(syn::Error::new(
9153 span,
9154 "`through` requires `intermediate_fk_column = \"<column>\"`",
9155 ));
9156 };
9157 let intermediate_pk_column =
9158 intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9159 if out.through_relations.iter().any(|t| t.name == name) {
9160 return Err(syn::Error::new(
9161 span,
9162 format!(
9163 "duplicate `through(name = \"{name}\")` — \
9164 pick a unique accessor name"
9165 ),
9166 ));
9167 }
9168 out.through_relations.push(ThroughAttr {
9169 name,
9170 far,
9171 far_fk_column,
9172 intermediate,
9173 intermediate_fk_column,
9174 intermediate_pk_column,
9175 });
9176 return Ok(());
9177 }
9178 if meta.path.is_ident("reverse_has") {
9179 // `#[rustango(reverse_has(name = "comments",
9180 // child = "Comment", child_fk_column = "post_id"
9181 // [, self_pk_column = "id"]))]` — issue #830.
9182 let span = meta.path.span();
9183 let mut accessor_name: Option<String> = None;
9184 let mut child_ident: Option<syn::Ident> = None;
9185 let mut child_fk_column: Option<String> = None;
9186 let mut self_pk_column: Option<String> = None;
9187 meta.parse_nested_meta(|inner| {
9188 if inner.path.is_ident("name") {
9189 let s: LitStr = inner.value()?.parse()?;
9190 let raw = s.value();
9191 if raw.trim().is_empty() {
9192 return Err(syn::Error::new(
9193 s.span(),
9194 "`reverse_has(name = \"...\")` must not be empty",
9195 ));
9196 }
9197 accessor_name = Some(raw);
9198 return Ok(());
9199 }
9200 if inner.path.is_ident("child") {
9201 let s: LitStr = inner.value()?.parse()?;
9202 let raw = s.value();
9203 let trimmed = raw.trim();
9204 if trimmed.is_empty() {
9205 return Err(syn::Error::new(
9206 s.span(),
9207 "`reverse_has(child = \"...\")` must not be empty",
9208 ));
9209 }
9210 child_ident = Some(syn::Ident::new(trimmed, s.span()));
9211 return Ok(());
9212 }
9213 if inner.path.is_ident("child_fk_column") {
9214 let s: LitStr = inner.value()?.parse()?;
9215 let raw = s.value();
9216 let trimmed = raw.trim();
9217 if trimmed.is_empty() {
9218 return Err(syn::Error::new(
9219 s.span(),
9220 "`reverse_has(child_fk_column = \"...\")` must not be empty",
9221 ));
9222 }
9223 child_fk_column = Some(trimmed.to_owned());
9224 return Ok(());
9225 }
9226 if inner.path.is_ident("self_pk_column") {
9227 let s: LitStr = inner.value()?.parse()?;
9228 let raw = s.value();
9229 let trimmed = raw.trim();
9230 if trimmed.is_empty() {
9231 return Err(syn::Error::new(
9232 s.span(),
9233 "`reverse_has(self_pk_column = \"...\")` must not be empty",
9234 ));
9235 }
9236 self_pk_column = Some(trimmed.to_owned());
9237 return Ok(());
9238 }
9239 Err(inner.error(
9240 "unknown `reverse_has` attribute (supported: \
9241 `name`, `child`, `child_fk_column`, \
9242 `self_pk_column`)",
9243 ))
9244 })?;
9245 let Some(name) = accessor_name else {
9246 return Err(syn::Error::new(
9247 span,
9248 "`reverse_has` requires `name = \"...\"`",
9249 ));
9250 };
9251 let Some(child) = child_ident else {
9252 return Err(syn::Error::new(
9253 span,
9254 "`reverse_has` requires `child = \"ChildModelType\"`",
9255 ));
9256 };
9257 let Some(child_fk_column) = child_fk_column else {
9258 return Err(syn::Error::new(
9259 span,
9260 "`reverse_has` requires `child_fk_column = \"<column>\"`",
9261 ));
9262 };
9263 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9264 if out.reverse_has_relations.iter().any(|r| r.name == name) {
9265 return Err(syn::Error::new(
9266 span,
9267 format!(
9268 "duplicate `reverse_has(name = \"{name}\")` — \
9269 pick a unique accessor name"
9270 ),
9271 ));
9272 }
9273 out.reverse_has_relations.push(ReverseHasAttr {
9274 name,
9275 child,
9276 child_fk_column,
9277 self_pk_column,
9278 });
9279 return Ok(());
9280 }
9281 if meta.path.is_ident("generic_has") {
9282 // `#[rustango(generic_has(name = "tags",
9283 // child = "Tag", ct_column = "content_type_id",
9284 // pk_column = "object_pk" [, self_pk_column = "id"]))]`
9285 // — issue #830, the reverse generic-FK (GFK) arm of the
9286 // relation-existence family.
9287 let span = meta.path.span();
9288 let mut accessor_name: Option<String> = None;
9289 let mut child_ident: Option<syn::Ident> = None;
9290 let mut ct_column: Option<String> = None;
9291 let mut pk_column: Option<String> = None;
9292 let mut self_pk_column: Option<String> = None;
9293 meta.parse_nested_meta(|inner| {
9294 if inner.path.is_ident("name") {
9295 let s: LitStr = inner.value()?.parse()?;
9296 let raw = s.value();
9297 if raw.trim().is_empty() {
9298 return Err(syn::Error::new(
9299 s.span(),
9300 "`generic_has(name = \"...\")` must not be empty",
9301 ));
9302 }
9303 accessor_name = Some(raw);
9304 return Ok(());
9305 }
9306 if inner.path.is_ident("child") {
9307 let s: LitStr = inner.value()?.parse()?;
9308 let trimmed = s.value().trim().to_owned();
9309 if trimmed.is_empty() {
9310 return Err(syn::Error::new(
9311 s.span(),
9312 "`generic_has(child = \"...\")` must not be empty",
9313 ));
9314 }
9315 child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9316 return Ok(());
9317 }
9318 if inner.path.is_ident("ct_column") {
9319 let s: LitStr = inner.value()?.parse()?;
9320 let trimmed = s.value().trim().to_owned();
9321 if trimmed.is_empty() {
9322 return Err(syn::Error::new(
9323 s.span(),
9324 "`generic_has(ct_column = \"...\")` must not be empty",
9325 ));
9326 }
9327 ct_column = Some(trimmed);
9328 return Ok(());
9329 }
9330 if inner.path.is_ident("pk_column") {
9331 let s: LitStr = inner.value()?.parse()?;
9332 let trimmed = s.value().trim().to_owned();
9333 if trimmed.is_empty() {
9334 return Err(syn::Error::new(
9335 s.span(),
9336 "`generic_has(pk_column = \"...\")` must not be empty",
9337 ));
9338 }
9339 pk_column = Some(trimmed);
9340 return Ok(());
9341 }
9342 if inner.path.is_ident("self_pk_column") {
9343 let s: LitStr = inner.value()?.parse()?;
9344 let trimmed = s.value().trim().to_owned();
9345 if trimmed.is_empty() {
9346 return Err(syn::Error::new(
9347 s.span(),
9348 "`generic_has(self_pk_column = \"...\")` must not be empty",
9349 ));
9350 }
9351 self_pk_column = Some(trimmed);
9352 return Ok(());
9353 }
9354 Err(inner.error(
9355 "unknown `generic_has` attribute (supported: \
9356 `name`, `child`, `ct_column`, `pk_column`, \
9357 `self_pk_column`)",
9358 ))
9359 })?;
9360 let Some(name) = accessor_name else {
9361 return Err(syn::Error::new(
9362 span,
9363 "`generic_has` requires `name = \"...\"`",
9364 ));
9365 };
9366 let Some(child) = child_ident else {
9367 return Err(syn::Error::new(
9368 span,
9369 "`generic_has` requires `child = \"ChildModelType\"`",
9370 ));
9371 };
9372 let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9373 let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9374 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9375 if out.generic_has_relations.iter().any(|r| r.name == name) {
9376 return Err(syn::Error::new(
9377 span,
9378 format!(
9379 "duplicate `generic_has(name = \"{name}\")` — \
9380 pick a unique accessor name"
9381 ),
9382 ));
9383 }
9384 out.generic_has_relations.push(GenericHasAttr {
9385 name,
9386 child,
9387 ct_column,
9388 pk_column,
9389 self_pk_column,
9390 });
9391 return Ok(());
9392 }
9393 if meta.path.is_ident("audit") {
9394 let mut audit = AuditAttrs::default();
9395 meta.parse_nested_meta(|inner| {
9396 if inner.path.is_ident("track") {
9397 let s: LitStr = inner.value()?.parse()?;
9398 audit.track =
9399 Some((split_field_list(&s.value()), s.span()));
9400 return Ok(());
9401 }
9402 Err(inner.error(
9403 "unknown audit attribute (supported: `track`)",
9404 ))
9405 })?;
9406 out.audit = Some(audit);
9407 return Ok(());
9408 }
9409 if meta.path.is_ident("permissions") {
9410 // Two forms accepted:
9411 // #[rustango(permissions)] — flag form, true
9412 // #[rustango(permissions = false)] — explicit opt-out
9413 // #[rustango(permissions = true)] — explicit opt-in
9414 if let Ok(v) = meta.value() {
9415 let lit: syn::LitBool = v.parse()?;
9416 out.permissions = lit.value;
9417 } else {
9418 out.permissions = true;
9419 }
9420 return Ok(());
9421 }
9422 if meta.path.is_ident("view") {
9423 // Issue #293 / T2.10. Two forms accepted, matching
9424 // the `permissions` flag pattern:
9425 // #[rustango(view)] — flag form, true
9426 // #[rustango(view = false)] — explicit opt-out
9427 // #[rustango(view = true)] — explicit opt-in
9428 if let Ok(v) = meta.value() {
9429 let lit: syn::LitBool = v.parse()?;
9430 out.is_view = lit.value;
9431 } else {
9432 out.is_view = true;
9433 }
9434 return Ok(());
9435 }
9436 if meta.path.is_ident("managed") {
9437 // Who owns the table. Issue #321.
9438 // #[rustango(managed = false)] — operator-managed table
9439 // #[rustango(managed = true)] — rustango-managed (the default)
9440 // Bare-flag form is intentionally not accepted: writing
9441 // `#[rustango(managed)]` reads as "yes please manage it"
9442 // which is already the default. The opt-out is the only
9443 // useful state, so it must be explicit.
9444 let v = meta.value()?;
9445 let lit: syn::LitBool = v.parse()?;
9446 out.managed = lit.value;
9447 return Ok(());
9448 }
9449 if meta.path.is_ident("verbose_name") {
9450 let s: LitStr = meta.value()?.parse()?;
9451 out.verbose_name = Some(s.value());
9452 return Ok(());
9453 }
9454 if meta.path.is_ident("verbose_name_plural") {
9455 let s: LitStr = meta.value()?.parse()?;
9456 out.verbose_name_plural = Some(s.value());
9457 return Ok(());
9458 }
9459 if meta.path.is_ident("db_table_comment") {
9460 // Free-form table-level comment attached to the DB
9461 // catalog.
9462 let s: LitStr = meta.value()?.parse()?;
9463 out.db_table_comment = Some(s.value());
9464 return Ok(());
9465 }
9466 if meta.path.is_ident("proxy") {
9467 // Declarative flag
9468 // marking the struct as a proxy of another model that
9469 // shares its DB table. Stored on `ModelSchema::proxy`
9470 // so future codegen can skip `CreateTable` emission
9471 // for proxies (parent owns the table) and route
9472 // per-instance method resolution to the proxy class.
9473 //
9474 // Accepts `proxy` (bare → true) and `proxy = true/false`.
9475 let value = if meta.input.peek(syn::Token![=]) {
9476 meta.value()?.parse::<syn::LitBool>()?.value
9477 } else {
9478 true
9479 };
9480 out.proxy = value;
9481 return Ok(());
9482 }
9483 if meta.path.is_ident("order_with_respect_to") {
9484 // `order_with_respect_to = "parent_fk"` — the model's
9485 // instances are intrinsically ordered relative to
9486 // their parent FK.
9487 //
9488 // rustango stores the FK field name on
9489 // `ModelSchema::order_with_respect_to`. Declarative-only
9490 // today: the migration writer + admin surfaces still
9491 // treat every model identically. Future codegen will
9492 // key off the metadata to auto-emit the `_order`
9493 // column and reorder helpers.
9494 //
9495 // Validated as a Rust-shape identifier so the macro
9496 // can reject typos at derive time.
9497 let s: LitStr = meta.value()?.parse()?;
9498 let raw = s.value();
9499 if raw.is_empty() {
9500 return Err(syn::Error::new(
9501 s.span(),
9502 "`order_with_respect_to` must be a non-empty FK field name",
9503 ));
9504 }
9505 let valid = raw
9506 .chars()
9507 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9508 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9509 if !valid {
9510 return Err(syn::Error::new(
9511 s.span(),
9512 format!(
9513 "`order_with_respect_to` must be a valid Rust \
9514 identifier (letters / digits / underscores, \
9515 not starting with a digit); got `{raw}`"
9516 ),
9517 ));
9518 }
9519 out.order_with_respect_to = Some(raw);
9520 return Ok(());
9521 }
9522 if meta.path.is_ident("required_db_features") {
9523 // Capability tokens the model needs (e.g.
9524 // `"json_extract"`, `"window_functions"`,
9525 // `"row_security"`). Comma-separated.
9526 // `manage check --deploy` walks every model and warns
9527 // when the active backend doesn't advertise the
9528 // capability.
9529 //
9530 // rustango ships a small registry of capability tokens
9531 // each dialect supports — see `Dialect::supports`.
9532 // Unknown tokens still parse (they end up on the
9533 // schema and show up in the warning) so projects can
9534 // declare aspirational capabilities and the check
9535 // verb will keep nagging until the dialect implements
9536 // them.
9537 let s: LitStr = meta.value()?.parse()?;
9538 let raw = s.value();
9539 let features: Vec<String> = raw
9540 .split(',')
9541 .map(str::trim)
9542 .filter(|s| !s.is_empty())
9543 .map(str::to_owned)
9544 .collect();
9545 if features.is_empty() {
9546 return Err(syn::Error::new(
9547 s.span(),
9548 "`required_db_features` must list at least one \
9549 comma-separated capability token",
9550 ));
9551 }
9552 out.required_db_features = features;
9553 return Ok(());
9554 }
9555 if meta.path.is_ident("required_db_vendor") {
9556 // The model is only meant to run against the named DB
9557 // backend. `manage check --deploy` flags a mismatch so
9558 // ops catches "I forgot to switch DATABASE_URL" at
9559 // deploy time rather than runtime.
9560 //
9561 // Restricted to the three backends rustango ships
9562 // dialects for so the check verb can compare reliably.
9563 let s: LitStr = meta.value()?.parse()?;
9564 let raw = s.value().to_ascii_lowercase();
9565 match raw.as_str() {
9566 "postgresql" | "postgres" | "pg" => {
9567 out.required_db_vendor = Some("postgres".to_owned());
9568 }
9569 "mysql" | "mariadb" => {
9570 out.required_db_vendor = Some("mysql".to_owned());
9571 }
9572 "sqlite" | "sqlite3" => {
9573 out.required_db_vendor = Some("sqlite".to_owned());
9574 }
9575 _ => {
9576 return Err(syn::Error::new(
9577 s.span(),
9578 format!(
9579 "unknown required_db_vendor `{raw}` — \
9580 expected `postgres` (aliases: `postgresql`, `pg`), \
9581 `mysql` (alias: `mariadb`), or `sqlite` \
9582 (alias: `sqlite3`)"
9583 ),
9584 ));
9585 }
9586 }
9587 return Ok(());
9588 }
9589 if meta.path.is_ident("base_manager_name") {
9590 // Name of the manager that `<instance>.<relation>_set`
9591 // uses when resolving reverse-relation managers.
9592 // Distinct from `default_manager_name` (what
9593 // `Self::objects()` returns). Stored on
9594 // `ModelSchema::base_manager_name`.
9595 //
9596 // Validated as a Rust identifier so it stays safe to
9597 // re-emit as code in future reverse-manager codegen.
9598 let s: LitStr = meta.value()?.parse()?;
9599 let raw = s.value();
9600 if raw.is_empty() {
9601 return Err(syn::Error::new(
9602 s.span(),
9603 "`base_manager_name` must be a non-empty string",
9604 ));
9605 }
9606 let valid = raw
9607 .chars()
9608 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9609 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9610 if !valid {
9611 return Err(syn::Error::new(
9612 s.span(),
9613 format!(
9614 "`base_manager_name` must be a valid Rust \
9615 identifier (letters / digits / underscores, \
9616 not starting with a digit); got `{raw}`"
9617 ),
9618 ));
9619 }
9620 out.base_manager_name = Some(raw);
9621 return Ok(());
9622 }
9623 if meta.path.is_ident("default_related_name") {
9624 // The name reverse-relation accessors use when callers
9625 // don't override `related_name = "..."` on the FK / M2M
9626 // field. Stored on `ModelSchema::default_related_name`
9627 // so future reverse-manager codegen / schema emit /
9628 // admin templates can pick the right accessor name
9629 // (today rustango doesn't auto-emit reverse managers;
9630 // the metadata is the foundation for that work).
9631 //
9632 // Enforced non-empty + ASCII identifier-shape so the
9633 // string is safe to use as a Rust ident later.
9634 let s: LitStr = meta.value()?.parse()?;
9635 let raw = s.value();
9636 if raw.is_empty() {
9637 return Err(syn::Error::new(
9638 s.span(),
9639 "`default_related_name` must be a non-empty string",
9640 ));
9641 }
9642 let valid = raw
9643 .chars()
9644 .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9645 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9646 if !valid {
9647 return Err(syn::Error::new(
9648 s.span(),
9649 format!(
9650 "`default_related_name` must be snake_case ASCII \
9651 (lowercase letters / digits / underscores, not \
9652 starting with a digit); got `{raw}`"
9653 ),
9654 ));
9655 }
9656 out.default_related_name = Some(raw);
9657 return Ok(());
9658 }
9659 if meta.path.is_ident("extra_permissions") {
9660 // Comma-separated `codename:label` pairs.
9661 let s: LitStr = meta.value()?.parse()?;
9662 let raw = s.value();
9663 let mut pairs = Vec::new();
9664 for entry in raw.split(',') {
9665 let entry = entry.trim();
9666 if entry.is_empty() {
9667 continue;
9668 }
9669 let (codename, label) = match entry.split_once(':') {
9670 Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9671 None => (entry.to_owned(), entry.to_owned()),
9672 };
9673 if codename.is_empty() {
9674 return Err(meta.error(
9675 "`extra_permissions` entries must be `codename:label` pairs",
9676 ));
9677 }
9678 pairs.push((codename, label));
9679 }
9680 if pairs.is_empty() {
9681 return Err(meta
9682 .error("`extra_permissions = \"…\"` must list at least one pair"));
9683 }
9684 out.extra_permissions = pairs;
9685 return Ok(());
9686 }
9687 if meta.path.is_ident("default_permissions") {
9688 // Comma-separated subset of the CRUD action set.
9689 // Empty means all four (the framework default).
9690 let s: LitStr = meta.value()?.parse()?;
9691 let raw = s.value();
9692 let mut actions: Vec<String> = Vec::new();
9693 for entry in raw.split(',') {
9694 let action = entry.trim().to_ascii_lowercase();
9695 if action.is_empty() {
9696 continue;
9697 }
9698 match action.as_str() {
9699 "add" | "change" | "delete" | "view" => {}
9700 other => {
9701 return Err(syn::Error::new(
9702 s.span(),
9703 format!(
9704 "unknown default_permissions action `{other}` — \
9705 expected one of `add`, `change`, `delete`, `view`"
9706 ),
9707 ));
9708 }
9709 }
9710 if !actions.contains(&action) {
9711 actions.push(action);
9712 }
9713 }
9714 if actions.is_empty() {
9715 return Err(syn::Error::new(
9716 s.span(),
9717 "`default_permissions = \"…\"` must list at least one action; \
9718 use `permissions = false` on the container if you want NO \
9719 permissions seeded for this model.",
9720 ));
9721 }
9722 out.default_permissions = actions;
9723 return Ok(());
9724 }
9725 if meta.path.is_ident("get_latest_by") {
9726 // The `-` prefix selects descending order.
9727 let s: LitStr = meta.value()?.parse()?;
9728 let raw = s.value();
9729 let trimmed = raw.trim();
9730 if trimmed.is_empty() {
9731 return Err(meta.error("`get_latest_by` must name a column"));
9732 }
9733 let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9734 (stripped.to_owned(), true)
9735 } else if let Some(stripped) = trimmed.strip_prefix('+') {
9736 (stripped.to_owned(), false)
9737 } else {
9738 (trimmed.to_owned(), false)
9739 };
9740 if col.is_empty() {
9741 return Err(meta.error("`get_latest_by` must name a column"));
9742 }
9743 out.get_latest_by = Some((col, desc));
9744 return Ok(());
9745 }
9746 if meta.path.is_ident("unique_together") {
9747 // Composite UNIQUE index. Two syntaxes:
9748 //
9749 // #[rustango(unique_together = "org_id, user_id")] — auto-derived name
9750 // #[rustango(unique_together(columns = "org_id, user_id", name = "x"))] — explicit name
9751 //
9752 // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9753 // (col1, col2)`, where <name> defaults to
9754 // `<table>_<col1>_<col2>_uq` when not supplied.
9755 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9756 out.indexes.push(IndexAttr {
9757 name,
9758 columns,
9759 unique: true,
9760 method: "btree".to_owned(),
9761 where_clause: None,
9762 include: Vec::new(),
9763 });
9764 return Ok(());
9765 }
9766 if meta.path.is_ident("index_together") {
9767 // Composite (non-unique) index. Two syntaxes
9768 // mirroring `unique_together`.
9769 //
9770 // #[rustango(index_together = "created_at, status")]
9771 // #[rustango(index_together(columns = "created_at, status", name = "x"))]
9772 let (columns, name) = parse_together_attr(&meta, "index_together")?;
9773 out.indexes.push(IndexAttr {
9774 name,
9775 columns,
9776 unique: false,
9777 method: "btree".to_owned(),
9778 where_clause: None,
9779 include: Vec::new(),
9780 });
9781 return Ok(());
9782 }
9783 if meta.path.is_ident("unique_when") {
9784 // Partial unique index. Issue #265 / T1.3.
9785 //
9786 // #[rustango(unique_when(
9787 // columns = "email",
9788 // condition = "deleted_at IS NULL",
9789 // name = "unique_active_email"
9790 // ))]
9791 //
9792 // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9793 // on PG / SQLite (both ship partial indexes natively).
9794 // MySQL falls back to a plain UNIQUE index — the
9795 // condition is lost; document the limitation in the
9796 // generated migration.
9797 let mut columns: Option<Vec<String>> = None;
9798 let mut condition: Option<String> = None;
9799 let mut name: Option<String> = None;
9800 let mut include: Vec<String> = Vec::new();
9801 meta.parse_nested_meta(|inner| {
9802 if inner.path.is_ident("columns") {
9803 let s: LitStr = inner.value()?.parse()?;
9804 columns = Some(split_field_list(&s.value()));
9805 return Ok(());
9806 }
9807 if inner.path.is_ident("condition") {
9808 let s: LitStr = inner.value()?.parse()?;
9809 condition = Some(s.value());
9810 return Ok(());
9811 }
9812 if inner.path.is_ident("name") {
9813 let s: LitStr = inner.value()?.parse()?;
9814 name = Some(s.value());
9815 return Ok(());
9816 }
9817 if inner.path.is_ident("include") {
9818 // PG 11+ covering-index columns. Non-key columns
9819 // travel with the index leaf for index-only
9820 // scans. Dropped on MySQL/SQLite by the writer.
9821 let s: LitStr = inner.value()?.parse()?;
9822 include = split_field_list(&s.value());
9823 return Ok(());
9824 }
9825 Err(inner.error(
9826 "unknown unique_when attribute (supported: \
9827 `columns = \"...\"`, `condition = \"...\"`, \
9828 `name = \"...\"`, `include = \"...\"`)",
9829 ))
9830 })?;
9831 let columns = columns.ok_or_else(|| {
9832 meta.error("`unique_when(...)` requires `columns = \"...\"`")
9833 })?;
9834 let condition = condition.ok_or_else(|| {
9835 meta.error("`unique_when(...)` requires `condition = \"...\"`")
9836 })?;
9837 if columns.is_empty() {
9838 return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9839 }
9840 out.indexes.push(IndexAttr {
9841 name,
9842 columns,
9843 unique: true,
9844 method: "btree".to_owned(),
9845 where_clause: Some(condition),
9846 include,
9847 });
9848 return Ok(());
9849 }
9850 if meta.path.is_ident("index_when") {
9851 // Non-unique partial index. Sibling of `unique_when`
9852 // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9853 //
9854 // #[rustango(index_when(
9855 // columns = "status, created_at",
9856 // condition = "deleted_at IS NULL",
9857 // name = "active_status_created_idx"
9858 // ))]
9859 //
9860 // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9861 // on PG / SQLite (both ship partial indexes natively).
9862 // MySQL has no native partial-index support — the writer
9863 // emits a plain CREATE INDEX and the condition is lost;
9864 // operators wanting that selectivity on MySQL should
9865 // declare a covering index plus an application-level
9866 // filter.
9867 let mut columns: Option<Vec<String>> = None;
9868 let mut condition: Option<String> = None;
9869 let mut name: Option<String> = None;
9870 let mut method: String = "btree".to_owned();
9871 let mut include: Vec<String> = Vec::new();
9872 meta.parse_nested_meta(|inner| {
9873 if inner.path.is_ident("columns") {
9874 let s: LitStr = inner.value()?.parse()?;
9875 columns = Some(split_field_list(&s.value()));
9876 return Ok(());
9877 }
9878 if inner.path.is_ident("condition") {
9879 let s: LitStr = inner.value()?.parse()?;
9880 condition = Some(s.value());
9881 return Ok(());
9882 }
9883 if inner.path.is_ident("name") {
9884 let s: LitStr = inner.value()?.parse()?;
9885 name = Some(s.value());
9886 return Ok(());
9887 }
9888 if inner.path.is_ident("method") {
9889 let s: LitStr = inner.value()?.parse()?;
9890 method = s.value();
9891 return Ok(());
9892 }
9893 if inner.path.is_ident("include") {
9894 // PG 11+ covering-index columns; non-key
9895 // columns travel with the index leaf. Dropped
9896 // on MySQL/SQLite.
9897 let s: LitStr = inner.value()?.parse()?;
9898 include = split_field_list(&s.value());
9899 return Ok(());
9900 }
9901 Err(inner.error(
9902 "unknown index_when attribute (supported: \
9903 `columns = \"...\"`, `condition = \"...\"`, \
9904 `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9905 `include = \"...\"`)",
9906 ))
9907 })?;
9908 let columns = columns
9909 .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9910 let condition = condition.ok_or_else(|| {
9911 meta.error("`index_when(...)` requires `condition = \"...\"`")
9912 })?;
9913 if columns.is_empty() {
9914 return Err(meta.error("`index_when(columns = \"\")` is empty"));
9915 }
9916 out.indexes.push(IndexAttr {
9917 name,
9918 columns,
9919 unique: false,
9920 method,
9921 where_clause: Some(condition),
9922 include,
9923 });
9924 return Ok(());
9925 }
9926 if meta.path.is_ident("index") {
9927 // Container-level composite index. Two syntaxes:
9928 // #[rustango(index = "col1, col2")] — bare, non-unique btree
9929 // #[rustango(index("col1, col2"))] — call form (same result)
9930 // #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
9931 // The call form takes the column list as a leading string
9932 // literal, then optional `unique` / `name = "..."` /
9933 // `method = "..."` flags (a leading literal can't compose
9934 // under `parse_nested_meta`, so the paren body is parsed by
9935 // hand). `unique_together` / `index_together` remain
9936 // aliases for the same feature.
9937 let cols_lit: LitStr;
9938 let mut unique = false;
9939 let mut name: Option<String> = None;
9940 let mut method = "btree".to_owned();
9941 if meta.input.peek(syn::token::Paren) {
9942 let content;
9943 syn::parenthesized!(content in meta.input);
9944 cols_lit = content.parse()?;
9945 while content.peek(syn::Token![,]) {
9946 content.parse::<syn::Token![,]>()?;
9947 if content.is_empty() {
9948 break;
9949 }
9950 let flag: syn::Ident = content.parse()?;
9951 if flag == "unique" {
9952 unique = true;
9953 } else if flag == "name" {
9954 content.parse::<syn::Token![=]>()?;
9955 let s: LitStr = content.parse()?;
9956 name = Some(s.value());
9957 } else if flag == "method" {
9958 content.parse::<syn::Token![=]>()?;
9959 let s: LitStr = content.parse()?;
9960 let v = s.value();
9961 match v.as_str() {
9962 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
9963 | "bloom" => method = v,
9964 other => {
9965 return Err(syn::Error::new(
9966 s.span(),
9967 format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
9968 ));
9969 }
9970 }
9971 } else {
9972 return Err(syn::Error::new(
9973 flag.span(),
9974 "unknown index flag (supported: `unique`, `name`, `method`)",
9975 ));
9976 }
9977 }
9978 } else {
9979 cols_lit = meta.value()?.parse()?;
9980 }
9981 let columns = split_field_list(&cols_lit.value());
9982 out.indexes.push(IndexAttr {
9983 name,
9984 columns,
9985 unique,
9986 method,
9987 where_clause: None,
9988 include: Vec::new(),
9989 });
9990 return Ok(());
9991 }
9992 if meta.path.is_ident("check") {
9993 // #[rustango(check(name = "…", expr = "…"))]
9994 let mut name: Option<String> = None;
9995 let mut expr: Option<String> = None;
9996 meta.parse_nested_meta(|inner| {
9997 if inner.path.is_ident("name") {
9998 let s: LitStr = inner.value()?.parse()?;
9999 name = Some(s.value());
10000 return Ok(());
10001 }
10002 if inner.path.is_ident("expr") {
10003 let s: LitStr = inner.value()?.parse()?;
10004 expr = Some(s.value());
10005 return Ok(());
10006 }
10007 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
10008 })?;
10009 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
10010 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
10011 out.checks.push(CheckAttr { name, expr });
10012 return Ok(());
10013 }
10014 if meta.path.is_ident("exclude") {
10015 // #[rustango(exclude(name = "…", using = "gist",
10016 // elements = "col WITH op, col WITH op",
10017 // where = "…"))]
10018 let mut name: Option<String> = None;
10019 let mut using: Option<String> = None;
10020 let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
10021 let mut where_clause: Option<String> = None;
10022 meta.parse_nested_meta(|inner| {
10023 if inner.path.is_ident("name") {
10024 let s: LitStr = inner.value()?.parse()?;
10025 name = Some(s.value());
10026 return Ok(());
10027 }
10028 if inner.path.is_ident("using") {
10029 let s: LitStr = inner.value()?.parse()?;
10030 using = Some(s.value());
10031 return Ok(());
10032 }
10033 if inner.path.is_ident("elements") {
10034 let s: LitStr = inner.value()?.parse()?;
10035 elements_raw = Some((s.value(), s.span()));
10036 return Ok(());
10037 }
10038 if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
10039 let s: LitStr = inner.value()?.parse()?;
10040 where_clause = Some(s.value());
10041 return Ok(());
10042 }
10043 Err(inner.error(
10044 "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
10045 ))
10046 })?;
10047 let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
10048 let using = using.unwrap_or_else(|| "gist".to_owned());
10049 let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
10050 meta.error(
10051 "exclude requires `elements = \"col WITH op, col WITH op\"`",
10052 )
10053 })?;
10054 // Parse `col WITH op` pairs separated by commas.
10055 let mut elements: Vec<(String, String)> = Vec::new();
10056 for pair in elements_str.split(',') {
10057 let pair = pair.trim();
10058 if pair.is_empty() {
10059 continue;
10060 }
10061 let mut split = pair.splitn(2, |c: char| c.is_whitespace());
10062 let col = split.next().unwrap_or("").trim();
10063 let rest = split.next().unwrap_or("").trim();
10064 // `WITH op` — case-insensitive on `WITH`, then op.
10065 let rest_lc = rest.to_ascii_lowercase();
10066 let op = rest_lc
10067 .strip_prefix("with")
10068 .map(|r| r.trim_start())
10069 .filter(|r| !r.is_empty())
10070 .map(|_| {
10071 // Pull the original-case op from `rest` after the
10072 // `WITH ` token (5 chars).
10073 rest[4..].trim_start().to_owned()
10074 });
10075 let Some(op) = op else {
10076 return Err(syn::Error::new(
10077 elements_span,
10078 format!(
10079 "exclude elements: `{pair}` must be `<col> WITH <op>` \
10080 (e.g. `room_id WITH =` or `during WITH &&`)"
10081 ),
10082 ));
10083 };
10084 if col.is_empty() || op.is_empty() {
10085 return Err(syn::Error::new(
10086 elements_span,
10087 format!(
10088 "exclude elements: `{pair}` must be `<col> WITH <op>` \
10089 (both sides non-empty)"
10090 ),
10091 ));
10092 }
10093 elements.push((col.to_owned(), op));
10094 }
10095 if elements.is_empty() {
10096 return Err(syn::Error::new(
10097 elements_span,
10098 "exclude requires at least one `col WITH op` element",
10099 ));
10100 }
10101 out.excludes.push(ExcludeAttr {
10102 name,
10103 using,
10104 elements,
10105 where_clause,
10106 });
10107 return Ok(());
10108 }
10109 if meta.path.is_ident("generic_fk") {
10110 let mut gfk = GenericFkAttr {
10111 name: String::new(),
10112 ct_column: String::new(),
10113 pk_column: String::new(),
10114 };
10115 meta.parse_nested_meta(|inner| {
10116 if inner.path.is_ident("name") {
10117 let s: LitStr = inner.value()?.parse()?;
10118 gfk.name = s.value();
10119 return Ok(());
10120 }
10121 if inner.path.is_ident("ct_column") {
10122 let s: LitStr = inner.value()?.parse()?;
10123 gfk.ct_column = s.value();
10124 return Ok(());
10125 }
10126 if inner.path.is_ident("pk_column") {
10127 let s: LitStr = inner.value()?.parse()?;
10128 gfk.pk_column = s.value();
10129 return Ok(());
10130 }
10131 Err(inner.error(
10132 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10133 ))
10134 })?;
10135 if gfk.name.is_empty() {
10136 return Err(meta.error("generic_fk requires `name = \"...\"`"));
10137 }
10138 if gfk.ct_column.is_empty() {
10139 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10140 }
10141 if gfk.pk_column.is_empty() {
10142 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10143 }
10144 out.generic_fks.push(gfk);
10145 return Ok(());
10146 }
10147 if meta.path.is_ident("fk_composite") {
10148 let mut fk = CompositeFkAttr {
10149 name: String::new(),
10150 to: String::new(),
10151 from: Vec::new(),
10152 on: Vec::new(),
10153 };
10154 meta.parse_nested_meta(|inner| {
10155 if inner.path.is_ident("name") {
10156 let s: LitStr = inner.value()?.parse()?;
10157 fk.name = s.value();
10158 return Ok(());
10159 }
10160 if inner.path.is_ident("to") {
10161 let s: LitStr = inner.value()?.parse()?;
10162 fk.to = s.value();
10163 return Ok(());
10164 }
10165 // `on = ("col1", "col2", ...)` — parse a parenthesised
10166 // comma-list of string literals.
10167 if inner.path.is_ident("on") || inner.path.is_ident("from") {
10168 let value = inner.value()?;
10169 let content;
10170 syn::parenthesized!(content in value);
10171 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10172 content.parse_terminated(
10173 |p| p.parse::<syn::LitStr>(),
10174 syn::Token![,],
10175 )?;
10176 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10177 if inner.path.is_ident("on") {
10178 fk.on = cols;
10179 } else {
10180 fk.from = cols;
10181 }
10182 return Ok(());
10183 }
10184 Err(inner.error(
10185 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10186 ))
10187 })?;
10188 if fk.name.is_empty() {
10189 return Err(meta.error("fk_composite requires `name = \"...\"`"));
10190 }
10191 if fk.to.is_empty() {
10192 return Err(meta.error("fk_composite requires `to = \"...\"`"));
10193 }
10194 if fk.from.is_empty() || fk.on.is_empty() {
10195 return Err(meta.error(
10196 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10197 ));
10198 }
10199 if fk.from.len() != fk.on.len() {
10200 return Err(meta.error(format!(
10201 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10202 fk.from.len(),
10203 fk.on.len(),
10204 )));
10205 }
10206 out.composite_fks.push(fk);
10207 return Ok(());
10208 }
10209 if meta.path.is_ident("m2m") {
10210 let mut m2m = M2MAttr {
10211 name: String::new(),
10212 to: String::new(),
10213 through: String::new(),
10214 src: String::new(),
10215 dst: String::new(),
10216 auto_create: true,
10217 };
10218 meta.parse_nested_meta(|inner| {
10219 if inner.path.is_ident("name") {
10220 let s: LitStr = inner.value()?.parse()?;
10221 m2m.name = s.value();
10222 return Ok(());
10223 }
10224 if inner.path.is_ident("to") {
10225 let s: LitStr = inner.value()?.parse()?;
10226 m2m.to = s.value();
10227 return Ok(());
10228 }
10229 if inner.path.is_ident("through") {
10230 let s: LitStr = inner.value()?.parse()?;
10231 m2m.through = s.value();
10232 return Ok(());
10233 }
10234 if inner.path.is_ident("src") {
10235 let s: LitStr = inner.value()?.parse()?;
10236 m2m.src = s.value();
10237 return Ok(());
10238 }
10239 if inner.path.is_ident("dst") {
10240 let s: LitStr = inner.value()?.parse()?;
10241 m2m.dst = s.value();
10242 return Ok(());
10243 }
10244 if inner.path.is_ident("auto_create") {
10245 let lit: syn::LitBool = inner.value()?.parse()?;
10246 m2m.auto_create = lit.value;
10247 return Ok(());
10248 }
10249 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10250 })?;
10251 if m2m.name.is_empty() {
10252 return Err(meta.error("m2m requires `name = \"...\"`"));
10253 }
10254 if m2m.to.is_empty() {
10255 return Err(meta.error("m2m requires `to = \"...\"`"));
10256 }
10257 if m2m.through.is_empty() {
10258 return Err(meta.error("m2m requires `through = \"...\"`"));
10259 }
10260 if m2m.src.is_empty() {
10261 return Err(meta.error("m2m requires `src = \"...\"`"));
10262 }
10263 if m2m.dst.is_empty() {
10264 return Err(meta.error("m2m requires `dst = \"...\"`"));
10265 }
10266 out.m2m.push(m2m);
10267 return Ok(());
10268 }
10269 if meta.path.is_ident("generic_m2m") {
10270 let mut gm = GenericM2MAttr {
10271 name: String::new(),
10272 through: String::new(),
10273 pk_column: String::new(),
10274 ct_column: String::new(),
10275 related_column: String::new(),
10276 };
10277 meta.parse_nested_meta(|inner| {
10278 let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10279 let s: LitStr = inner.value()?.parse()?;
10280 Ok(s.value())
10281 };
10282 if inner.path.is_ident("name") {
10283 gm.name = field(&inner)?;
10284 return Ok(());
10285 }
10286 if inner.path.is_ident("through") {
10287 gm.through = field(&inner)?;
10288 return Ok(());
10289 }
10290 if inner.path.is_ident("pk_column") {
10291 gm.pk_column = field(&inner)?;
10292 return Ok(());
10293 }
10294 if inner.path.is_ident("ct_column") {
10295 gm.ct_column = field(&inner)?;
10296 return Ok(());
10297 }
10298 if inner.path.is_ident("related_column") {
10299 gm.related_column = field(&inner)?;
10300 return Ok(());
10301 }
10302 Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10303 })?;
10304 for (val, label) in [
10305 (&gm.name, "name"),
10306 (&gm.through, "through"),
10307 (&gm.pk_column, "pk_column"),
10308 (&gm.ct_column, "ct_column"),
10309 (&gm.related_column, "related_column"),
10310 ] {
10311 if val.is_empty() {
10312 return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10313 }
10314 }
10315 out.generic_m2m.push(gm);
10316 return Ok(());
10317 }
10318 Err(meta.error("unknown rustango container attribute"))
10319 })?;
10320 }
10321 Ok(out)
10322}
10323
10324/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10325/// owned field names, trimming whitespace and skipping empty entries.
10326/// Field-name validation against the model is done by the caller.
10327fn split_field_list(raw: &str) -> Vec<String> {
10328 raw.split(',')
10329 .map(str::trim)
10330 .filter(|s| !s.is_empty())
10331 .map(str::to_owned)
10332 .collect()
10333}
10334
10335/// Shared parser for `unique_together` and `index_together` container
10336/// attrs. Accepts both shapes:
10337///
10338/// * `attr = "col1, col2"` — auto-derived index name.
10339/// * `attr(columns = "col1, col2", name = "...")` — explicit name.
10340///
10341/// Returns `(columns, name)`.
10342fn parse_together_attr(
10343 meta: &syn::meta::ParseNestedMeta<'_>,
10344 attr: &str,
10345) -> syn::Result<(Vec<String>, Option<String>)> {
10346 // Disambiguate by whether the next token is `=` (key-value) or
10347 // `(` (parenthesized).
10348 if meta.input.peek(syn::Token![=]) {
10349 let cols_lit: LitStr = meta.value()?.parse()?;
10350 let columns = split_field_list(&cols_lit.value());
10351 check_together_columns(meta, attr, &columns)?;
10352 return Ok((columns, None));
10353 }
10354 let mut columns: Option<Vec<String>> = None;
10355 let mut name: Option<String> = None;
10356 meta.parse_nested_meta(|inner| {
10357 if inner.path.is_ident("columns") {
10358 let s: LitStr = inner.value()?.parse()?;
10359 columns = Some(split_field_list(&s.value()));
10360 return Ok(());
10361 }
10362 if inner.path.is_ident("name") {
10363 let s: LitStr = inner.value()?.parse()?;
10364 name = Some(s.value());
10365 return Ok(());
10366 }
10367 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10368 })?;
10369 let columns = columns.ok_or_else(|| {
10370 meta.error(format!(
10371 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10372 ))
10373 })?;
10374 check_together_columns(meta, attr, &columns)?;
10375 Ok((columns, name))
10376}
10377
10378fn check_together_columns(
10379 meta: &syn::meta::ParseNestedMeta<'_>,
10380 attr: &str,
10381 columns: &[String],
10382) -> syn::Result<()> {
10383 if columns.len() < 2 {
10384 let single = if attr == "unique_together" {
10385 "#[rustango(unique)] on the field"
10386 } else {
10387 "#[rustango(index)] on the field"
10388 };
10389 return Err(meta.error(format!(
10390 "{attr} expects two or more columns; for a single-column equivalent use {single}",
10391 )));
10392 }
10393 Ok(())
10394}
10395
10396/// Parse the fieldsets DSL: pipe-separated sections, optional
10397/// `"Title:"` prefix on each, comma-separated field names after.
10398/// Examples:
10399/// * `"name, office"` → one untitled section with two fields
10400/// * `"Identity: name, office | Metadata: created_at"` → two titled
10401/// sections
10402///
10403/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10404fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10405 raw.split('|')
10406 .map(str::trim)
10407 .filter(|s| !s.is_empty())
10408 .map(|section| {
10409 // Split off an optional `Title:` prefix (first colon).
10410 let (title, rest) = match section.split_once(':') {
10411 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10412 _ => (String::new(), section),
10413 };
10414 let fields = split_field_list(rest);
10415 (title, fields)
10416 })
10417 .collect()
10418}
10419
10420/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10421/// comma-separated entry maps a target field to one or more source
10422/// fields joined with `+`. Whitespace around tokens is trimmed.
10423/// Entries missing `:` or with empty target/source lists are dropped.
10424fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10425 raw.split(',')
10426 .map(str::trim)
10427 .filter(|s| !s.is_empty())
10428 .filter_map(|entry| {
10429 let (target, sources_raw) = entry.split_once(':')?;
10430 let target = target.trim().to_owned();
10431 if target.is_empty() {
10432 return None;
10433 }
10434 let sources: Vec<String> = sources_raw
10435 .split('+')
10436 .map(|s| s.trim().to_owned())
10437 .filter(|s| !s.is_empty())
10438 .collect();
10439 if sources.is_empty() {
10440 return None;
10441 }
10442 Some((target, sources))
10443 })
10444 .collect()
10445}
10446
10447/// Parse `formfield_overrides` — `"field:widget,field2:widget2"`
10448/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10449/// and empty halves drop silently — the macro layer only enforces shape,
10450/// not field-name vs. widget-name validity (those checks happen at
10451/// `AdminConfig` consumption time). Issue #359.
10452fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10453 raw.split(',')
10454 .map(str::trim)
10455 .filter(|s| !s.is_empty())
10456 .filter_map(|entry| {
10457 let (field, widget) = entry.split_once(':')?;
10458 let field = field.trim().to_owned();
10459 let widget = widget.trim().to_owned();
10460 if field.is_empty() || widget.is_empty() {
10461 return None;
10462 }
10463 Some((field, widget))
10464 })
10465 .collect()
10466}
10467
10468/// Parse an ordering list — `"name"` is ASC, `"-name"` is DESC.
10469/// Returns `(field_name, desc)` pairs in the same order as the input.
10470fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10471 raw.split(',')
10472 .map(str::trim)
10473 .filter(|s| !s.is_empty())
10474 .map(|spec| {
10475 spec.strip_prefix('-')
10476 .map_or((spec.to_owned(), false), |rest| {
10477 (rest.trim().to_owned(), true)
10478 })
10479 })
10480 .collect()
10481}
10482
10483struct FieldAttrs {
10484 column: Option<String>,
10485 primary_key: bool,
10486 fk: Option<String>,
10487 o2o: Option<String>,
10488 on: Option<String>,
10489 /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10490 /// "set_default" | "no_action")]` — what happens to this row when
10491 /// the row it points at is deleted.
10492 /// Only meaningful when `fk` / `o2o` is
10493 /// also set; the macro errors at compile time if applied to a
10494 /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10495 /// DDL writer renders `ON DELETE <action>` after the constraint
10496 /// clause when this is `Some`; `None` falls back to the database
10497 /// default (NO ACTION on every backend rustango supports).
10498 on_delete: Option<String>,
10499 /// `#[rustango(related_name = "...")]` — per-FK
10500 /// reverse-accessor override. When set, the derive emits
10501 /// `Parent::<related_name>[_pool]` instead of the container-level
10502 /// `default_related_name` or the `<child_snake>_set[_pool]`
10503 /// fallback. Only meaningful when `fk` / `o2o` is also set;
10504 /// silently ignored on non-FK fields. Follow-up to #816.
10505 related_name: Option<String>,
10506 max_length: Option<u32>,
10507 /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10508 /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10509 /// unconstrained `vector` column.
10510 vector_dims: Option<u32>,
10511 /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10512 /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10513 /// unconstrained `geometry(Point)` column (SRID 0).
10514 geometry_srid: Option<u32>,
10515 min: Option<i64>,
10516 max: Option<i64>,
10517 default: Option<String>,
10518 /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10519 /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10520 /// "gen_random_uuid()"`. The Rust field type must be
10521 /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10522 /// INSERTs so the DB DEFAULT fires.
10523 auto_uuid: bool,
10524 /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10525 /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10526 /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10527 /// the field is `Auto::Unset`, then bound as a normal parameter
10528 /// rather than relying on a per-dialect DB function. Issue #823
10529 /// (Eloquent `HasUuids`).
10530 ///
10531 /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10532 /// Composes with every backend (PG / MySQL / SQLite) — no
10533 /// `gen_random_uuid()` requirement on the database.
10534 default_uuid_v7: bool,
10535 /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10536 /// Server-set on insert, immutable from app code afterwards.
10537 /// Implies `auto + default = "now()"`. Field type must be
10538 /// `DateTime<Utc>`.
10539 auto_now_add: bool,
10540 /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10541 /// every insert AND every update. Implies `auto + default =
10542 /// "now()"`; the macro additionally rewrites `update_on` /
10543 /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10544 /// field value.
10545 auto_now: bool,
10546 /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10547 /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10548 /// `soft_delete_on(executor)` and `restore_on(executor)`
10549 /// methods on the model.
10550 soft_delete: bool,
10551 /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10552 /// the column in the generated DDL.
10553 unique: bool,
10554 /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10555 /// generates a `CREATE INDEX` for this column. `unique` here means
10556 /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10557 index: bool,
10558 index_unique: bool,
10559 index_name: Option<String>,
10560 /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10561 /// `"btree"`. Issue #34.
10562 index_method: String,
10563 /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10564 /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10565 /// the macro skips this column from every INSERT and UPDATE
10566 /// path, so the database always recomputes the value from
10567 /// `EXPR`. Backlog item #35.
10568 generated_as: Option<String>,
10569 /// `#[rustango(help_text = "…")]` — help text
10570 /// rendered below the admin form's input. Threaded into
10571 /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10572 /// layers can read it.
10573 help_text: Option<String>,
10574 /// `#[rustango(choices = "value:Label, value:Label")]` — the
10575 /// enumerated allowed values. Threaded into `FieldSchema::choices`
10576 /// as a `&'static [(&'static str, &'static str)]` slice. When
10577 /// present, the admin form renders a `<select>` instead of `<input>`
10578 /// and the validator rejects values not in the list. Only meaningful
10579 /// for `FieldType::String`; the macro errors at compile time if
10580 /// applied to a non-string field.
10581 choices: Option<Vec<(String, String)>>,
10582 /// `#[rustango(db_comment = "…")]` — DB-side column
10583 /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10584 /// the comment in CREATE TABLE; Postgres emits a separate
10585 /// `COMMENT ON COLUMN` statement after the table is created;
10586 /// SQLite silently drops the value (no native column comments).
10587 db_comment: Option<String>,
10588 /// `#[rustango(verbose_name = "…")]` — human-readable
10589 /// label for the field. Threaded into `FieldSchema::verbose_name`
10590 /// so admin column headers, form labels, and other display
10591 /// surfaces can prefer the friendly caption over the Rust
10592 /// identifier. `None` means renderers fall back to the field name.
10593 verbose_name: Option<String>,
10594 /// `#[rustango(editable = false)]` — opt out of
10595 /// auto-generated form rendering. Defaults to `true` so existing
10596 /// fields keep their current admin / form behavior; setting
10597 /// `false` removes the field from the admin change-form entirely
10598 /// (the value is still visible on detail / list views, just not
10599 /// editable).
10600 editable: bool,
10601 /// `#[rustango(blank)]` / `#[rustango(blank = true)]` —
10602 /// "form may submit empty even when DB is NOT NULL". Threaded into
10603 /// `FieldSchema::blank`. Defaults to `false`.
10604 blank: bool,
10605 /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10606 /// case-insensitive text column. Threaded into
10607 /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10608 /// fields; the macro errors at derive time if applied elsewhere.
10609 case_insensitive: bool,
10610 /// `#[rustango(validators = "email,url")]` — the
10611 /// model-level validator chain. Comma-separated names that
10612 /// dispatch to the `validators::*` family in `validate_value`.
10613 /// Empty by default; fires on every typed INSERT/UPDATE.
10614 validators: Vec<String>,
10615}
10616
10617fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10618 let mut out = FieldAttrs {
10619 column: None,
10620 primary_key: false,
10621 fk: None,
10622 o2o: None,
10623 on: None,
10624 on_delete: None,
10625 related_name: None,
10626 max_length: None,
10627 vector_dims: None,
10628 geometry_srid: None,
10629 min: None,
10630 max: None,
10631 default: None,
10632 auto_uuid: false,
10633 default_uuid_v7: false,
10634 auto_now_add: false,
10635 auto_now: false,
10636 soft_delete: false,
10637 unique: false,
10638 index: false,
10639 index_unique: false,
10640 index_name: None,
10641 index_method: "btree".to_owned(),
10642 generated_as: None,
10643 help_text: None,
10644 choices: None,
10645 db_comment: None,
10646 verbose_name: None,
10647 editable: true,
10648 blank: false,
10649 case_insensitive: false,
10650 validators: Vec::new(),
10651 };
10652 for attr in &field.attrs {
10653 if !attr.path().is_ident("rustango") {
10654 continue;
10655 }
10656 attr.parse_nested_meta(|meta| {
10657 if meta.path.is_ident("column") {
10658 let s: LitStr = meta.value()?.parse()?;
10659 let name = s.value();
10660 validate_sql_identifier(&name, "column", s.span())?;
10661 out.column = Some(name);
10662 return Ok(());
10663 }
10664 if meta.path.is_ident("primary_key") {
10665 out.primary_key = true;
10666 return Ok(());
10667 }
10668 if meta.path.is_ident("fk") {
10669 let s: LitStr = meta.value()?.parse()?;
10670 out.fk = Some(s.value());
10671 return Ok(());
10672 }
10673 if meta.path.is_ident("o2o") {
10674 let s: LitStr = meta.value()?.parse()?;
10675 out.o2o = Some(s.value());
10676 return Ok(());
10677 }
10678 if meta.path.is_ident("on") {
10679 let s: LitStr = meta.value()?.parse()?;
10680 out.on = Some(s.value());
10681 return Ok(());
10682 }
10683 if meta.path.is_ident("on_delete") {
10684 let s: LitStr = meta.value()?.parse()?;
10685 let raw = s.value();
10686 let normalized = raw.trim().to_ascii_lowercase();
10687 // Validate at parse time so the user gets a clear span
10688 // rather than a downstream compile error in the emit.
10689 match normalized.as_str() {
10690 "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10691 _ => {
10692 return Err(syn::Error::new(
10693 s.span(),
10694 format!(
10695 "unknown on_delete action `{raw}`; expected one of \
10696 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10697 ),
10698 ));
10699 }
10700 }
10701 out.on_delete = Some(normalized);
10702 return Ok(());
10703 }
10704 if meta.path.is_ident("related_name") {
10705 let s: LitStr = meta.value()?.parse()?;
10706 let raw = s.value();
10707 if raw.trim().is_empty() {
10708 return Err(syn::Error::new(
10709 s.span(),
10710 "`related_name` must be a non-empty identifier",
10711 ));
10712 }
10713 // Validate as a Rust-ident shape — the value becomes
10714 // a method name on the parent type. Same rule as
10715 // `default_related_name` so the two surfaces match.
10716 if !raw
10717 .chars()
10718 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10719 || raw.starts_with(char::is_numeric)
10720 {
10721 return Err(syn::Error::new(
10722 s.span(),
10723 "`related_name` must be snake_case ASCII (lowercase letters, \
10724 digits, underscores; no leading digit)",
10725 ));
10726 }
10727 out.related_name = Some(raw);
10728 return Ok(());
10729 }
10730 if meta.path.is_ident("max_length") {
10731 let lit: syn::LitInt = meta.value()?.parse()?;
10732 out.max_length = Some(lit.base10_parse::<u32>()?);
10733 return Ok(());
10734 }
10735 // `#[rustango(vector(dims = N))]` — pgvector column
10736 // dimension (#824). Nested-meta form so it reads like the
10737 // other typed-column attrs.
10738 if meta.path.is_ident("vector") {
10739 meta.parse_nested_meta(|inner| {
10740 if inner.path.is_ident("dims") {
10741 let lit: syn::LitInt = inner.value()?.parse()?;
10742 out.vector_dims = Some(lit.base10_parse::<u32>()?);
10743 return Ok(());
10744 }
10745 Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10746 })?;
10747 return Ok(());
10748 }
10749 // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10750 // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10751 if meta.path.is_ident("geometry") {
10752 meta.parse_nested_meta(|inner| {
10753 if inner.path.is_ident("srid") {
10754 let lit: syn::LitInt = inner.value()?.parse()?;
10755 out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10756 return Ok(());
10757 }
10758 Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10759 })?;
10760 return Ok(());
10761 }
10762 if meta.path.is_ident("min") {
10763 out.min = Some(parse_signed_i64(&meta)?);
10764 return Ok(());
10765 }
10766 if meta.path.is_ident("max") {
10767 out.max = Some(parse_signed_i64(&meta)?);
10768 return Ok(());
10769 }
10770 if meta.path.is_ident("default") {
10771 let s: LitStr = meta.value()?.parse()?;
10772 out.default = Some(s.value());
10773 return Ok(());
10774 }
10775 if meta.path.is_ident("generated_as") {
10776 let s: LitStr = meta.value()?.parse()?;
10777 out.generated_as = Some(s.value());
10778 return Ok(());
10779 }
10780 if meta.path.is_ident("help_text") {
10781 let s: LitStr = meta.value()?.parse()?;
10782 out.help_text = Some(s.value());
10783 return Ok(());
10784 }
10785 if meta.path.is_ident("choices") {
10786 let s: LitStr = meta.value()?.parse()?;
10787 let raw = s.value();
10788 let mut pairs: Vec<(String, String)> = Vec::new();
10789 for chunk in raw.split(',') {
10790 let chunk = chunk.trim();
10791 if chunk.is_empty() {
10792 continue;
10793 }
10794 let (value, label) = match chunk.split_once(':') {
10795 Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10796 None => (chunk.to_owned(), chunk.to_owned()),
10797 };
10798 if value.is_empty() {
10799 return Err(syn::Error::new(
10800 s.span(),
10801 "`choices` entry has empty value before `:`",
10802 ));
10803 }
10804 pairs.push((value, label));
10805 }
10806 if pairs.is_empty() {
10807 return Err(syn::Error::new(
10808 s.span(),
10809 "`choices = \"…\"` must contain at least one value",
10810 ));
10811 }
10812 out.choices = Some(pairs);
10813 return Ok(());
10814 }
10815 if meta.path.is_ident("db_comment") {
10816 let s: LitStr = meta.value()?.parse()?;
10817 out.db_comment = Some(s.value());
10818 return Ok(());
10819 }
10820 if meta.path.is_ident("verbose_name") {
10821 let s: LitStr = meta.value()?.parse()?;
10822 out.verbose_name = Some(s.value());
10823 return Ok(());
10824 }
10825 if meta.path.is_ident("editable") {
10826 // Two forms accepted:
10827 // #[rustango(editable = false)] / true — explicit
10828 // #[rustango(editable)] — flag form (= true, the
10829 // default, so harmless; included for symmetry)
10830 if let Ok(v) = meta.value() {
10831 let lit: syn::LitBool = v.parse()?;
10832 out.editable = lit.value;
10833 } else {
10834 out.editable = true;
10835 }
10836 return Ok(());
10837 }
10838 if meta.path.is_ident("blank") {
10839 // Two forms accepted:
10840 // #[rustango(blank)] — flag form, true
10841 // #[rustango(blank = false)] / true — explicit
10842 if let Ok(v) = meta.value() {
10843 let lit: syn::LitBool = v.parse()?;
10844 out.blank = lit.value;
10845 } else {
10846 out.blank = true;
10847 }
10848 return Ok(());
10849 }
10850 if meta.path.is_ident("citext") {
10851 // Case-insensitive text column (#344). Two forms:
10852 // #[rustango(citext)] — flag form, true
10853 // #[rustango(citext = true)] — explicit
10854 // #[rustango(citext = false)] — explicit opt-out
10855 // String-only validation lives in the field-type
10856 // emitter (the FieldType discriminant is computed in
10857 // `detect_type`); the macro records the flag and the
10858 // DDL writer emits dialect-specific COLLATE / CITEXT.
10859 if let Ok(v) = meta.value() {
10860 let lit: syn::LitBool = v.parse()?;
10861 out.case_insensitive = lit.value;
10862 } else {
10863 out.case_insensitive = true;
10864 }
10865 return Ok(());
10866 }
10867 if meta.path.is_ident("validators") {
10868 let s: LitStr = meta.value()?.parse()?;
10869 let raw = s.value();
10870 out.validators = raw
10871 .split(',')
10872 .map(str::trim)
10873 .filter(|s| !s.is_empty())
10874 .map(str::to_owned)
10875 .collect();
10876 if out.validators.is_empty() {
10877 return Err(syn::Error::new(
10878 s.span(),
10879 "`validators = \"…\"` must list at least one name",
10880 ));
10881 }
10882 return Ok(());
10883 }
10884 if meta.path.is_ident("auto_uuid") {
10885 out.auto_uuid = true;
10886 // Implied: PK + auto + DEFAULT gen_random_uuid().
10887 // Each is also explicitly settable; the explicit
10888 // value wins if conflicting.
10889 out.primary_key = true;
10890 if out.default.is_none() {
10891 out.default = Some("gen_random_uuid()".into());
10892 }
10893 return Ok(());
10894 }
10895 if meta.path.is_ident("default_uuid_v7") {
10896 // Backend-neutral counterpart of `auto_uuid` — issue #823.
10897 // No SQL DEFAULT (the macro fills the value Rust-side
10898 // before binding); just mark the field as PK + Auto
10899 // so the insert path is the `Auto::Unset → generate`
10900 // branch.
10901 out.default_uuid_v7 = true;
10902 out.primary_key = true;
10903 return Ok(());
10904 }
10905 if meta.path.is_ident("auto_now_add") {
10906 out.auto_now_add = true;
10907 if out.default.is_none() {
10908 out.default = Some("now()".into());
10909 }
10910 return Ok(());
10911 }
10912 if meta.path.is_ident("auto_now") {
10913 out.auto_now = true;
10914 if out.default.is_none() {
10915 out.default = Some("now()".into());
10916 }
10917 return Ok(());
10918 }
10919 if meta.path.is_ident("soft_delete") {
10920 out.soft_delete = true;
10921 return Ok(());
10922 }
10923 if meta.path.is_ident("unique") {
10924 out.unique = true;
10925 return Ok(());
10926 }
10927 if meta.path.is_ident("index") {
10928 out.index = true;
10929 // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
10930 if meta.input.peek(syn::token::Paren) {
10931 meta.parse_nested_meta(|inner| {
10932 if inner.path.is_ident("unique") {
10933 out.index_unique = true;
10934 return Ok(());
10935 }
10936 if inner.path.is_ident("name") {
10937 let s: LitStr = inner.value()?.parse()?;
10938 out.index_name = Some(s.value());
10939 return Ok(());
10940 }
10941 if inner.path.is_ident("method") {
10942 let s: LitStr = inner.value()?.parse()?;
10943 let v = s.value();
10944 match v.as_str() {
10945 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
10946 out.index_method = v;
10947 }
10948 other => {
10949 return Err(inner.error(format!(
10950 "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
10951 )));
10952 }
10953 }
10954 return Ok(());
10955 }
10956 Err(inner.error(
10957 "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
10958 ))
10959 })?;
10960 }
10961 return Ok(());
10962 }
10963 Err(meta.error("unknown rustango field attribute"))
10964 })?;
10965 }
10966 Ok(out)
10967}
10968
10969/// Parse a signed integer literal, accepting optional leading `-`.
10970fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
10971 let expr: syn::Expr = meta.value()?.parse()?;
10972 match expr {
10973 syn::Expr::Lit(syn::ExprLit {
10974 lit: syn::Lit::Int(lit),
10975 ..
10976 }) => lit.base10_parse::<i64>(),
10977 syn::Expr::Unary(syn::ExprUnary {
10978 op: syn::UnOp::Neg(_),
10979 expr,
10980 ..
10981 }) => {
10982 if let syn::Expr::Lit(syn::ExprLit {
10983 lit: syn::Lit::Int(lit),
10984 ..
10985 }) = *expr
10986 {
10987 let v: i64 = lit.base10_parse()?;
10988 Ok(-v)
10989 } else {
10990 Err(syn::Error::new_spanned(expr, "expected integer literal"))
10991 }
10992 }
10993 other => Err(syn::Error::new_spanned(
10994 other,
10995 "expected integer literal (signed)",
10996 )),
10997 }
10998}
10999
11000struct FieldInfo<'a> {
11001 ident: &'a syn::Ident,
11002 column: String,
11003 primary_key: bool,
11004 /// `true` when the Rust type was `Auto<T>` — the INSERT path will
11005 /// skip this column when `Auto::Unset` and emit it under
11006 /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
11007 auto: bool,
11008 /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
11009 /// the `Column::Value` associated type for typed-column tokens.
11010 value_ty: &'a Type,
11011 /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
11012 field_type_tokens: TokenStream2,
11013 schema: TokenStream2,
11014 from_row_init: TokenStream2,
11015 /// Variant of [`Self::from_row_init`] that reads the column via
11016 /// `format!("{prefix}__{col}")` so a model can be decoded out of
11017 /// the aliased columns of a JOINed row. Drives slice 9.0d's
11018 /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
11019 /// helper that `select_related` calls when stitching loaded FKs.
11020 from_aliased_row_init: TokenStream2,
11021 /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
11022 /// relation helper emit (`Author::<child>_set`) needs to know `T`
11023 /// to point the generated method at the right child model.
11024 fk_inner: Option<Type>,
11025 /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
11026 /// `kind` (since ForeignKey detection sets `kind` to K's
11027 /// underlying type) but stored separately for clarity at the
11028 /// `FkRelation` construction site, which only sees the FK's
11029 /// surface fields.
11030 fk_pk_kind: DetectedKind,
11031 /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
11032 /// the bare `ForeignKey<T, K>`. Routes the load_related and
11033 /// fk_pk_access emitters to wrap assignments / accessors in
11034 /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
11035 /// FK column compiles end-to-end. The DDL writer reads this off
11036 /// the field schema (`nullable` flag); the macro just needs to
11037 /// keep the Rust-side codegen consistent.
11038 nullable: bool,
11039 /// `true` when this column was marked `#[rustango(auto_now)]` —
11040 /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
11041 /// column instead of the user-supplied value, so `updated_at`
11042 /// always reflects the latest write without the caller having
11043 /// to remember to set it.
11044 auto_now: bool,
11045 /// `true` when this column was marked `#[rustango(auto_now_add)]`
11046 /// — the column is server-set on INSERT (DB DEFAULT) and
11047 /// **immutable** afterwards. `update_on` / `save_on` skip the
11048 /// column entirely so a stale `created_at` value in memory never
11049 /// rewrites the persisted timestamp.
11050 auto_now_add: bool,
11051 /// `true` when this column was marked `#[rustango(soft_delete)]`.
11052 /// Triggers emission of `soft_delete_on(executor)` and
11053 /// `restore_on(executor)` on the model's inherent impl. There is
11054 /// at most one such column per model — emission asserts this.
11055 soft_delete: bool,
11056 /// `Some` when this column was marked
11057 /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
11058 /// every INSERT and UPDATE path; the database recomputes the
11059 /// value from `EXPR`. Backlog item #35.
11060 generated_as: Option<String>,
11061 /// `true` when this column was marked
11062 /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
11063 /// emit an `insert_push` that auto-fills an `Auto::Unset` value
11064 /// with `Uuid::now_v7()` before binding, so the PK is generated
11065 /// Rust-side and the column is always present in the INSERT
11066 /// statement (no DB DEFAULT requirement). Issue #823.
11067 default_uuid_v7: bool,
11068 /// `Some` when this FK field carried `#[rustango(related_name =
11069 /// "...")]`. Threaded into [`FkRelation::related_name`] and
11070 /// consumed by [`reverse_helper_tokens`] to override the default
11071 /// `<child_snake>_set` accessor name. Follow-up to #816.
11072 related_name: Option<String>,
11073}
11074
11075/// Reject table names that won't survive SQL identifier
11076/// derivation downstream. Postgres' regular-identifier rule
11077/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
11078/// through the framework's unquoted FK / index / constraint name
11079/// emission without surprises. We also disallow leading-digit and
11080/// the empty string for clarity.
11081///
11082/// Reserved-word collisions (`select`, `from`, …) aren't flagged
11083/// here — those produce a runtime error from the SQL parser,
11084/// which is loud enough; statically enumerating reserved words
11085/// across the three supported dialects is more friction than help.
11086///
11087/// Backlog item #65.
11088fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11089 validate_sql_identifier(name, "table", span)
11090}
11091
11092/// Reject SQL identifiers that compile but break downstream SQL
11093/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11094/// `kind` is "table" / "column" — used for the error message so users
11095/// see which attribute caused the failure.
11096fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11097 if name.is_empty() {
11098 return Err(syn::Error::new(
11099 span,
11100 format!("`{kind} = \"\"` is not a valid SQL identifier"),
11101 ));
11102 }
11103 let mut chars = name.chars();
11104 let first = chars.next().unwrap();
11105 if !(first.is_ascii_alphabetic() || first == '_') {
11106 return Err(syn::Error::new(
11107 span,
11108 format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11109 ));
11110 }
11111 for c in chars {
11112 if !(c.is_ascii_alphanumeric() || c == '_') {
11113 return Err(syn::Error::new(
11114 span,
11115 format!(
11116 "{kind} name `{name}` contains invalid character {c:?} — \
11117 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11118 Hyphens in particular break FK / index name derivation \
11119 downstream; use underscores instead (e.g. `{}`)",
11120 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11121 ),
11122 ));
11123 }
11124 }
11125 Ok(())
11126}
11127
11128fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11129 let root = rustango_root();
11130 let attrs = parse_field_attrs(field)?;
11131 let ident = field
11132 .ident
11133 .as_ref()
11134 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11135 let name = ident.to_string();
11136 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11137 let primary_key = attrs.primary_key;
11138 let DetectedType {
11139 kind,
11140 nullable,
11141 auto: detected_auto,
11142 fk_inner,
11143 } = detect_type(&field.ty)?;
11144 check_bound_compatibility(field, &attrs, kind)?;
11145 let auto = detected_auto;
11146 // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11147 // INSERT path: the user must wrap the field in `Auto<T>`, which
11148 // marks the column as DB-default-supplied. The mixin attrs then
11149 // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11150 // for `auto_now`, force the value on UPDATE too.
11151 if attrs.auto_uuid {
11152 if kind != DetectedKind::Uuid {
11153 return Err(syn::Error::new_spanned(
11154 field,
11155 "`#[rustango(auto_uuid)]` requires the field type to be \
11156 `Auto<uuid::Uuid>`",
11157 ));
11158 }
11159 if !detected_auto {
11160 return Err(syn::Error::new_spanned(
11161 field,
11162 "`#[rustango(auto_uuid)]` requires the field type to be \
11163 wrapped in `Auto<...>` so the macro skips the column on \
11164 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11165 ));
11166 }
11167 }
11168 if attrs.default_uuid_v7 {
11169 if kind != DetectedKind::Uuid {
11170 return Err(syn::Error::new_spanned(
11171 field,
11172 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11173 `Auto<uuid::Uuid>`",
11174 ));
11175 }
11176 if !detected_auto {
11177 return Err(syn::Error::new_spanned(
11178 field,
11179 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11180 wrapped in `Auto<...>` so the macro can detect the \
11181 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11182 ));
11183 }
11184 if attrs.auto_uuid {
11185 return Err(syn::Error::new_spanned(
11186 field,
11187 "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11188 `#[rustango(auto_uuid)]` — the former generates the UUID \
11189 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11190 Pick one.",
11191 ));
11192 }
11193 }
11194 if attrs.auto_now_add || attrs.auto_now {
11195 if kind != DetectedKind::DateTime {
11196 return Err(syn::Error::new_spanned(
11197 field,
11198 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11199 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11200 ));
11201 }
11202 if !detected_auto {
11203 return Err(syn::Error::new_spanned(
11204 field,
11205 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11206 the field type to be wrapped in `Auto<...>` so the macro skips \
11207 the column on INSERT and the DB DEFAULT (`now()`) fires",
11208 ));
11209 }
11210 }
11211 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11212 return Err(syn::Error::new_spanned(
11213 field,
11214 "`#[rustango(soft_delete)]` requires the field type to be \
11215 `Option<chrono::DateTime<chrono::Utc>>`",
11216 ));
11217 }
11218 let is_mixin_auto =
11219 attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11220 if detected_auto && !primary_key && !is_mixin_auto {
11221 return Err(syn::Error::new_spanned(
11222 field,
11223 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11224 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11225 `auto_now`",
11226 ));
11227 }
11228 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11229 return Err(syn::Error::new_spanned(
11230 field,
11231 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11232 SERIAL / BIGSERIAL already supplies a default sequence.",
11233 ));
11234 }
11235 if fk_inner.is_some() && primary_key {
11236 return Err(syn::Error::new_spanned(
11237 field,
11238 "`ForeignKey<T>` is not allowed on a primary-key field — \
11239 a row's PK is its own identity, not a reference to a parent.",
11240 ));
11241 }
11242 if attrs.generated_as.is_some() {
11243 if primary_key {
11244 return Err(syn::Error::new_spanned(
11245 field,
11246 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11247 primary-key field — a PK must be writable so the row \
11248 has an identity at INSERT time.",
11249 ));
11250 }
11251 if attrs.default.is_some() {
11252 return Err(syn::Error::new_spanned(
11253 field,
11254 "`#[rustango(generated_as = \"…\")]` cannot combine with \
11255 `default = \"…\"` — Postgres rejects DEFAULT on \
11256 generated columns. The expression IS the default.",
11257 ));
11258 }
11259 if detected_auto {
11260 return Err(syn::Error::new_spanned(
11261 field,
11262 "`#[rustango(generated_as = \"…\")]` is not allowed on \
11263 an `Auto<T>` field — generated columns are computed \
11264 by the DB, not server-assigned via a sequence. Use a \
11265 plain Rust type (e.g. `f64`).",
11266 ));
11267 }
11268 if fk_inner.is_some() {
11269 return Err(syn::Error::new_spanned(
11270 field,
11271 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11272 ForeignKey field.",
11273 ));
11274 }
11275 }
11276 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11277 let column_lit = column.as_str();
11278 // pgvector (#824): the `vector(dims = N)` attribute supplies the
11279 // dimension that the bare `Vector` Rust type can't carry, so emit
11280 // `FieldType::Vector(N)` here rather than the `variant_tokens`
11281 // fallback of `Vector(0)`.
11282 let field_type_tokens = if kind == DetectedKind::Vector {
11283 let root = rustango_root();
11284 let dims = attrs.vector_dims.unwrap_or(0);
11285 quote!(#root::core::FieldType::Vector(#dims))
11286 } else if kind == DetectedKind::Geometry {
11287 // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11288 // SRID that the bare `Point` Rust type can't carry, so emit
11289 // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11290 let root = rustango_root();
11291 let srid = attrs.geometry_srid.unwrap_or(0);
11292 quote!(#root::core::FieldType::Geometry(#srid))
11293 } else {
11294 kind.variant_tokens()
11295 };
11296 let max_length = optional_u32(attrs.max_length);
11297 let min = optional_i64(attrs.min);
11298 let max = optional_i64(attrs.max);
11299 let default = optional_str(attrs.default.as_deref());
11300
11301 let unique = attrs.unique;
11302 let generated_as = optional_str(attrs.generated_as.as_deref());
11303 let help_text = optional_str(attrs.help_text.as_deref());
11304 let choices = optional_choices(attrs.choices.as_deref());
11305 let db_comment = optional_str(attrs.db_comment.as_deref());
11306 let verbose_name = optional_str(attrs.verbose_name.as_deref());
11307 let editable = attrs.editable;
11308 let blank = attrs.blank;
11309 let case_insensitive = attrs.case_insensitive;
11310 let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11311 if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() {
11312 return Err(syn::Error::new_spanned(
11313 field,
11314 "`#[rustango(on_delete = \"…\")]` requires either `fk = \"<table>\"` \
11315 or `o2o = \"<table>\"` on the same field — it has no meaning on a \
11316 non-FK column.",
11317 ));
11318 }
11319 let fk_on_delete = match attrs.on_delete.as_deref() {
11320 None => quote!(::core::option::Option::None),
11321 Some(action) => {
11322 let variant = match action {
11323 "cascade" => quote!(Cascade),
11324 "restrict" => quote!(Restrict),
11325 "set_null" => quote!(SetNull),
11326 "set_default" => quote!(SetDefault),
11327 "no_action" => quote!(NoAction),
11328 // parse_field_attrs already validated this — guard against future drift.
11329 other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11330 };
11331 quote!(::core::option::Option::Some(
11332 #root::core::OnDeleteAction::#variant
11333 ))
11334 }
11335 };
11336 let schema = quote! {
11337 #root::core::FieldSchema {
11338 name: #name,
11339 column: #column_lit,
11340 ty: #field_type_tokens,
11341 nullable: #nullable,
11342 primary_key: #primary_key,
11343 relation: #relation,
11344 max_length: #max_length,
11345 min: #min,
11346 max: #max,
11347 default: #default,
11348 auto: #auto,
11349 unique: #unique,
11350 generated_as: #generated_as,
11351 help_text: #help_text,
11352 choices: #choices,
11353 db_comment: #db_comment,
11354 verbose_name: #verbose_name,
11355 editable: #editable,
11356 blank: #blank,
11357 case_insensitive: #case_insensitive,
11358 fk_on_delete: #fk_on_delete,
11359 validators: &[ #(#validators_lits),* ],
11360 }
11361 };
11362
11363 let from_row_init = quote! {
11364 #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11365 };
11366 let from_aliased_row_init = quote! {
11367 #ident: #root::sql::sqlx::Row::try_get(
11368 row,
11369 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11370 )?
11371 };
11372
11373 Ok(FieldInfo {
11374 ident,
11375 column,
11376 primary_key,
11377 auto,
11378 value_ty: &field.ty,
11379 field_type_tokens,
11380 schema,
11381 from_row_init,
11382 from_aliased_row_init,
11383 fk_inner: fk_inner.cloned(),
11384 fk_pk_kind: kind,
11385 nullable,
11386 auto_now: attrs.auto_now,
11387 auto_now_add: attrs.auto_now_add,
11388 soft_delete: attrs.soft_delete,
11389 generated_as: attrs.generated_as.clone(),
11390 default_uuid_v7: attrs.default_uuid_v7,
11391 related_name: attrs.related_name.clone(),
11392 })
11393}
11394
11395fn check_bound_compatibility(
11396 field: &syn::Field,
11397 attrs: &FieldAttrs,
11398 kind: DetectedKind,
11399) -> syn::Result<()> {
11400 if attrs.max_length.is_some() && kind != DetectedKind::String {
11401 return Err(syn::Error::new_spanned(
11402 field,
11403 "`max_length` is only valid on `String` fields (or `Option<String>`)",
11404 ));
11405 }
11406 if attrs.choices.is_some() && kind != DetectedKind::String {
11407 return Err(syn::Error::new_spanned(
11408 field,
11409 "`choices` is only valid on `String` fields (or `Option<String>`) — \
11410 integer-valued enumerations should be modeled with a Rust enum and \
11411 custom (de)serializer for now",
11412 ));
11413 }
11414 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11415 return Err(syn::Error::new_spanned(
11416 field,
11417 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11418 ));
11419 }
11420 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11421 if min > max {
11422 return Err(syn::Error::new_spanned(
11423 field,
11424 format!("`min` ({min}) is greater than `max` ({max})"),
11425 ));
11426 }
11427 }
11428 Ok(())
11429}
11430
11431fn optional_u32(value: Option<u32>) -> TokenStream2 {
11432 if let Some(v) = value {
11433 quote!(::core::option::Option::Some(#v))
11434 } else {
11435 quote!(::core::option::Option::None)
11436 }
11437}
11438
11439fn optional_i64(value: Option<i64>) -> TokenStream2 {
11440 if let Some(v) = value {
11441 quote!(::core::option::Option::Some(#v))
11442 } else {
11443 quote!(::core::option::Option::None)
11444 }
11445}
11446
11447fn optional_str(value: Option<&str>) -> TokenStream2 {
11448 if let Some(v) = value {
11449 quote!(::core::option::Option::Some(#v))
11450 } else {
11451 quote!(::core::option::Option::None)
11452 }
11453}
11454
11455fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11456 let Some(pairs) = pairs else {
11457 return quote!(::core::option::Option::None);
11458 };
11459 let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11460 quote!(::core::option::Option::Some(&[#(#entries),*]))
11461}
11462
11463fn relation_tokens(
11464 field: &syn::Field,
11465 attrs: &FieldAttrs,
11466 fk_inner: Option<&syn::Type>,
11467 table: &str,
11468) -> syn::Result<TokenStream2> {
11469 let root = rustango_root();
11470 if let Some(inner) = fk_inner {
11471 if attrs.fk.is_some() || attrs.o2o.is_some() {
11472 return Err(syn::Error::new_spanned(
11473 field,
11474 "`ForeignKey<T>` already declares the FK target via the type parameter — \
11475 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11476 ));
11477 }
11478 let on = attrs.on.as_deref().unwrap_or("id");
11479 return Ok(quote! {
11480 ::core::option::Option::Some(#root::core::Relation::Fk {
11481 to: <#inner as #root::core::Model>::SCHEMA.table,
11482 on: #on,
11483 })
11484 });
11485 }
11486 match (&attrs.fk, &attrs.o2o) {
11487 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11488 field,
11489 "`fk` and `o2o` are mutually exclusive",
11490 )),
11491 (Some(to), None) => {
11492 let on = attrs.on.as_deref().unwrap_or("id");
11493 // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11494 // the model's own table. Threaded as a literal string at
11495 // macro-expansion time to sidestep the const-eval cycle
11496 // that `Self::SCHEMA.table` would create when referenced
11497 // inside Self::SCHEMA's own initializer.
11498 let resolved = if to == "self" { table } else { to };
11499 Ok(quote! {
11500 ::core::option::Option::Some(#root::core::Relation::Fk { to: #resolved, on: #on })
11501 })
11502 }
11503 (None, Some(to)) => {
11504 let on = attrs.on.as_deref().unwrap_or("id");
11505 let resolved = if to == "self" { table } else { to };
11506 Ok(quote! {
11507 ::core::option::Option::Some(#root::core::Relation::O2O { to: #resolved, on: #on })
11508 })
11509 }
11510 (None, None) => {
11511 if attrs.on.is_some() {
11512 return Err(syn::Error::new_spanned(
11513 field,
11514 "`on` requires `fk` or `o2o`",
11515 ));
11516 }
11517 Ok(quote!(::core::option::Option::None))
11518 }
11519 }
11520}
11521
11522/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11523/// about kinds without depending on `rustango-core` (which would require a
11524/// proc-macro/normal split it doesn't have today).
11525#[derive(Clone, Copy, PartialEq, Eq)]
11526enum DetectedKind {
11527 I16,
11528 I32,
11529 I64,
11530 F32,
11531 F64,
11532 Bool,
11533 String,
11534 DateTime,
11535 Date,
11536 Time,
11537 Uuid,
11538 Json,
11539 Decimal,
11540 Binary,
11541 /// `Array<String>` → PG `text[]` (#341).
11542 ArrayText,
11543 /// `Array<i32>` → PG `integer[]` (#341).
11544 ArrayInt,
11545 /// `Array<i64>` → PG `bigint[]` (#341).
11546 ArrayBigInt,
11547 /// `Range<i32>` → PG `int4range` (#343).
11548 RangeInt,
11549 /// `Range<i64>` → PG `int8range` (#343).
11550 RangeBigInt,
11551 /// `Range<Decimal>` → PG `numrange` (#343).
11552 RangeNumeric,
11553 /// `Range<NaiveDate>` → PG `daterange` (#343).
11554 RangeDate,
11555 /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11556 RangeDateTime,
11557 /// `HStore` → PG `hstore` (#342).
11558 HStore,
11559 /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11560 /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11561 /// in at the `FieldType` emission site (not carried on this enum).
11562 Vector,
11563 /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11564 /// from the `#[rustango(geometry(srid = N))]` field attribute,
11565 /// threaded in at the `FieldType` emission site.
11566 Geometry,
11567}
11568
11569impl DetectedKind {
11570 fn variant_tokens(self) -> TokenStream2 {
11571 let root = rustango_root();
11572 match self {
11573 Self::I16 => quote!(#root::core::FieldType::I16),
11574 Self::I32 => quote!(#root::core::FieldType::I32),
11575 Self::I64 => quote!(#root::core::FieldType::I64),
11576 Self::F32 => quote!(#root::core::FieldType::F32),
11577 Self::F64 => quote!(#root::core::FieldType::F64),
11578 Self::Bool => quote!(#root::core::FieldType::Bool),
11579 Self::String => quote!(#root::core::FieldType::String),
11580 Self::DateTime => quote!(#root::core::FieldType::DateTime),
11581 Self::Date => quote!(#root::core::FieldType::Date),
11582 Self::Time => quote!(#root::core::FieldType::Time),
11583 Self::Uuid => quote!(#root::core::FieldType::Uuid),
11584 Self::Json => quote!(#root::core::FieldType::Json),
11585 Self::Decimal => quote!(#root::core::FieldType::Decimal),
11586 Self::Binary => quote!(#root::core::FieldType::Binary),
11587 Self::ArrayText => {
11588 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11589 }
11590 Self::ArrayInt => {
11591 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11592 }
11593 Self::ArrayBigInt => {
11594 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11595 }
11596 Self::RangeInt => {
11597 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11598 }
11599 Self::RangeBigInt => {
11600 quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11601 }
11602 Self::RangeNumeric => {
11603 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11604 }
11605 Self::RangeDate => {
11606 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11607 }
11608 Self::RangeDateTime => {
11609 quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11610 }
11611 Self::HStore => quote!(#root::core::FieldType::HStore),
11612 // Dimension comes from the `vector(dims = N)` attribute,
11613 // applied at the emission site; `0` here is just a fallback.
11614 Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11615 // SRID comes from the `geometry(srid = N)` attribute, applied
11616 // at the emission site; `0` here is just a fallback.
11617 Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11618 }
11619 }
11620
11621 fn is_integer(self) -> bool {
11622 matches!(self, Self::I16 | Self::I32 | Self::I64)
11623 }
11624
11625 /// `(SqlValue::<Variant>, default expr)` for emitting the
11626 /// `match SqlValue { … }` arm in `LoadRelated::__rustango_load_related`
11627 /// for a `ForeignKey<T, K>` FK whose K maps to `self`. The default
11628 /// fires only when the parent's `__rustango_pk_value` returns a
11629 /// different variant than expected, which is a compile-time bug —
11630 /// but we still need a value-typed fallback to keep the match
11631 /// total.
11632 fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
11633 let root = rustango_root();
11634 match self {
11635 Self::I16 => (quote!(I16), quote!(0i16)),
11636 Self::I32 => (quote!(I32), quote!(0i32)),
11637 Self::I64 => (quote!(I64), quote!(0i64)),
11638 Self::F32 => (quote!(F32), quote!(0f32)),
11639 Self::F64 => (quote!(F64), quote!(0f64)),
11640 Self::Bool => (quote!(Bool), quote!(false)),
11641 Self::String => (quote!(String), quote!(::std::string::String::new())),
11642 Self::DateTime => (
11643 quote!(DateTime),
11644 quote!(<#root::__chrono::DateTime<#root::__chrono::Utc> as ::std::default::Default>::default()),
11645 ),
11646 Self::Date => (
11647 quote!(Date),
11648 quote!(<#root::__chrono::NaiveDate as ::std::default::Default>::default()),
11649 ),
11650 Self::Time => (
11651 quote!(Time),
11652 quote!(<#root::__chrono::NaiveTime as ::std::default::Default>::default()),
11653 ),
11654 Self::Uuid => (quote!(Uuid), quote!(#root::__uuid::Uuid::nil())),
11655 Self::Json => (quote!(Json), quote!(#root::__serde_json::Value::Null)),
11656 Self::Decimal => (
11657 quote!(Decimal),
11658 quote!(<#root::__rust_decimal::Decimal as ::std::default::Default>::default()),
11659 ),
11660 Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
11661 // Arrays (#341) can never be a foreign-key primary key, so
11662 // this arm is never reached at runtime — but the match must
11663 // stay total. A bare empty `SqlValue::Array` is the dummy.
11664 Self::ArrayText | Self::ArrayInt | Self::ArrayBigInt => {
11665 (quote!(Array), quote!(::std::vec::Vec::new()))
11666 }
11667 // Ranges (#343) likewise can't be a FK PK — never reached.
11668 Self::RangeInt
11669 | Self::RangeBigInt
11670 | Self::RangeNumeric
11671 | Self::RangeDate
11672 | Self::RangeDateTime => (quote!(RangeLiteral), quote!(::std::string::String::new())),
11673 // HStore (#342) can't be a FK PK — never reached.
11674 Self::HStore => (quote!(HStore), quote!(::std::vec::Vec::new())),
11675 // Vector (#824) can't be a FK PK — never reached.
11676 Self::Vector => (quote!(Vector), quote!(::std::vec::Vec::new())),
11677 // Geometry (#443) can't be a FK PK — never reached (arm is
11678 // exhaustiveness-only; never interpolated into emitted code).
11679 Self::Geometry => (quote!(Geometry), quote!(::std::vec::Vec::new())),
11680 }
11681 }
11682}
11683
11684/// Result of walking a field's Rust type. `kind` is the underlying
11685/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11686/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11687/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11688/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11689#[derive(Clone, Copy)]
11690struct DetectedType<'a> {
11691 kind: DetectedKind,
11692 nullable: bool,
11693 auto: bool,
11694 fk_inner: Option<&'a syn::Type>,
11695}
11696
11697/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11698/// non-`Auto` types — the caller should already have routed Auto-only
11699/// codegen through this helper, so a `None` indicates a macro-internal
11700/// invariant break.
11701fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11702 let Type::Path(TypePath { path, qself: None }) = ty else {
11703 return None;
11704 };
11705 let last = path.segments.last()?;
11706 if last.ident != "Auto" {
11707 return None;
11708 }
11709 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11710 return None;
11711 };
11712 args.args.iter().find_map(|a| match a {
11713 syn::GenericArgument::Type(t) => Some(t),
11714 _ => None,
11715 })
11716}
11717
11718fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11719 let Type::Path(TypePath { path, qself: None }) = ty else {
11720 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11721 };
11722 let last = path
11723 .segments
11724 .last()
11725 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11726
11727 if last.ident == "Option" {
11728 let inner = generic_inner(ty, &last.arguments, "Option")?;
11729 let inner_det = detect_type(inner)?;
11730 if inner_det.nullable {
11731 return Err(syn::Error::new_spanned(
11732 ty,
11733 "nested Option is not supported",
11734 ));
11735 }
11736 if inner_det.auto {
11737 return Err(syn::Error::new_spanned(
11738 ty,
11739 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11740 ));
11741 }
11742 return Ok(DetectedType {
11743 nullable: true,
11744 ..inner_det
11745 });
11746 }
11747
11748 if last.ident == "Auto" {
11749 let inner = generic_inner(ty, &last.arguments, "Auto")?;
11750 let inner_det = detect_type(inner)?;
11751 if inner_det.auto {
11752 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11753 }
11754 if inner_det.nullable {
11755 return Err(syn::Error::new_spanned(
11756 ty,
11757 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11758 ));
11759 }
11760 if inner_det.fk_inner.is_some() {
11761 return Err(syn::Error::new_spanned(
11762 ty,
11763 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11764 ));
11765 }
11766 if !matches!(
11767 inner_det.kind,
11768 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11769 ) {
11770 return Err(syn::Error::new_spanned(
11771 ty,
11772 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11773 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11774 (DEFAULT now())",
11775 ));
11776 }
11777 return Ok(DetectedType {
11778 auto: true,
11779 ..inner_det
11780 });
11781 }
11782
11783 if last.ident == "ForeignKey" {
11784 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11785 // Resolve the FK column's underlying SQL type from `K`. When the
11786 // user wrote `ForeignKey<T>` without a key parameter, the type
11787 // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11788 // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11789 // recurse into K so the column DDL emits the right SQL type
11790 // (VARCHAR for String, UUID for Uuid, …) and the load_related
11791 // emitter knows which `SqlValue` variant to match.
11792 let kind = match key_ty {
11793 Some(k) => detect_type(k)?.kind,
11794 None => DetectedKind::I64,
11795 };
11796 return Ok(DetectedType {
11797 kind,
11798 nullable: false,
11799 auto: false,
11800 fk_inner: Some(inner),
11801 });
11802 }
11803
11804 let kind = match last.ident.to_string().as_str() {
11805 "i16" => DetectedKind::I16,
11806 "i32" => DetectedKind::I32,
11807 "i64" => DetectedKind::I64,
11808 "f32" => DetectedKind::F32,
11809 "f64" => DetectedKind::F64,
11810 "bool" => DetectedKind::Bool,
11811 "String" => DetectedKind::String,
11812 "DateTime" => DetectedKind::DateTime,
11813 "NaiveDate" => DetectedKind::Date,
11814 "NaiveTime" => DetectedKind::Time,
11815 "Uuid" => DetectedKind::Uuid,
11816 "Value" => DetectedKind::Json,
11817 "Decimal" => DetectedKind::Decimal,
11818 // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11819 // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11820 // — that would emit Binary DDL and decode-fail at runtime.
11821 "Vec" => {
11822 let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11823 if let Type::Path(TypePath { path, qself: None }) = inner {
11824 if let Some(seg) = path.segments.last() {
11825 if seg.ident == "u8" && seg.arguments.is_empty() {
11826 return Ok(DetectedType {
11827 kind: DetectedKind::Binary,
11828 nullable: false,
11829 auto: false,
11830 fk_inner: None,
11831 });
11832 }
11833 }
11834 }
11835 return Err(syn::Error::new_spanned(
11836 ty,
11837 "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11838 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11839 ));
11840 }
11841 // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11842 // `integer[]` / `bigint[]` (#341).
11843 "Array" => {
11844 let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11845 let elem = match inner {
11846 Type::Path(TypePath { path, qself: None }) => {
11847 path.segments.last().map(|s| s.ident.to_string())
11848 }
11849 _ => None,
11850 };
11851 let kind = match elem.as_deref() {
11852 Some("String") => DetectedKind::ArrayText,
11853 Some("i32") => DetectedKind::ArrayInt,
11854 Some("i64") => DetectedKind::ArrayBigInt,
11855 _ => {
11856 return Err(syn::Error::new_spanned(
11857 ty,
11858 "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
11859 `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
11860 ));
11861 }
11862 };
11863 return Ok(DetectedType {
11864 kind,
11865 nullable: false,
11866 auto: false,
11867 fk_inner: None,
11868 });
11869 }
11870 // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
11871 // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
11872 // `int8range` / `numrange` / `daterange` / `tstzrange` (#343).
11873 "Range" => {
11874 let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
11875 let elem = match inner {
11876 Type::Path(TypePath { path, qself: None }) => {
11877 path.segments.last().map(|s| s.ident.to_string())
11878 }
11879 _ => None,
11880 };
11881 let kind = match elem.as_deref() {
11882 Some("i32") => DetectedKind::RangeInt,
11883 Some("i64") => DetectedKind::RangeBigInt,
11884 Some("Decimal") => DetectedKind::RangeNumeric,
11885 Some("NaiveDate") => DetectedKind::RangeDate,
11886 Some("DateTime") => DetectedKind::RangeDateTime,
11887 _ => {
11888 return Err(syn::Error::new_spanned(
11889 ty,
11890 "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
11891 `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
11892 `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
11893 (→ tstzrange) are supported (#343)",
11894 ));
11895 }
11896 };
11897 return Ok(DetectedType {
11898 kind,
11899 nullable: false,
11900 auto: false,
11901 fk_inner: None,
11902 });
11903 }
11904 // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
11905 // (the `CastValue` impl bridges logical↔stored); the field's own
11906 // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
11907 // schema just needs `FieldType::String`.
11908 "Cast" => {
11909 return Ok(DetectedType {
11910 kind: DetectedKind::String,
11911 nullable: false,
11912 auto: false,
11913 fk_inner: None,
11914 });
11915 }
11916 // `HStore` → PG `hstore` (#342). No generic
11917 // parameter — always a string→string map.
11918 "HStore" => {
11919 return Ok(DetectedType {
11920 kind: DetectedKind::HStore,
11921 nullable: false,
11922 auto: false,
11923 fk_inner: None,
11924 });
11925 }
11926 // `Vector` → pgvector `vector(N)` (#824). The dimension is
11927 // supplied by `#[rustango(vector(dims = N))]`, not the type.
11928 "Vector" => {
11929 return Ok(DetectedType {
11930 kind: DetectedKind::Vector,
11931 nullable: false,
11932 auto: false,
11933 fk_inner: None,
11934 });
11935 }
11936 // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
11937 // supplied by `#[rustango(geometry(srid = N))]`, not the type.
11938 "Point" => {
11939 return Ok(DetectedType {
11940 kind: DetectedKind::Geometry,
11941 nullable: false,
11942 auto: false,
11943 fk_inner: None,
11944 });
11945 }
11946 other => {
11947 return Err(syn::Error::new_spanned(
11948 ty,
11949 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)"),
11950 ));
11951 }
11952 };
11953 Ok(DetectedType {
11954 kind,
11955 nullable: false,
11956 auto: false,
11957 fk_inner: None,
11958 })
11959}
11960
11961fn generic_inner<'a>(
11962 ty: &'a Type,
11963 arguments: &'a PathArguments,
11964 wrapper: &str,
11965) -> syn::Result<&'a Type> {
11966 let PathArguments::AngleBracketed(args) = arguments else {
11967 return Err(syn::Error::new_spanned(
11968 ty,
11969 format!("{wrapper} requires a generic argument"),
11970 ));
11971 };
11972 args.args
11973 .iter()
11974 .find_map(|a| match a {
11975 GenericArgument::Type(t) => Some(t),
11976 _ => None,
11977 })
11978 .ok_or_else(|| {
11979 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11980 })
11981}
11982
11983/// Like [`generic_inner`] but pulls *two* type args — the first is
11984/// required, the second is optional. Used by the `ForeignKey<T, K>`
11985/// detection where K defaults to `i64` when omitted.
11986fn generic_pair<'a>(
11987 ty: &'a Type,
11988 arguments: &'a PathArguments,
11989 wrapper: &str,
11990) -> syn::Result<(&'a Type, Option<&'a Type>)> {
11991 let PathArguments::AngleBracketed(args) = arguments else {
11992 return Err(syn::Error::new_spanned(
11993 ty,
11994 format!("{wrapper} requires a generic argument"),
11995 ));
11996 };
11997 let mut types = args.args.iter().filter_map(|a| match a {
11998 GenericArgument::Type(t) => Some(t),
11999 _ => None,
12000 });
12001 let first = types.next().ok_or_else(|| {
12002 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12003 })?;
12004 let second = types.next();
12005 Ok((first, second))
12006}
12007
12008fn to_snake_case(s: &str) -> String {
12009 let mut out = String::with_capacity(s.len() + 4);
12010 for (i, ch) in s.chars().enumerate() {
12011 if ch.is_ascii_uppercase() {
12012 if i > 0 {
12013 out.push('_');
12014 }
12015 out.push(ch.to_ascii_lowercase());
12016 } else {
12017 out.push(ch);
12018 }
12019 }
12020 out
12021}
12022
12023// ============================================================
12024// #[derive(Form)] — slice 8.4B
12025// ============================================================
12026
12027/// Per-field `#[form(...)]` attributes recognised by the derive.
12028#[derive(Default)]
12029struct FormFieldAttrs {
12030 min: Option<i64>,
12031 max: Option<i64>,
12032 min_length: Option<u32>,
12033 max_length: Option<u32>,
12034 /// `#[form(clean = "fn_name")]` — per-field clean hook.
12035 /// The named static method on the form struct is called after the
12036 /// field's typed parse + length/range checks; it gets the parsed
12037 /// value by reference and returns `Result<<FieldType>, String>`.
12038 /// On Ok, the returned value replaces the parsed one; on Err, the
12039 /// message is attached to the field error list. Issue #372.
12040 clean: Option<syn::Ident>,
12041}
12042
12043/// Container-level `#[form(...)]` attributes. Currently only the
12044/// cross-field `validate` hook (issue #373).
12045#[derive(Default)]
12046struct FormContainerAttrs {
12047 /// `#[form(validate = "fn_name")]` — whole-form clean hook.
12048 /// After every per-field parse succeeds, the named method on the
12049 /// form struct is called with `&self` and may return
12050 /// `Result<(), FormErrors>`. Errors merge into the field error
12051 /// list. Issue #373.
12052 validate: Option<syn::Ident>,
12053}
12054
12055/// Detected shape of a form field's Rust type.
12056#[derive(Clone, Copy)]
12057enum FormFieldKind {
12058 String,
12059 I16,
12060 I32,
12061 I64,
12062 F32,
12063 F64,
12064 Bool,
12065}
12066
12067impl FormFieldKind {
12068 fn parse_method(self) -> &'static str {
12069 match self {
12070 Self::I16 => "i16",
12071 Self::I32 => "i32",
12072 Self::I64 => "i64",
12073 Self::F32 => "f32",
12074 Self::F64 => "f64",
12075 // String + Bool don't go through `str::parse`; the codegen
12076 // handles them inline.
12077 Self::String | Self::Bool => "",
12078 }
12079 }
12080}
12081
12082fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
12083 let root = rustango_root();
12084 let struct_name = &input.ident;
12085
12086 let Data::Struct(data) = &input.data else {
12087 return Err(syn::Error::new_spanned(
12088 struct_name,
12089 "Form can only be derived on structs",
12090 ));
12091 };
12092 let Fields::Named(named) = &data.fields else {
12093 return Err(syn::Error::new_spanned(
12094 struct_name,
12095 "Form requires a struct with named fields",
12096 ));
12097 };
12098
12099 // #373 — container-level `#[form(validate = "fn")]` hook.
12100 let container = parse_form_container_attrs(input)?;
12101 let post_field_clean: Vec<TokenStream2> = Vec::new();
12102 let _ = post_field_clean;
12103
12104 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12105 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12106
12107 for field in &named.named {
12108 let ident = field
12109 .ident
12110 .as_ref()
12111 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12112 let attrs = parse_form_field_attrs(field)?;
12113 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12114
12115 let name_lit = ident.to_string();
12116 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12117 // #372 — append the per-field `clean_<field>` call right after
12118 // the parse block when the attribute is set. The clean fn
12119 // takes &T and returns Result<T, String>; on Err we attach
12120 // the message to the field error list without aborting, so
12121 // the caller sees every field error at once.
12122 let clean_block = if let Some(clean_fn) = &attrs.clean {
12123 quote! {
12124 if __errors.fields().get(#name_lit).is_none() {
12125 match Self::#clean_fn(&#ident) {
12126 ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
12127 ::core::result::Result::Err(__msg) => {
12128 __errors.add(#name_lit, __msg);
12129 }
12130 }
12131 }
12132 }
12133 } else {
12134 quote! {}
12135 };
12136 field_blocks.push(quote! {
12137 #parse_block
12138 #clean_block
12139 });
12140 field_idents.push(ident);
12141 }
12142
12143 // #373 — after every per-field parse + clean succeeds, call the
12144 // cross-field validator if declared. Errors merge into the
12145 // outgoing FormErrors via the existing `FormErrors::merge` helper
12146 // (same primitive the serializer cross-field hook uses).
12147 let cross_field_call = if let Some(validate_fn) = &container.validate {
12148 quote! {
12149 if __errors.is_empty() {
12150 let __candidate = Self { #( #field_idents ),* };
12151 if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12152 __errors.merge(__other);
12153 }
12154 if !__errors.is_empty() {
12155 return ::core::result::Result::Err(__errors);
12156 }
12157 return ::core::result::Result::Ok(__candidate);
12158 }
12159 }
12160 } else {
12161 quote! {}
12162 };
12163
12164 Ok(quote! {
12165 impl #root::forms::Form for #struct_name {
12166 fn parse(
12167 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12168 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12169 let mut __errors = #root::forms::FormErrors::default();
12170 #( #field_blocks )*
12171 #cross_field_call
12172 if !__errors.is_empty() {
12173 return ::core::result::Result::Err(__errors);
12174 }
12175 ::core::result::Result::Ok(Self {
12176 #( #field_idents ),*
12177 })
12178 }
12179 }
12180 })
12181}
12182
12183fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12184 let mut out = FormContainerAttrs::default();
12185 for attr in &input.attrs {
12186 if !attr.path().is_ident("form") {
12187 continue;
12188 }
12189 attr.parse_nested_meta(|meta| {
12190 if meta.path.is_ident("validate") {
12191 let s: LitStr = meta.value()?.parse()?;
12192 out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12193 return Ok(());
12194 }
12195 Err(meta.error("unknown form container attribute (supported: `validate`)"))
12196 })?;
12197 }
12198 Ok(out)
12199}
12200
12201fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12202 let mut out = FormFieldAttrs::default();
12203 for attr in &field.attrs {
12204 if !attr.path().is_ident("form") {
12205 continue;
12206 }
12207 attr.parse_nested_meta(|meta| {
12208 if meta.path.is_ident("min") {
12209 let lit: syn::LitInt = meta.value()?.parse()?;
12210 out.min = Some(lit.base10_parse::<i64>()?);
12211 return Ok(());
12212 }
12213 if meta.path.is_ident("max") {
12214 let lit: syn::LitInt = meta.value()?.parse()?;
12215 out.max = Some(lit.base10_parse::<i64>()?);
12216 return Ok(());
12217 }
12218 if meta.path.is_ident("min_length") {
12219 let lit: syn::LitInt = meta.value()?.parse()?;
12220 out.min_length = Some(lit.base10_parse::<u32>()?);
12221 return Ok(());
12222 }
12223 if meta.path.is_ident("max_length") {
12224 let lit: syn::LitInt = meta.value()?.parse()?;
12225 out.max_length = Some(lit.base10_parse::<u32>()?);
12226 return Ok(());
12227 }
12228 if meta.path.is_ident("clean") {
12229 let s: LitStr = meta.value()?.parse()?;
12230 out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12231 return Ok(());
12232 }
12233 Err(meta.error(
12234 "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12235 ))
12236 })?;
12237 }
12238 Ok(out)
12239}
12240
12241fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12242 let Type::Path(TypePath { path, qself: None }) = ty else {
12243 return Err(syn::Error::new(
12244 span,
12245 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12246 ));
12247 };
12248 let last = path
12249 .segments
12250 .last()
12251 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12252
12253 if last.ident == "Option" {
12254 let inner = generic_inner(ty, &last.arguments, "Option")?;
12255 let (kind, nested) = detect_form_field(inner, span)?;
12256 if nested {
12257 return Err(syn::Error::new(
12258 span,
12259 "nested Option in Form fields is not supported",
12260 ));
12261 }
12262 return Ok((kind, true));
12263 }
12264
12265 let kind = match last.ident.to_string().as_str() {
12266 "String" => FormFieldKind::String,
12267 "i16" => FormFieldKind::I16,
12268 "i32" => FormFieldKind::I32,
12269 "i64" => FormFieldKind::I64,
12270 "f32" => FormFieldKind::F32,
12271 "f64" => FormFieldKind::F64,
12272 "bool" => FormFieldKind::Bool,
12273 other => {
12274 return Err(syn::Error::new(
12275 span,
12276 format!(
12277 "Form field type `{other}` is not supported in v0.8 — use String / \
12278 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12279 ),
12280 ));
12281 }
12282 };
12283 Ok((kind, false))
12284}
12285
12286#[allow(clippy::too_many_lines)]
12287fn render_form_field_parse(
12288 ident: &syn::Ident,
12289 name_lit: &str,
12290 kind: FormFieldKind,
12291 nullable: bool,
12292 attrs: &FormFieldAttrs,
12293) -> TokenStream2 {
12294 // Pull the raw &str from the payload. Uses variable name `data` to
12295 // match the new `Form::parse(data: &HashMap<…>)` signature.
12296 let lookup = quote! {
12297 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12298 };
12299
12300 let parsed_value = match kind {
12301 FormFieldKind::Bool => quote! {
12302 let __v: bool = match __raw {
12303 ::core::option::Option::None => false,
12304 ::core::option::Option::Some(__s) => !matches!(
12305 __s.to_ascii_lowercase().as_str(),
12306 "" | "false" | "0" | "off" | "no"
12307 ),
12308 };
12309 },
12310 FormFieldKind::String => {
12311 if nullable {
12312 quote! {
12313 let __v: ::core::option::Option<::std::string::String> = match __raw {
12314 ::core::option::Option::None => ::core::option::Option::None,
12315 ::core::option::Option::Some(__s) if __s.is_empty() => {
12316 ::core::option::Option::None
12317 }
12318 ::core::option::Option::Some(__s) => {
12319 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12320 }
12321 };
12322 }
12323 } else {
12324 quote! {
12325 let __v: ::std::string::String = match __raw {
12326 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12327 ::core::clone::Clone::clone(__s)
12328 }
12329 _ => {
12330 __errors.add(#name_lit, "This field is required.");
12331 ::std::string::String::new()
12332 }
12333 };
12334 }
12335 }
12336 }
12337 FormFieldKind::I16
12338 | FormFieldKind::I32
12339 | FormFieldKind::I64
12340 | FormFieldKind::F32
12341 | FormFieldKind::F64 => {
12342 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12343 let ty_lit = kind.parse_method();
12344 let default_val = match kind {
12345 FormFieldKind::I16 => quote! { 0i16 },
12346 FormFieldKind::I32 => quote! { 0i32 },
12347 FormFieldKind::I64 => quote! { 0i64 },
12348 FormFieldKind::F32 => quote! { 0f32 },
12349 FormFieldKind::F64 => quote! { 0f64 },
12350 _ => quote! { Default::default() },
12351 };
12352 if nullable {
12353 quote! {
12354 let __v: ::core::option::Option<#parse_ty> = match __raw {
12355 ::core::option::Option::None => ::core::option::Option::None,
12356 ::core::option::Option::Some(__s) if __s.is_empty() => {
12357 ::core::option::Option::None
12358 }
12359 ::core::option::Option::Some(__s) => {
12360 match __s.parse::<#parse_ty>() {
12361 ::core::result::Result::Ok(__n) => {
12362 ::core::option::Option::Some(__n)
12363 }
12364 ::core::result::Result::Err(__e) => {
12365 __errors.add(
12366 #name_lit,
12367 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12368 );
12369 ::core::option::Option::None
12370 }
12371 }
12372 }
12373 };
12374 }
12375 } else {
12376 quote! {
12377 let __v: #parse_ty = match __raw {
12378 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12379 match __s.parse::<#parse_ty>() {
12380 ::core::result::Result::Ok(__n) => __n,
12381 ::core::result::Result::Err(__e) => {
12382 __errors.add(
12383 #name_lit,
12384 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12385 );
12386 #default_val
12387 }
12388 }
12389 }
12390 _ => {
12391 __errors.add(#name_lit, "This field is required.");
12392 #default_val
12393 }
12394 };
12395 }
12396 }
12397 }
12398 };
12399
12400 let validators = render_form_validators(name_lit, kind, nullable, attrs);
12401
12402 quote! {
12403 // `mut` so the per-field `clean` hook (#372) can rewrite the
12404 // parsed value in-place when it returns Ok with a normalized
12405 // form (e.g. trim / lowercase).
12406 let mut #ident = {
12407 #lookup
12408 #parsed_value
12409 #validators
12410 __v
12411 };
12412 }
12413}
12414
12415fn render_form_validators(
12416 name_lit: &str,
12417 kind: FormFieldKind,
12418 nullable: bool,
12419 attrs: &FormFieldAttrs,
12420) -> TokenStream2 {
12421 let mut checks: Vec<TokenStream2> = Vec::new();
12422
12423 let val_ref = if nullable {
12424 quote! { __v.as_ref() }
12425 } else {
12426 quote! { ::core::option::Option::Some(&__v) }
12427 };
12428
12429 let is_string = matches!(kind, FormFieldKind::String);
12430 let is_numeric = matches!(
12431 kind,
12432 FormFieldKind::I16
12433 | FormFieldKind::I32
12434 | FormFieldKind::I64
12435 | FormFieldKind::F32
12436 | FormFieldKind::F64
12437 );
12438
12439 if is_string {
12440 if let Some(min_len) = attrs.min_length {
12441 let min_len_usize = min_len as usize;
12442 checks.push(quote! {
12443 if let ::core::option::Option::Some(__s) = #val_ref {
12444 if __s.len() < #min_len_usize {
12445 __errors.add(
12446 #name_lit,
12447 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12448 );
12449 }
12450 }
12451 });
12452 }
12453 if let Some(max_len) = attrs.max_length {
12454 let max_len_usize = max_len as usize;
12455 checks.push(quote! {
12456 if let ::core::option::Option::Some(__s) = #val_ref {
12457 if __s.len() > #max_len_usize {
12458 __errors.add(
12459 #name_lit,
12460 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12461 );
12462 }
12463 }
12464 });
12465 }
12466 }
12467
12468 if is_numeric {
12469 if let Some(min) = attrs.min {
12470 checks.push(quote! {
12471 if let ::core::option::Option::Some(__n) = #val_ref {
12472 if (*__n as f64) < (#min as f64) {
12473 __errors.add(
12474 #name_lit,
12475 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12476 );
12477 }
12478 }
12479 });
12480 }
12481 if let Some(max) = attrs.max {
12482 checks.push(quote! {
12483 if let ::core::option::Option::Some(__n) = #val_ref {
12484 if (*__n as f64) > (#max as f64) {
12485 __errors.add(
12486 #name_lit,
12487 ::std::format!("Ensure this value is less than or equal to {}.", #max),
12488 );
12489 }
12490 }
12491 });
12492 }
12493 }
12494
12495 quote! { #( #checks )* }
12496}
12497
12498// ============================================================
12499// #[derive(ViewSet)]
12500// ============================================================
12501
12502struct ViewSetAttrs {
12503 model: syn::Path,
12504 fields: Option<Vec<String>>,
12505 filter_fields: Vec<String>,
12506 search_fields: Vec<String>,
12507 /// (field_name, desc)
12508 ordering: Vec<(String, bool)>,
12509 page_size: Option<usize>,
12510 read_only: bool,
12511 perms: ViewSetPermsAttrs,
12512 /// `#[viewset(serializer = SomeSerializer)]` — render list /
12513 /// retrieve / create responses through this `#[derive(Serializer)]`
12514 /// type instead of the default field-level projection (requires the
12515 /// `serializer` feature). Tri-dialect.
12516 serializer: Option<syn::Path>,
12517}
12518
12519#[derive(Default)]
12520struct ViewSetPermsAttrs {
12521 list: Vec<String>,
12522 retrieve: Vec<String>,
12523 create: Vec<String>,
12524 update: Vec<String>,
12525 destroy: Vec<String>,
12526}
12527
12528fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12529 let root = rustango_root();
12530 let struct_name = &input.ident;
12531
12532 // Must be a unit struct or an empty named struct.
12533 match &input.data {
12534 Data::Struct(s) => match &s.fields {
12535 Fields::Unit | Fields::Named(_) => {}
12536 Fields::Unnamed(_) => {
12537 return Err(syn::Error::new_spanned(
12538 struct_name,
12539 "ViewSet can only be derived on a unit struct or an empty named struct",
12540 ));
12541 }
12542 },
12543 _ => {
12544 return Err(syn::Error::new_spanned(
12545 struct_name,
12546 "ViewSet can only be derived on a struct",
12547 ));
12548 }
12549 }
12550
12551 let attrs = parse_viewset_attrs(input)?;
12552 let model_path = &attrs.model;
12553
12554 // `.fields(&[...])` call — None means skip (use all scalar fields).
12555 let fields_call = if let Some(ref fields) = attrs.fields {
12556 let lits = fields.iter().map(|f| f.as_str());
12557 quote!(.fields(&[ #(#lits),* ]))
12558 } else {
12559 quote!()
12560 };
12561
12562 let filter_fields_call = if attrs.filter_fields.is_empty() {
12563 quote!()
12564 } else {
12565 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12566 quote!(.filter_fields(&[ #(#lits),* ]))
12567 };
12568
12569 let search_fields_call = if attrs.search_fields.is_empty() {
12570 quote!()
12571 } else {
12572 let lits = attrs.search_fields.iter().map(|f| f.as_str());
12573 quote!(.search_fields(&[ #(#lits),* ]))
12574 };
12575
12576 let ordering_call = if attrs.ordering.is_empty() {
12577 quote!()
12578 } else {
12579 let pairs = attrs.ordering.iter().map(|(f, desc)| {
12580 let f = f.as_str();
12581 quote!((#f, #desc))
12582 });
12583 quote!(.ordering(&[ #(#pairs),* ]))
12584 };
12585
12586 let page_size_call = if let Some(n) = attrs.page_size {
12587 quote!(.page_size(#n))
12588 } else {
12589 quote!()
12590 };
12591
12592 let read_only_call = if attrs.read_only {
12593 quote!(.read_only())
12594 } else {
12595 quote!()
12596 };
12597
12598 // `.serializer::<S>()` — reshape responses through a derived
12599 // serializer. Requires the downstream crate to enable the
12600 // `serializer` feature (the method is gated on it).
12601 let serializer_call = if let Some(ref ser) = attrs.serializer {
12602 quote!(.serializer::<#ser>())
12603 } else {
12604 quote!()
12605 };
12606
12607 let perms = &attrs.perms;
12608 let perms_call = if perms.list.is_empty()
12609 && perms.retrieve.is_empty()
12610 && perms.create.is_empty()
12611 && perms.update.is_empty()
12612 && perms.destroy.is_empty()
12613 {
12614 quote!()
12615 } else {
12616 let list_lits = perms.list.iter().map(|s| s.as_str());
12617 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12618 let create_lits = perms.create.iter().map(|s| s.as_str());
12619 let update_lits = perms.update.iter().map(|s| s.as_str());
12620 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12621 quote! {
12622 .permissions(#root::viewset::ViewSetPerms {
12623 list: ::std::vec![ #(#list_lits.to_owned()),* ],
12624 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12625 create: ::std::vec![ #(#create_lits.to_owned()),* ],
12626 update: ::std::vec![ #(#update_lits.to_owned()),* ],
12627 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
12628 })
12629 }
12630 };
12631
12632 Ok(quote! {
12633 impl #struct_name {
12634 /// Build an `axum::Router` with the six standard REST endpoints
12635 /// for this ViewSet, mounted at `prefix`.
12636 ///
12637 /// Accepts any backend's pool — a `sqlx::PgPool`,
12638 /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
12639 /// enum — because they all `Into<Pool>` (#1273). This is why
12640 /// the derive no longer names `PgPool`, which would not
12641 /// exist on a project built without the `postgres` feature.
12642 pub fn router(
12643 prefix: &str,
12644 pool: impl ::core::convert::Into<#root::sql::Pool>,
12645 ) -> #root::__axum::Router {
12646 #root::viewset::ViewSet::for_model(
12647 <#model_path as #root::core::Model>::SCHEMA
12648 )
12649 #fields_call
12650 #filter_fields_call
12651 #search_fields_call
12652 #ordering_call
12653 #page_size_call
12654 #perms_call
12655 #read_only_call
12656 #serializer_call
12657 .router_pool(prefix, pool.into())
12658 }
12659 }
12660 })
12661}
12662
12663fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
12664 let mut model: Option<syn::Path> = None;
12665 let mut fields: Option<Vec<String>> = None;
12666 let mut filter_fields: Vec<String> = Vec::new();
12667 let mut search_fields: Vec<String> = Vec::new();
12668 let mut ordering: Vec<(String, bool)> = Vec::new();
12669 let mut page_size: Option<usize> = None;
12670 let mut read_only = false;
12671 let mut perms = ViewSetPermsAttrs::default();
12672 let mut serializer: Option<syn::Path> = None;
12673
12674 for attr in &input.attrs {
12675 if !attr.path().is_ident("viewset") {
12676 continue;
12677 }
12678 attr.parse_nested_meta(|meta| {
12679 if meta.path.is_ident("model") {
12680 let path: syn::Path = meta.value()?.parse()?;
12681 model = Some(path);
12682 return Ok(());
12683 }
12684 if meta.path.is_ident("serializer") {
12685 let path: syn::Path = meta.value()?.parse()?;
12686 serializer = Some(path);
12687 return Ok(());
12688 }
12689 if meta.path.is_ident("fields") {
12690 let s: LitStr = meta.value()?.parse()?;
12691 fields = Some(split_field_list(&s.value()));
12692 return Ok(());
12693 }
12694 if meta.path.is_ident("filter_fields") {
12695 let s: LitStr = meta.value()?.parse()?;
12696 filter_fields = split_field_list(&s.value());
12697 return Ok(());
12698 }
12699 if meta.path.is_ident("search_fields") {
12700 let s: LitStr = meta.value()?.parse()?;
12701 search_fields = split_field_list(&s.value());
12702 return Ok(());
12703 }
12704 if meta.path.is_ident("ordering") {
12705 let s: LitStr = meta.value()?.parse()?;
12706 ordering = parse_ordering_list(&s.value());
12707 return Ok(());
12708 }
12709 if meta.path.is_ident("page_size") {
12710 let lit: syn::LitInt = meta.value()?.parse()?;
12711 page_size = Some(lit.base10_parse::<usize>()?);
12712 return Ok(());
12713 }
12714 if meta.path.is_ident("read_only") {
12715 read_only = true;
12716 return Ok(());
12717 }
12718 if meta.path.is_ident("permissions") {
12719 meta.parse_nested_meta(|inner| {
12720 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
12721 let s: LitStr = inner.value()?.parse()?;
12722 Ok(split_field_list(&s.value()))
12723 };
12724 if inner.path.is_ident("list") {
12725 perms.list = parse_codenames(&inner)?;
12726 } else if inner.path.is_ident("retrieve") {
12727 perms.retrieve = parse_codenames(&inner)?;
12728 } else if inner.path.is_ident("create") {
12729 perms.create = parse_codenames(&inner)?;
12730 } else if inner.path.is_ident("update") {
12731 perms.update = parse_codenames(&inner)?;
12732 } else if inner.path.is_ident("destroy") {
12733 perms.destroy = parse_codenames(&inner)?;
12734 } else {
12735 return Err(inner.error(
12736 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
12737 ));
12738 }
12739 Ok(())
12740 })?;
12741 return Ok(());
12742 }
12743 Err(meta.error(
12744 "unknown viewset attribute (supported: model, fields, filter_fields, \
12745 search_fields, ordering, page_size, read_only, serializer, permissions(...))",
12746 ))
12747 })?;
12748 }
12749
12750 let model = model.ok_or_else(|| {
12751 syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
12752 })?;
12753
12754 Ok(ViewSetAttrs {
12755 model,
12756 fields,
12757 filter_fields,
12758 search_fields,
12759 ordering,
12760 page_size,
12761 read_only,
12762 perms,
12763 serializer,
12764 })
12765}
12766
12767// ============================================================ #[derive(Serializer)]
12768
12769struct SerializerContainerAttrs {
12770 model: syn::Path,
12771 /// `#[serializer(validate = "fn_name")]` on the struct — the
12772 /// cross-field validation hook (#436). The named inherent method
12773 /// must take `&self` and return
12774 /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
12775 /// `validate()` runs every per-field validator first; then calls
12776 /// the cross-field method if declared; aggregates all errors into
12777 /// one `FormErrors`.
12778 cross_validate: Option<syn::Ident>,
12779}
12780
12781#[derive(Default)]
12782struct SerializerFieldAttrs {
12783 read_only: bool,
12784 write_only: bool,
12785 source: Option<String>,
12786 skip: bool,
12787 /// `#[serializer(method = "fn_name")]` — computed field. The macro
12788 /// emits a `from_model` initializer that calls
12789 /// `Self::fn_name(&model)` and stores the return value.
12790 method: Option<String>,
12791 /// `#[serializer(validate = "fn_name")]` — per-field validator
12792 /// callable run by `Self::validate(&self)`. Must return
12793 /// `Result<(), String>`. Errors land in `FormErrors` keyed by
12794 /// the field name.
12795 validate: Option<String>,
12796 /// `#[serializer(nested)]` on a field whose type is another
12797 /// `Serializer` — the macro emits `from_model` initializer that
12798 /// reads the parent via `model.<source>.value()` then calls the
12799 /// child serializer's `from_model(parent)`. When the FK is
12800 /// unloaded the field falls back to `Default::default()` (does
12801 /// NOT panic) so a missing prefetch in prod degrades gracefully.
12802 /// Source field on the model defaults to the field name; override
12803 /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
12804 /// panic-on-unloaded behavior for tests.
12805 nested: bool,
12806 /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
12807 /// strict behavior: panic when the FK isn't loaded. Useful in
12808 /// test code where forgetting select_related must trip a hard
12809 /// failure rather than render a blank nested object.
12810 nested_strict: bool,
12811 /// `#[serializer(many = TagSerializer)]` — declare the field as
12812 /// a list of nested serializers. Field type must be `Vec<S>`
12813 /// where `S` is the inner serializer. The macro initializes the
12814 /// field to `Vec::new()` in `from_model` and emits a typed
12815 /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
12816 /// maps each model row through `S::from_model`. Auto-load isn't
12817 /// possible (the M2M / one-to-many accessor is async); callers
12818 /// fetch the children + call the setter post-from_model.
12819 many: Option<syn::Type>,
12820 /// `#[serializer(slug = "name")]` — render a relation as one of
12821 /// its fields.
12822 /// Source field on the model must be a `ForeignKey<T>`; the
12823 /// macro emits `from_model` glue that walks
12824 /// `model.<source>.value()?.<slug>` and clones it. Field type on
12825 /// the serializer is typically `String` (whatever type the slug
12826 /// column has). When the FK is unloaded the field falls back to
12827 /// `Default::default()`, same graceful-degrade contract as
12828 /// `nested`. Source defaults to the field name; override with
12829 /// `source = "..."`. v0.44.
12830 slug: Option<String>,
12831 /// `#[serializer(max_length = N)]` — caps
12832 /// the character count of a string field on write. Overrides the
12833 /// model's `max_length`; when absent the model value is inherited.
12834 max_length: Option<u64>,
12835 /// `#[serializer(min_length = N)]` — minimum character count.
12836 /// Serializer-only (the model has no `min_length` column).
12837 min_length: Option<u64>,
12838 /// `#[serializer(min = N)]` — inclusive
12839 /// integer lower bound; overrides the model's `min` when given.
12840 min: Option<i64>,
12841 /// `#[serializer(max = N)]` — inclusive
12842 /// integer upper bound; overrides the model's `max` when given.
12843 max: Option<i64>,
12844}
12845
12846fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
12847 let mut model: Option<syn::Path> = None;
12848 let mut cross_validate: Option<syn::Ident> = None;
12849 for attr in &input.attrs {
12850 if !attr.path().is_ident("serializer") {
12851 continue;
12852 }
12853 attr.parse_nested_meta(|meta| {
12854 if meta.path.is_ident("model") {
12855 let _eq: syn::Token![=] = meta.input.parse()?;
12856 model = Some(meta.input.parse()?);
12857 return Ok(());
12858 }
12859 if meta.path.is_ident("validate") {
12860 // #436 — container-level `validate = "fn_name"` for
12861 // cross-field validation. Field-level
12862 // `#[serializer(validate = "...")]` on a field is
12863 // parsed separately in `parse_serializer_field_attrs`.
12864 let s: LitStr = meta.value()?.parse()?;
12865 cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
12866 return Ok(());
12867 }
12868 Err(meta.error(
12869 "unknown serializer container attribute \
12870 (supported: `model`, `validate`)",
12871 ))
12872 })?;
12873 }
12874 let model = model.ok_or_else(|| {
12875 syn::Error::new_spanned(
12876 &input.ident,
12877 "`#[serializer(model = SomeModel)]` is required",
12878 )
12879 })?;
12880 Ok(SerializerContainerAttrs {
12881 model,
12882 cross_validate,
12883 })
12884}
12885
12886fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
12887 let mut out = SerializerFieldAttrs::default();
12888 for attr in &field.attrs {
12889 if !attr.path().is_ident("serializer") {
12890 continue;
12891 }
12892 attr.parse_nested_meta(|meta| {
12893 if meta.path.is_ident("read_only") {
12894 out.read_only = true;
12895 return Ok(());
12896 }
12897 if meta.path.is_ident("write_only") {
12898 out.write_only = true;
12899 return Ok(());
12900 }
12901 if meta.path.is_ident("skip") {
12902 out.skip = true;
12903 return Ok(());
12904 }
12905 if meta.path.is_ident("source") {
12906 let s: LitStr = meta.value()?.parse()?;
12907 out.source = Some(s.value());
12908 return Ok(());
12909 }
12910 if meta.path.is_ident("method") {
12911 let s: LitStr = meta.value()?.parse()?;
12912 out.method = Some(s.value());
12913 return Ok(());
12914 }
12915 if meta.path.is_ident("validate") {
12916 let s: LitStr = meta.value()?.parse()?;
12917 out.validate = Some(s.value());
12918 return Ok(());
12919 }
12920 if meta.path.is_ident("many") {
12921 let _eq: syn::Token![=] = meta.input.parse()?;
12922 out.many = Some(meta.input.parse()?);
12923 return Ok(());
12924 }
12925 if meta.path.is_ident("nested") {
12926 out.nested = true;
12927 // Optional strict flag inside parentheses:
12928 // #[serializer(nested(strict))]
12929 if meta.input.peek(syn::token::Paren) {
12930 meta.parse_nested_meta(|inner| {
12931 if inner.path.is_ident("strict") {
12932 out.nested_strict = true;
12933 return Ok(());
12934 }
12935 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
12936 })?;
12937 }
12938 return Ok(());
12939 }
12940 if meta.path.is_ident("slug") {
12941 let s: LitStr = meta.value()?.parse()?;
12942 out.slug = Some(s.value());
12943 return Ok(());
12944 }
12945 if meta.path.is_ident("max_length") {
12946 let lit: syn::LitInt = meta.value()?.parse()?;
12947 out.max_length = Some(lit.base10_parse::<u64>()?);
12948 return Ok(());
12949 }
12950 if meta.path.is_ident("min_length") {
12951 let lit: syn::LitInt = meta.value()?.parse()?;
12952 out.min_length = Some(lit.base10_parse::<u64>()?);
12953 return Ok(());
12954 }
12955 if meta.path.is_ident("min") {
12956 let lit: syn::LitInt = meta.value()?.parse()?;
12957 out.min = Some(lit.base10_parse::<i64>()?);
12958 return Ok(());
12959 }
12960 if meta.path.is_ident("max") {
12961 let lit: syn::LitInt = meta.value()?.parse()?;
12962 out.max = Some(lit.base10_parse::<i64>()?);
12963 return Ok(());
12964 }
12965 Err(meta.error(
12966 "unknown serializer field attribute (supported: \
12967 `read_only`, `write_only`, `source`, `skip`, `method`, \
12968 `validate`, `nested`, `many`, `slug`, `max_length`, \
12969 `min_length`, `min`, `max`)",
12970 ))
12971 })?;
12972 }
12973 // Validate: read_only + write_only is nonsensical
12974 if out.read_only && out.write_only {
12975 return Err(syn::Error::new_spanned(
12976 field,
12977 "a field cannot be both `read_only` and `write_only`",
12978 ));
12979 }
12980 if out.method.is_some() && out.source.is_some() {
12981 return Err(syn::Error::new_spanned(
12982 field,
12983 "`method` and `source` are mutually exclusive — `method` computes \
12984 the value from a method, `source` reads it from a different model field",
12985 ));
12986 }
12987 if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
12988 return Err(syn::Error::new_spanned(
12989 field,
12990 "`slug` is mutually exclusive with `method`, `nested`, and `many` \
12991 — pick one strategy for populating the field",
12992 ));
12993 }
12994 Ok(out)
12995}
12996
12997fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
12998 let root = rustango_root();
12999 let struct_name = &input.ident;
13000 let struct_name_lit = struct_name.to_string();
13001
13002 let Data::Struct(data) = &input.data else {
13003 return Err(syn::Error::new_spanned(
13004 struct_name,
13005 "Serializer can only be derived on structs",
13006 ));
13007 };
13008 let Fields::Named(named) = &data.fields else {
13009 return Err(syn::Error::new_spanned(
13010 struct_name,
13011 "Serializer requires a struct with named fields",
13012 ));
13013 };
13014
13015 let container = parse_serializer_container_attrs(input)?;
13016 let model_path = &container.model;
13017
13018 // Classify each field. `ty` is only consumed by the
13019 // `#[cfg(feature = "openapi")]` block below, but we always
13020 // capture it to keep the field-info build a single pass.
13021 #[allow(dead_code)]
13022 struct FieldInfo {
13023 ident: syn::Ident,
13024 ty: syn::Type,
13025 attrs: SerializerFieldAttrs,
13026 }
13027 let mut fields_info: Vec<FieldInfo> = Vec::new();
13028 for field in &named.named {
13029 let ident = field.ident.clone().expect("named field has ident");
13030 let attrs = parse_serializer_field_attrs(field)?;
13031 fields_info.push(FieldInfo {
13032 ident,
13033 ty: field.ty.clone(),
13034 attrs,
13035 });
13036 }
13037
13038 // Generate from_model body: struct literal with each field assigned.
13039 let from_model_fields = fields_info.iter().map(|fi| {
13040 let ident = &fi.ident;
13041 let ty = &fi.ty;
13042 if let Some(_inner) = &fi.attrs.many {
13043 // Many — collection field. Initialize empty; caller
13044 // populates via the macro-emitted set_<field> helper
13045 // after fetching the M2M children.
13046 quote! { #ident: ::std::vec::Vec::new() }
13047 } else if let Some(method) = &fi.attrs.method {
13048 // SerializerMethodField: call Self::<method>(&model) to
13049 // compute the value. Method signature must be
13050 // `fn <method>(model: &T) -> <field type>`.
13051 let method_ident = syn::Ident::new(method, ident.span());
13052 quote! { #ident: Self::#method_ident(model) }
13053 } else if let Some(slug_field) = &fi.attrs.slug {
13054 // v0.44 — SlugRelatedField. Source defaults to the field
13055 // name on this struct; override via `source = "..."`. The
13056 // source field on the model is expected to be a
13057 // `ForeignKey<T>`; the slug field on the parent is named
13058 // by the attribute value. When the FK is unloaded the
13059 // field falls back to `Default::default()` — same
13060 // graceful-degrade contract as `nested`.
13061 let src_name = fi
13062 .attrs
13063 .source
13064 .as_deref()
13065 .unwrap_or(&fi.ident.to_string())
13066 .to_owned();
13067 let src_ident = syn::Ident::new(&src_name, ident.span());
13068 let slug_ident = syn::Ident::new(slug_field, ident.span());
13069 quote! {
13070 #ident: match model.#src_ident.value() {
13071 ::core::option::Option::Some(__loaded) =>
13072 ::core::clone::Clone::clone(&__loaded.#slug_ident),
13073 ::core::option::Option::None =>
13074 ::core::default::Default::default(),
13075 }
13076 }
13077 } else if fi.attrs.nested {
13078 // Nested serializer. Source defaults to the field name on
13079 // this struct; override via `source = "..."`. The source
13080 // field on the model is expected to be a `ForeignKey<T>`
13081 // whose `.value()` returns `Option<&T>` after lazy-load.
13082 //
13083 // Behavior matrix (tweakable per-field):
13084 // * FK loaded → nested object materializes via
13085 // ChildSerializer::from_model(parent).
13086 // * FK unloaded → fall back to ChildSerializer::default()
13087 // (so prod doesn't crash on a missing
13088 // prefetch — just renders a blank nested
13089 // object). Add `#[serializer(nested,
13090 // strict)]` to keep the v0.18.1
13091 // panic-on-unloaded behavior for tests
13092 // that want hard guardrails.
13093 let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
13094 let src_ident = syn::Ident::new(&src_name, ident.span());
13095 if fi.attrs.nested_strict {
13096 let panic_msg = format!(
13097 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
13098 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
13099 );
13100 quote! {
13101 #ident: <#ty as #root::serializer::ModelSerializer>::from_model(
13102 model.#src_ident.value().expect(#panic_msg),
13103 )
13104 }
13105 } else {
13106 quote! {
13107 #ident: match model.#src_ident.value() {
13108 ::core::option::Option::Some(__loaded) =>
13109 <#ty as #root::serializer::ModelSerializer>::from_model(__loaded),
13110 ::core::option::Option::None =>
13111 ::core::default::Default::default(),
13112 }
13113 }
13114 }
13115 } else if fi.attrs.write_only || fi.attrs.skip {
13116 // Not read from model — use default
13117 quote! { #ident: ::core::default::Default::default() }
13118 } else if let Some(src) = &fi.attrs.source {
13119 let src_ident = syn::Ident::new(src, ident.span());
13120 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13121 } else {
13122 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13123 }
13124 });
13125
13126 // is_writable predicate (also used by writable_lits /
13127 // writable_source_lits below).
13128 let is_writable = |fi: &&FieldInfo| {
13129 !fi.attrs.read_only
13130 && !fi.attrs.skip
13131 && fi.attrs.method.is_none()
13132 && !fi.attrs.nested
13133 && fi.attrs.many.is_none()
13134 && fi.attrs.slug.is_none()
13135 };
13136
13137 // Declarative field constraints: one block
13138 // per writable field, run inside `validate()`. Each resolves its
13139 // bounds as the serializer attr when given (`#[serializer(max_length
13140 // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13141 // `max` / `choices`), then dispatches on the field's JSON value via
13142 // `forms::validators::check_value` (string → length/choices, integer
13143 // → min/max).
13144 let opt_usize = |v: Option<u64>| match v {
13145 Some(n) => {
13146 let n = n as usize;
13147 quote!(::core::option::Option::Some(#n))
13148 }
13149 None => quote!(::core::option::Option::None),
13150 };
13151 let opt_i64 = |v: Option<i64>| match v {
13152 Some(n) => quote!(::core::option::Option::Some(#n)),
13153 None => quote!(::core::option::Option::None),
13154 };
13155 let constraint_blocks: Vec<_> = fields_info
13156 .iter()
13157 .filter(is_writable)
13158 .map(|fi| {
13159 let ident = &fi.ident;
13160 let fname = ident.to_string();
13161 let mname = fi
13162 .attrs
13163 .source
13164 .clone()
13165 .unwrap_or_else(|| fi.ident.to_string());
13166 let attr_max_len = opt_usize(fi.attrs.max_length);
13167 let attr_min_len = opt_usize(fi.attrs.min_length);
13168 let attr_min = opt_i64(fi.attrs.min);
13169 let attr_max = opt_i64(fi.attrs.max);
13170 quote! {
13171 {
13172 let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13173 let __max_length: ::core::option::Option<usize> = #attr_max_len
13174 .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13175 let __min_length: ::core::option::Option<usize> = #attr_min_len;
13176 let __min: ::core::option::Option<i64> =
13177 #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13178 let __max: ::core::option::Option<i64> =
13179 #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13180 let __choices = __sf.and_then(|__f| __f.choices);
13181 let __v = #root::__serde_json::to_value(&self.#ident)
13182 .unwrap_or(#root::__serde_json::Value::Null);
13183 #root::forms::validators::check_value(
13184 #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13185 &mut __errors,
13186 );
13187 }
13188 }
13189 })
13190 .collect();
13191 let has_constraints = !constraint_blocks.is_empty();
13192
13193 // Per-field validators. Emit a
13194 // `validate(&self)` method that runs each user-defined validator
13195 // and aggregates errors into `FormErrors`.
13196 let validator_calls: Vec<_> = fields_info
13197 .iter()
13198 .filter_map(|fi| {
13199 let ident = &fi.ident;
13200 let name_lit = ident.to_string();
13201 let method = fi.attrs.validate.as_ref()?;
13202 let method_ident = syn::Ident::new(method, ident.span());
13203 Some(quote! {
13204 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13205 __errors.add(#name_lit.to_owned(), __e);
13206 }
13207 })
13208 })
13209 .collect();
13210 // #436 — cross-field `validate(&self)`. If the
13211 // container declared `#[serializer(validate = "fn_name")]`,
13212 // the macro-generated `validate(&self)` runs every per-field
13213 // validator first, then calls the user's cross-field method,
13214 // merging its `FormErrors` into the per-field errors. Either
13215 // alone is enough to emit the wrapper.
13216 let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13217 quote! {
13218 // Merge cross-field errors into the per-field bucket so
13219 // a single .validate() call surfaces both layers.
13220 if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13221 __errors.merge(__cross);
13222 }
13223 }
13224 });
13225 let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13226 // Anything to run? Declarative constraints (for any writable field) +
13227 // per-field validators + cross-field hook.
13228 let has_run_validations = has_validators || has_constraints;
13229 // Shared body: declarative field constraints first (length / range /
13230 // choices, serializer-attr-or-model), then per-field validators, then
13231 // the cross-field hook.
13232 let validate_body = quote! {
13233 let mut __errors = #root::forms::FormErrors::default();
13234 #( #constraint_blocks )*
13235 #( #validator_calls )*
13236 #cross_validate_call
13237 if __errors.is_empty() {
13238 ::core::result::Result::Ok(())
13239 } else {
13240 ::core::result::Result::Err(__errors)
13241 }
13242 };
13243 // Inherent `validate(&self)` — kept for back-compat with direct
13244 // `serializer.validate()` calls that don't import `ModelSerializer`.
13245 // Emitted only when the serializer declares per-field/cross-field
13246 // validators (unchanged rule) so it never collides with a
13247 // hand-written inherent `validate`.
13248 let validate_method = if has_validators {
13249 quote! {
13250 impl #struct_name {
13251 /// Run the declarative field constraints, every
13252 /// `#[serializer(validate = "...")]` per-field validator,
13253 /// and (when declared) the container-level cross-field
13254 /// validator. Aggregates errors into `FormErrors` keyed by
13255 /// the field name. Returns `Ok(())` when all pass.
13256 pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13257 #validate_body
13258 }
13259 }
13260 }
13261 } else {
13262 quote! {}
13263 };
13264 // Trait-method override so the type-erased ViewSet write path can
13265 // dispatch `ModelSerializer::validate` generically. Emitted whenever
13266 // there's anything to run (declarative constraints inherited from the
13267 // model count); otherwise the trait's default no-op is used.
13268 let trait_validate_override = if has_run_validations {
13269 quote! {
13270 fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13271 #validate_body
13272 }
13273 }
13274 } else {
13275 quote! {}
13276 };
13277
13278 // For every `#[serializer(many = S)]` field, emit a
13279 // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13280 // helper that maps the parents through `S::from_model`.
13281 let many_setters: Vec<_> = fields_info
13282 .iter()
13283 .filter_map(|fi| {
13284 let many_ty = fi.attrs.many.as_ref()?;
13285 let ident = &fi.ident;
13286 let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
13287 Some(quote! {
13288 /// Populate this `many` field by mapping each parent model
13289 /// through the inner serializer's `from_model`. Call after
13290 /// fetching the M2M / one-to-many children since
13291 /// `from_model` itself can't await an SQL query.
13292 pub fn #setter(
13293 &mut self,
13294 models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13295 ) -> &mut Self {
13296 self.#ident = models.iter()
13297 .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13298 .collect();
13299 self
13300 }
13301 })
13302 })
13303 .collect();
13304 let many_setters_impl = if many_setters.is_empty() {
13305 quote! {}
13306 } else {
13307 quote! {
13308 impl #struct_name {
13309 #( #many_setters )*
13310 }
13311 }
13312 };
13313
13314 // Generate custom Serialize: skip write_only fields
13315 let output_fields: Vec<_> = fields_info
13316 .iter()
13317 .filter(|fi| !fi.attrs.write_only)
13318 .collect();
13319 let output_field_count = output_fields.len();
13320 let serialize_fields = output_fields.iter().map(|fi| {
13321 let ident = &fi.ident;
13322 let name_lit = ident.to_string();
13323 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13324 });
13325
13326 // writable_fields: normal + write_only.
13327 // Exclude:
13328 // - `read_only` — server-computed.
13329 // - `skip` — caller sets manually post-from_model.
13330 // - `method` — computed from a Self::fn(&model) call; accepting
13331 // it on write is meaningless.
13332 // - `nested` / `many` — populated from related-model data, not
13333 // from a field on the wire body.
13334 // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13335 // `many` in `writable_fields()`, which made the ViewSet write
13336 // path accept those fields from the JSON body and try to bind
13337 // them to the SQL UPDATE — a silent no-op at best, a type
13338 // mismatch at worst.
13339 // (`is_writable` is defined above, near the constraint codegen.)
13340 let writable_lits: Vec<_> = fields_info
13341 .iter()
13342 .filter(is_writable)
13343 .map(|fi| fi.ident.to_string())
13344 .collect();
13345
13346 // `writable_source_fields`: the MODEL field names of writable
13347 // serializer fields (source-resolved). The ViewSet write path skips
13348 // every model column NOT in this set, so `read_only` / `method` /
13349 // computed fields a client posts are ignored instead of written.
13350 let writable_source_lits: Vec<String> = fields_info
13351 .iter()
13352 .filter(is_writable)
13353 .map(|fi| {
13354 fi.attrs
13355 .source
13356 .clone()
13357 .unwrap_or_else(|| fi.ident.to_string())
13358 })
13359 .collect();
13360
13361 // `from_writable_json`: build a partial instance for input
13362 // validation. Writable fields are parsed from the JSON body (keyed
13363 // by serializer field name); every other field defaults. Per-field
13364 // type errors collect into `FormErrors` keyed by the field name.
13365 // Construction is field-by-field (not `Self::default()`) so it works
13366 // for serializers regardless of a struct-level `Default`.
13367 let from_writable_json_inits: Vec<_> = fields_info
13368 .iter()
13369 .map(|fi| {
13370 let ident = &fi.ident;
13371 let fname = ident.to_string();
13372 let ty = &fi.ty;
13373 if is_writable(&fi) {
13374 quote! {
13375 #ident: match __obj.and_then(|__o| __o.get(#fname)) {
13376 ::core::option::Option::Some(__v) => {
13377 match #root::__serde_json::from_value::<#ty>(
13378 ::core::clone::Clone::clone(__v),
13379 ) {
13380 ::core::result::Result::Ok(__x) => __x,
13381 ::core::result::Result::Err(__e) => {
13382 __errors.add(#fname.to_owned(), __e.to_string());
13383 ::core::default::Default::default()
13384 }
13385 }
13386 }
13387 ::core::option::Option::None => ::core::default::Default::default(),
13388 }
13389 }
13390 } else {
13391 quote! { #ident: ::core::default::Default::default() }
13392 }
13393 })
13394 .collect();
13395
13396 // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
13397 // Only includes fields shown in JSON output (skips write_only). For each
13398 // `Option<T>` field, omit from `required` and add `.nullable()`.
13399 let openapi_impl = {
13400 #[cfg(feature = "openapi")]
13401 {
13402 let property_calls = output_fields.iter().map(|fi| {
13403 let ident = &fi.ident;
13404 let name_lit = ident.to_string();
13405 let ty = &fi.ty;
13406 let nullable_call = if is_option(ty) {
13407 quote! { .nullable() }
13408 } else {
13409 quote! {}
13410 };
13411 quote! {
13412 .property(
13413 #name_lit,
13414 <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13415 #nullable_call,
13416 )
13417 }
13418 });
13419 let required_lits: Vec<_> = output_fields
13420 .iter()
13421 .filter(|fi| !is_option(&fi.ty))
13422 .map(|fi| fi.ident.to_string())
13423 .collect();
13424 quote! {
13425 impl #root::openapi::OpenApiSchema for #struct_name {
13426 fn openapi_schema() -> #root::openapi::Schema {
13427 #root::openapi::Schema::object()
13428 #( #property_calls )*
13429 .required([ #( #required_lits ),* ])
13430 }
13431 }
13432 }
13433 }
13434 #[cfg(not(feature = "openapi"))]
13435 {
13436 quote! {}
13437 }
13438 };
13439
13440 Ok(quote! {
13441 impl #root::serializer::ModelSerializer for #struct_name {
13442 type Model = #model_path;
13443
13444 fn from_model(model: &Self::Model) -> Self {
13445 Self {
13446 #( #from_model_fields ),*
13447 }
13448 }
13449
13450 fn writable_fields() -> &'static [&'static str] {
13451 &[ #( #writable_lits ),* ]
13452 }
13453
13454 fn writable_source_fields() -> &'static [&'static str] {
13455 &[ #( #writable_source_lits ),* ]
13456 }
13457
13458 fn from_writable_json(
13459 __body: &#root::__serde_json::Value,
13460 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
13461 let mut __errors = #root::forms::FormErrors::default();
13462 let __obj = __body.as_object();
13463 let __out = Self {
13464 #( #from_writable_json_inits ),*
13465 };
13466 if __errors.is_empty() {
13467 ::core::result::Result::Ok(__out)
13468 } else {
13469 ::core::result::Result::Err(__errors)
13470 }
13471 }
13472
13473 #trait_validate_override
13474 }
13475
13476 impl #root::__serde::Serialize for #struct_name {
13477 fn serialize<S>(&self, serializer: S)
13478 -> ::core::result::Result<S::Ok, S::Error>
13479 where
13480 S: #root::__serde::Serializer,
13481 {
13482 use #root::__serde::ser::SerializeStruct;
13483 let mut __state = serializer.serialize_struct(
13484 #struct_name_lit,
13485 #output_field_count,
13486 )?;
13487 #( #serialize_fields )*
13488 __state.end()
13489 }
13490 }
13491
13492 #openapi_impl
13493
13494 #validate_method
13495
13496 #many_setters_impl
13497 })
13498}
13499
13500/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
13501/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
13502/// when the feature is off.
13503#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
13504fn is_option(ty: &syn::Type) -> bool {
13505 if let syn::Type::Path(p) = ty {
13506 if let Some(last) = p.path.segments.last() {
13507 return last.ident == "Option";
13508 }
13509 }
13510 false
13511}
13512
13513#[cfg(test)]
13514mod main_attr_tests {
13515 use super::*;
13516
13517 fn expand(args: &str) -> String {
13518 let body = quote! { async fn main() { run().await } };
13519 expand_main(args.parse().unwrap(), body)
13520 .expect("expansion failed")
13521 .to_string()
13522 }
13523
13524 /// Asserts on the expansion rather than on `parse_logging`,
13525 /// because the defect was a subscriber reaching the output, not
13526 /// an argument being misread (#1465).
13527 #[test]
13528 fn logging_false_emits_no_subscriber() {
13529 let out = expand("logging = false");
13530 assert!(
13531 !out.contains("tracing_subscriber"),
13532 "`logging = false` must not install a subscriber:\n{out}",
13533 );
13534 assert!(
13535 out.contains("block_on"),
13536 "the runtime must still be built:\n{out}",
13537 );
13538 }
13539
13540 #[test]
13541 fn default_still_installs_a_subscriber() {
13542 // The control. Without it, an `expand_main` that dropped the
13543 // prologue unconditionally would pass the test above while
13544 // leaving every default project with no logging at all.
13545 let out = expand("");
13546 assert!(
13547 out.contains("tracing_subscriber"),
13548 "the default entrypoint must still install one:\n{out}",
13549 );
13550 }
13551
13552 #[test]
13553 fn logging_false_composes_with_flavor() {
13554 let out = expand("flavor = \"current_thread\", logging = false");
13555 assert!(!out.contains("tracing_subscriber"), "{out}");
13556 assert!(out.contains("new_current_thread"), "{out}");
13557 }
13558
13559 #[test]
13560 fn logging_true_is_the_default_shape() {
13561 // `logging = true` is accepted and means the default, so the
13562 // match must not fire on the substring `logging`.
13563 assert!(parse_logging(&"logging = true".parse().unwrap()));
13564 assert!(!parse_logging(&"logging=false".parse().unwrap()));
13565 }
13566}