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/// * `allow_anonymous` — flag; write actions without codenames are
89/// intended, so the mount warning is skipped.
90/// * `serializer = SomeSerializer` — render list / retrieve / create
91/// responses through a `#[derive(Serializer)]` type instead of the
92/// default field-level projection (`read_only` / `source` / `method`
93/// / `write_only` overrides apply). Tri-dialect. Requires the
94/// `serializer` feature.
95/// * `permissions(list = "...", retrieve = "...", create = "...",
96/// update = "...", destroy = "...")` — codenames required per action.
97#[proc_macro_derive(ViewSet, attributes(viewset))]
98pub fn derive_viewset(input: TokenStream) -> TokenStream {
99 let input = parse_macro_input!(input as DeriveInput);
100 expand_viewset(&input)
101 .unwrap_or_else(syn::Error::into_compile_error)
102 .into()
103}
104
105/// Derive `rustango::forms::Form` (slice 8.4B). Generates a
106/// `parse(&HashMap<String, String>) -> Result<Self, FormErrors>` impl
107/// that walks every named field and:
108///
109/// * Parses the string value into the field's Rust type (`String`,
110/// `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
111/// nullable case).
112/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
113/// `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
114/// validators in declaration order, returning `FormError::Parse`
115/// on the first failure.
116///
117/// Example:
118///
119/// ```ignore
120/// #[derive(Form)]
121/// pub struct CreateItemForm {
122/// #[form(min_length = 1, max_length = 64)]
123/// pub name: String,
124/// #[form(min = 0, max = 150)]
125/// pub age: i32,
126/// pub active: bool,
127/// pub email: Option<String>,
128/// }
129///
130/// let parsed = CreateItemForm::parse(&form_map)?;
131/// ```
132#[proc_macro_derive(Form, attributes(form))]
133pub fn derive_form(input: TokenStream) -> TokenStream {
134 let input = parse_macro_input!(input as DeriveInput);
135 expand_form(&input)
136 .unwrap_or_else(syn::Error::into_compile_error)
137 .into()
138}
139
140/// Derive `rustango::serializer::ModelSerializer` for a struct.
141/// (intra-doc link disabled — the macro crate doesn't depend on
142/// `rustango` itself, so rustdoc can't resolve the path.)
143///
144/// # Container attribute (required)
145/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
146///
147/// # Field attributes
148/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
149/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
150/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
151/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
152/// - `#[serializer(method = "fn_name")]` — computed field: calls `Self::fn_name(&model)` for the field value; excluded from `writable_fields()`
153/// - `#[serializer(nested)]` / `nested(strict)` — auto-resolves nested serializer from a loaded `ForeignKey`; excluded from `writable_fields()`
154/// - `#[serializer(many = ChildSerializer)]` — collection of nested serializers; populated via macro-emitted `set_<field>(&[Child::Model])`; excluded from `writable_fields()`
155/// - `#[serializer(slug = "name")]` — render a relation (`ForeignKey<T>` or `Option<ForeignKey<T>>`) as one of its fields: clones the loaded parent's `name`; excluded from `writable_fields()` (v0.44)
156/// - `#[serializer(validate = "fn_name")]` — per-field validator surfaced by `Self::validate(&self)`
157/// - `#[serializer(max_length = N)]` / `min_length` / `min` / `max` — declarative bounds checked on
158/// write; auto-inherit from the model's `FieldSchema` (`max_length`/`min`/`max`/`choices`) when
159/// no attr is given, override it when given (`min_length` is serializer-only)
160///
161/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
162#[proc_macro_derive(Serializer, attributes(serializer))]
163pub fn derive_serializer(input: TokenStream) -> TokenStream {
164 let input = parse_macro_input!(input as DeriveInput);
165 expand_serializer(&input)
166 .unwrap_or_else(syn::Error::into_compile_error)
167 .into()
168}
169
170/// Bake every `*.json` migration file in a directory into the binary
171/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
172/// of `(name, json_content)` pairs, lex-sorted by file stem.
173///
174/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
175/// behaviour as `migrate(pool, dir)` but with no filesystem access.
176/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
177/// (i.e. the crate that invokes the macro). Default is
178/// `"./migrations"` if no argument is supplied.
179///
180/// ```ignore
181/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
182/// // or:
183/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
184///
185/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
186/// ```
187///
188/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
189/// file's `name` field must equal its file stem, every `prev`
190/// reference must point to another migration in the same directory,
191/// and the JSON must parse. A broken chain — orphan `prev`, missing
192/// predecessor, malformed file — fails at macro-expansion time with
193/// a clear `compile_error!`. *No other Rust web framework
194/// validates migration chains at compile time*: Cot's migrations are
195/// imperative Rust code (no static chain), Loco's are SeaORM
196/// up/down (same), Rwf's are raw SQL (no chain at all).
197///
198/// Each migration is included via `include_str!` so cargo's rebuild
199/// detection picks up file *content* changes. Cargo does not watch
200/// directory listings, so a new migration file is missed until the
201/// crate rebuilds. Add a `build.rs` that tracks the directory:
202///
203/// ```ignore
204/// fn main() { println!("cargo:rerun-if-changed=migrations"); }
205/// ```
206///
207/// A missing directory embeds nothing and emits a deprecation warning.
208#[proc_macro]
209pub fn embed_migrations(input: TokenStream) -> TokenStream {
210 expand_embed_migrations(input.into())
211 .unwrap_or_else(syn::Error::into_compile_error)
212 .into()
213}
214
215/// `Q!()` — `Model.field__lookup = value` filter syntax, resolved at
216/// compile time against typed columns. Issue #269 / T1.7.
217///
218/// Each invocation lowers to the equivalent typed-column method call:
219///
220/// ```ignore
221/// // These expand identically:
222/// Q!(User.email__icontains = "alice")
223/// User::email.ilike("%alice%")
224/// ```
225///
226/// Field-name typos fail the build (the macro emits `User::no_such_field`
227/// which doesn't exist) — the headline ergonomic win of this slice over
228/// filters keyed by a plain string.
229///
230/// # Supported lookup suffixes
231///
232/// * bare `=` / `__exact` → `.eq(value)`
233/// * `__iexact` → `.ilike(value)` (case-insensitive equality, no wildcards)
234/// * `__ne` → `.ne(value)`
235/// * `__gt` / `__gte` / `__lt` / `__lte` → corresponding comparison
236/// * `__contains` / `__icontains` → `.like("%v%")` / `.ilike("%v%")`
237/// * `__startswith` / `__istartswith` → `.like("v%")` / `.ilike("v%")`
238/// * `__endswith` / `__iendswith` → `.like("%v")` / `.ilike("%v")`
239/// * `__in` → `.is_in(iterable)`
240/// * `__not_in` → `.not_in(iterable)`
241/// * `__isnull = true` → `.is_null()`; `__isnull = false` → `.is_not_null()`
242/// * `__between` accepts a tuple literal `(lo, hi)` → `.between(lo, hi)`
243/// * `__regex` / `__iregex` → `.regex(pattern)` / `.iregex(pattern)`
244///
245/// Unknown suffixes fail the build with a `compile_error!` pointing at
246/// the lookup token.
247///
248/// # Combine
249///
250/// Each `Q!()` returns a `TypedFilter<Model>` — chain via the existing
251/// `.and()` / `.or()` / `.not()` methods:
252///
253/// ```ignore
254/// User::objects()
255/// .where_(
256/// Q!(User.active = true)
257/// .and(Q!(User.email__icontains = "alice"))
258/// )
259/// .fetch(&pool).await?;
260/// ```
261///
262/// All emitted code routes through existing per-dialect writers — no new
263/// SQL emission machinery. Tri-dialect support is inherent.
264#[allow(non_snake_case)]
265#[proc_macro]
266pub fn Q(input: TokenStream) -> TokenStream {
267 expand_q(input.into())
268 .unwrap_or_else(syn::Error::into_compile_error)
269 .into()
270}
271
272/// `#[rustango::main]` — the runserver entrypoint. Wraps
273/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
274/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
275/// functions are zero-boilerplate. `RUST_LOG` and
276/// `RUSTANGO__LOGGING__FORMAT` (e.g. `json`) come from the real
277/// environment, else from `./.env` (only those keys are read):
278///
279/// ```ignore
280/// #[rustango::main]
281/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
282/// rustango::server::Builder::from_env().await?
283/// .migrate("migrations").await?
284/// .api(my_app::urls::api())
285/// .seed_with(my_app::seed::run).await?
286/// .serve("0.0.0.0:8080").await
287/// }
288/// ```
289///
290/// Optional `flavor = "current_thread"` passes through to
291/// `#[tokio::main]`; default is the multi-threaded runtime.
292///
293/// `logging = false` suppresses the default subscriber, for apps
294/// that install their own. Needed whenever the body reaches
295/// `Cli::with_logging()` or `logging::Setup`: the subscriber this
296/// macro installs gets there first, and `tracing` keeps the first
297/// one, so the `[logging]` section would otherwise be read and
298/// discarded (#1465).
299///
300/// ```ignore
301/// #[rustango::main(logging = false)]
302/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
303/// rustango::manage::Cli::new()
304/// .with_settings_from_env()
305/// .with_logging() // now the first installer
306/// .run().await
307/// }
308/// ```
309///
310/// Pulls `tracing-subscriber` into the rustango crate behind the
311/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
312/// out get plain `#[tokio::main]` ergonomics without the dependency.
313#[proc_macro_attribute]
314pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
315 expand_main(args.into(), item.into())
316 .unwrap_or_else(syn::Error::into_compile_error)
317 .into()
318}
319
320fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
321 let mut input: syn::ItemFn = syn::parse2(item)?;
322 if input.sig.asyncness.is_none() {
323 return Err(syn::Error::new(
324 input.sig.ident.span(),
325 "`#[rustango::main]` must wrap an `async fn`",
326 ));
327 }
328
329 // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
330 // to `#[tokio::main]`. Tokio's proc-macro internally emits
331 // `::tokio::*` paths that resolve against the user crate's deps,
332 // so calling it through the rustango re-export still requires the
333 // user to add tokio to their own Cargo.toml. Building the
334 // runtime ourselves keeps the dep transitive through the
335 // `runtime` feature on rustango.
336 //
337 // Parse optional `flavor = "current_thread"` / `flavor =
338 // "multi_thread"` from the attribute args. Unknown args are
339 // tolerated (forward-compat with tokio's own arg surface).
340 let root = rustango_root();
341 let flavor = parse_flavor(&args);
342 let builder_call = match flavor {
343 Flavor::CurrentThread => quote! {
344 #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
345 },
346 Flavor::MultiThread => quote! {
347 #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
348 },
349 };
350
351 // Detach the user body and rewrite `main` as a sync fn that
352 // builds the runtime and blocks on the async body.
353 let user_body = input.block.clone();
354 input.sig.asyncness = None;
355
356 // `logging = false` drops the block entirely, so a later
357 // `Cli::with_logging()` is the first installer and its
358 // `[logging]` settings actually take effect (#1465).
359 let logging_prologue = if parse_logging(&args) {
360 quote! {
361 // `Setup`'s defaults, not a copy of them: filter, format and
362 // the `Color::Auto` rule live in one place (#1493).
363 // `logging` and this macro share the `runtime` gate.
364 // The real `RUST_LOG` still wins over `./.env`'s (#2204).
365 let _ = #root::logging::Setup::new()
366 .with_default_env_filter(#root::__private_runtime::main_default_filter())
367 .with_format(#root::__private_runtime::main_format())
368 .install();
369 }
370 } else {
371 quote! {}
372 };
373
374 input.block = syn::parse2(quote! {{
375 { #logging_prologue }
376 let __rt = #builder_call
377 .enable_all()
378 .build()
379 .expect("failed to build tokio runtime");
380 __rt.block_on(async move #user_body)
381 }})?;
382
383 Ok(quote! {
384 #input
385 })
386}
387
388enum Flavor {
389 MultiThread,
390 CurrentThread,
391}
392
393fn parse_flavor(args: &TokenStream2) -> Flavor {
394 // Cheap parser: look for the literal token sequence
395 // `flavor = "current_thread"`. Everything else (including
396 // bare `multi_thread` or no args) defaults to multi-thread.
397 let s = args.to_string();
398 if s.contains("current_thread") {
399 Flavor::CurrentThread
400 } else {
401 Flavor::MultiThread
402 }
403}
404
405/// `false` when the attribute carries `logging = false` — the caller
406/// installs their own subscriber, usually via `Cli::with_logging`
407/// (#1465). Same cheap token match as `parse_flavor`.
408fn parse_logging(args: &TokenStream2) -> bool {
409 !args.to_string().replace(' ', "").contains("logging=false")
410}
411
412/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
413struct QInput {
414 base_path: syn::Path,
415 field: syn::Ident,
416 value: syn::Expr,
417}
418
419impl syn::parse::Parse for QInput {
420 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
421 let base_path: syn::Path = input.parse()?;
422 input.parse::<syn::Token![.]>()?;
423 let field: syn::Ident = input.parse()?;
424 input.parse::<syn::Token![=]>()?;
425 let value: syn::Expr = input.parse()?;
426 Ok(QInput {
427 base_path,
428 field,
429 value,
430 })
431 }
432}
433
434fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
435 let q: QInput = syn::parse2(input)?;
436 let root = rustango_root();
437 let field_str = ident_name(&q.field);
438 let field_span = q.field.span();
439 let (base, suffix) = match field_str.find("__") {
440 Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
441 None => (field_str.as_str(), ""),
442 };
443 if base.is_empty() {
444 return Err(syn::Error::new(
445 field_span,
446 "Q!(): field name is empty before `__` suffix",
447 ));
448 }
449 let base_ident = ident_from_name(base, field_span);
450 let value = &q.value;
451 let path = &q.base_path;
452
453 // Most suffixes map directly to a Column method with the value
454 // forwarded unchanged. Some need value-shape massaging (wildcards
455 // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
456 // ISNULL). Unknown suffixes fail the build.
457 let expanded = match suffix {
458 "" | "exact" => quote! {
459 #root::core::Column::eq(#path::#base_ident, #value)
460 },
461 "ne" => quote! {
462 #root::core::Column::ne(#path::#base_ident, #value)
463 },
464 "gt" => quote! {
465 #root::core::Column::gt(#path::#base_ident, #value)
466 },
467 "gte" => quote! {
468 #root::core::Column::gte(#path::#base_ident, #value)
469 },
470 "lt" => quote! {
471 #root::core::Column::lt(#path::#base_ident, #value)
472 },
473 "lte" => quote! {
474 #root::core::Column::lte(#path::#base_ident, #value)
475 },
476 // The escaped-LIKE lookups delegate to the typed Column
477 // methods, which escape the value and pair it with the
478 // *Escaped ops so `%` / `_` match literally on every dialect
479 // (#1257) — the macro no longer owns any pattern wrapping.
480 "iexact" => quote! {
481 #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
482 },
483 "contains" => quote! {
484 #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
485 },
486 "icontains" => quote! {
487 #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
488 },
489 "startswith" => quote! {
490 #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
491 },
492 "istartswith" => quote! {
493 #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
494 },
495 "endswith" => quote! {
496 #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
497 },
498 "iendswith" => quote! {
499 #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
500 },
501 "in" => quote! {
502 #root::core::Column::is_in(#path::#base_ident, #value)
503 },
504 "not_in" => quote! {
505 #root::core::Column::not_in(#path::#base_ident, #value)
506 },
507 "isnull" => {
508 // Must be a bool literal at macro time so we can route to
509 // is_null vs is_not_null without a runtime branch.
510 let b = match value {
511 syn::Expr::Lit(syn::ExprLit {
512 lit: syn::Lit::Bool(b),
513 ..
514 }) => b.value(),
515 _ => {
516 return Err(syn::Error::new_spanned(
517 value,
518 "Q!(): `__isnull` requires a `true` or `false` literal",
519 ));
520 }
521 };
522 if b {
523 quote! { #root::core::Column::is_null(#path::#base_ident) }
524 } else {
525 quote! { #root::core::Column::is_not_null(#path::#base_ident) }
526 }
527 }
528 "between" => {
529 // Accept a tuple literal `(lo, hi)`.
530 let tuple = match value {
531 syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
532 _ => {
533 return Err(syn::Error::new_spanned(
534 value,
535 "Q!(): `__between` requires a tuple literal `(lo, hi)`",
536 ));
537 }
538 };
539 let lo = &tuple.elems[0];
540 let hi = &tuple.elems[1];
541 quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
542 }
543 "regex" => quote! {
544 #root::core::Column::regex(#path::#base_ident, #value)
545 },
546 "iregex" => quote! {
547 #root::core::Column::iregex(#path::#base_ident, #value)
548 },
549 _ => {
550 return Err(syn::Error::new(
551 field_span,
552 format!(
553 "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",
554 suffix
555 ),
556 ));
557 }
558 };
559 Ok(expanded)
560}
561
562fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
563 // Default to "./migrations" if invoked without args.
564 let path_str = if input.is_empty() {
565 "./migrations".to_string()
566 } else {
567 let lit: LitStr = syn::parse2(input)?;
568 lit.value()
569 };
570
571 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
572 syn::Error::new(
573 proc_macro2::Span::call_site(),
574 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
575 )
576 })?;
577 let abs = std::path::Path::new(&manifest).join(&path_str);
578
579 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
580 if abs.is_dir() {
581 let read = std::fs::read_dir(&abs).map_err(|e| {
582 syn::Error::new(
583 proc_macro2::Span::call_site(),
584 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
585 )
586 })?;
587 for entry in read.flatten() {
588 let path = entry.path();
589 if !path.is_file() {
590 continue;
591 }
592 if path.extension().and_then(|s| s.to_str()) != Some("json") {
593 continue;
594 }
595 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
596 continue;
597 };
598 entries.push((stem.to_owned(), path));
599 }
600 }
601 entries.sort_by(|a, b| a.0.cmp(&b.0));
602
603 // Compile-time chain validation: read each migration's JSON,
604 // pull `name` and `prev` (file-stem-keyed for the chain check),
605 // and verify every `prev` points to another migration in the
606 // slice. Mismatches between the file stem and the embedded
607 // `name` field — and broken `prev` chains — fail at MACRO
608 // EXPANSION time so a misshapen migration set never compiles.
609 //
610 // This is the v0.4 Slice 5 distinguisher: rustango's JSON
611 // migrations + a Rust proc-macro that reads them is a combo no
612 // other Rust web framework matches (Cot's are imperative Rust
613 // code, Loco's are SeaORM up/down, Rwf's are raw SQL — none have
614 // a static chain to validate).
615 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
616 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
617 for (stem, path) in &entries {
618 let raw = std::fs::read_to_string(path).map_err(|e| {
619 syn::Error::new(
620 proc_macro2::Span::call_site(),
621 format!(
622 "embed_migrations!: cannot read {} for chain validation: {e}",
623 path.display()
624 ),
625 )
626 })?;
627 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
628 syn::Error::new(
629 proc_macro2::Span::call_site(),
630 format!(
631 "embed_migrations!: {} is not valid JSON: {e}",
632 path.display()
633 ),
634 )
635 })?;
636 let name = json
637 .get("name")
638 .and_then(|v| v.as_str())
639 .ok_or_else(|| {
640 syn::Error::new(
641 proc_macro2::Span::call_site(),
642 format!(
643 "embed_migrations!: {} is missing the `name` field",
644 path.display()
645 ),
646 )
647 })?
648 .to_owned();
649 if name != *stem {
650 return Err(syn::Error::new(
651 proc_macro2::Span::call_site(),
652 format!(
653 "embed_migrations!: file stem `{stem}` does not match the migration's \
654 `name` field `{name}` — rename the file or fix the JSON",
655 ),
656 ));
657 }
658 let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
659 chain_names.push(name.clone());
660 prev_refs.push((name, prev));
661 }
662
663 let name_set: std::collections::HashSet<&str> =
664 chain_names.iter().map(String::as_str).collect();
665 for (name, prev) in &prev_refs {
666 if let Some(p) = prev {
667 if !name_set.contains(p.as_str()) {
668 return Err(syn::Error::new(
669 proc_macro2::Span::call_site(),
670 format!(
671 "embed_migrations!: broken migration chain — `{name}` declares \
672 prev=`{p}` but no migration with that name exists in {}",
673 abs.display()
674 ),
675 ));
676 }
677 }
678 }
679
680 let pairs: Vec<TokenStream2> = entries
681 .iter()
682 .map(|(name, path)| {
683 let path_lit = path.display().to_string();
684 quote! { (#name, ::core::include_str!(#path_lit)) }
685 })
686 .collect();
687
688 // A wrong path used to embed nothing silently; a deprecation use is
689 // the only stable way for a proc macro to warn without breaking builds.
690 let missing_warning = if abs.is_dir() {
691 quote! {}
692 } else {
693 let note = format!(
694 "embed_migrations!: directory {} does not exist; no migrations are embedded",
695 abs.display()
696 );
697 quote! {
698 #[deprecated(note = #note)]
699 const __RUSTANGO_MIGRATIONS_DIR_MISSING: () = ();
700 #[allow(clippy::let_unit_value)]
701 let _ = __RUSTANGO_MIGRATIONS_DIR_MISSING;
702 }
703 };
704
705 Ok(quote! {
706 {
707 #missing_warning
708 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
709 __RUSTANGO_EMBEDDED
710 }
711 })
712}
713
714fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
715 let root = rustango_root();
716 let struct_name = &input.ident;
717
718 let Data::Struct(data) = &input.data else {
719 return Err(syn::Error::new_spanned(
720 struct_name,
721 "Model can only be derived on structs",
722 ));
723 };
724 let Fields::Named(named) = &data.fields else {
725 return Err(syn::Error::new_spanned(
726 struct_name,
727 "Model requires a struct with named fields",
728 ));
729 };
730
731 let container = parse_container_attrs(input)?;
732 let table = container
733 .table
734 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
735 let model_name = struct_name.to_string();
736
737 let collected = collect_fields(named, &table)?;
738
739 // Validate that #[rustango(display = "…")] names a real field.
740 if let Some((ref display, span)) = container.display {
741 if !collected.field_names.iter().any(|n| n == display) {
742 return Err(syn::Error::new(
743 span,
744 format!("`display = \"{display}\"` does not match any field on this struct"),
745 ));
746 }
747 }
748 let display = container.display.map(|(name, _)| name);
749 let app_label = container.app.clone();
750
751 // Validate admin field-name lists against declared field names.
752 // Note: `list_display` is intentionally NOT validated here. As of
753 // v0.32 it may also reference inventory-registered computed
754 // fields (via `register_admin_computed!`) whose existence the
755 // macro can't see at compile time — they're submitted from any
756 // crate that depends on rustango. The runtime list-view resolves
757 // unknown names against the inventory + silently drops the
758 // truly-bogus ones, which is the cheaper trade-off versus
759 // forcing a per-Model attr to opt out.
760 if let Some(admin) = &container.admin {
761 for (label, list) in [
762 ("search_fields", &admin.search_fields),
763 ("readonly_fields", &admin.readonly_fields),
764 ("list_filter", &admin.list_filter),
765 ] {
766 if let Some((names, span)) = list {
767 for name in names {
768 if !collected.field_names.iter().any(|n| n == name) {
769 return Err(syn::Error::new(
770 *span,
771 format!(
772 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
773 ),
774 ));
775 }
776 }
777 }
778 }
779 if let Some((pairs, span)) = &admin.ordering {
780 for (name, _) in pairs {
781 if !collected.field_names.iter().any(|n| n == name) {
782 return Err(syn::Error::new(
783 *span,
784 format!(
785 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
786 ),
787 ));
788 }
789 }
790 }
791 if let Some((groups, span)) = &admin.fieldsets {
792 for (_, fields) in groups {
793 for name in fields {
794 if !collected.field_names.iter().any(|n| n == name) {
795 return Err(syn::Error::new(
796 *span,
797 format!(
798 "`fieldsets`: \"{name}\" is not a declared field on this struct"
799 ),
800 ));
801 }
802 }
803 }
804 }
805 }
806 if let Some(audit) = &container.audit {
807 if let Some((names, span)) = &audit.track {
808 for name in names {
809 if !collected.field_names.iter().any(|n| n == name) {
810 return Err(syn::Error::new(
811 *span,
812 format!(
813 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
814 ),
815 ));
816 }
817 }
818 }
819 }
820
821 // Issue #291 / T2.5 — validate each `default_order` column name
822 // against the model's collected fields. Typos fail at macro-expand
823 // time, not at the database.
824 for (col, _desc, span) in &container.default_order {
825 if !collected.field_names.iter().any(|n| n == col) {
826 return Err(syn::Error::new(
827 *span,
828 format!(
829 "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
830 ),
831 ));
832 }
833 }
834
835 // Build the audit_track list for ModelSchema: None when no audit attr,
836 // Some(empty) when audit present without track, Some(names) when explicit.
837 let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
838 audit
839 .track
840 .as_ref()
841 .map(|(names, _)| names.clone())
842 .unwrap_or_default()
843 });
844
845 // The upsert conflict target: a container-level composite unique,
846 // never a field `index(unique)` or a partial index PG can't target.
847 let upsert_unique: Option<Vec<String>> = container
848 .indexes
849 .iter()
850 .find(|i| i.unique && i.where_clause.is_none() && !i.columns.is_empty())
851 .map(|i| i.columns.clone());
852
853 // Merge field-level indexes into the container's index list.
854 let mut all_indexes: Vec<IndexAttr> = container.indexes;
855 for field in &named.named {
856 let ident = field.ident.as_ref().expect("named");
857 // The same default column as `process_field` (#1937).
858 let col = ident_name(ident);
859 // Re-parse field attrs to check for index flag
860 if let Ok(fa) = parse_field_attrs(field) {
861 if fa.index {
862 let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
863 let auto_name = capped_index_name(if fa.index_unique {
864 format!("{table}_{col_name}_uq_idx")
865 } else {
866 format!("{table}_{col_name}_idx")
867 });
868 all_indexes.push(IndexAttr {
869 name: fa.index_name.or(Some(auto_name)),
870 columns: vec![col_name],
871 unique: fa.index_unique,
872 method: fa.index_method,
873 where_clause: None,
874 include: Vec::new(),
875 });
876 }
877 }
878 }
879 // An unbounded String is LONGTEXT on MySQL, which no key can hold (1170, #2575).
880 for field in &named.named {
881 let (Ok(fa), Ok(det)) = (parse_field_attrs(field), detect_type(&field.ty)) else {
882 continue;
883 };
884 if det.kind != DetectedKind::String || fa.max_length.is_some() {
885 continue;
886 }
887 let col = fa
888 .column
889 .clone()
890 .unwrap_or_else(|| ident_name(field.ident.as_ref().expect("named")));
891 // MySQL indexes every foreign-key column too.
892 let relation = det.fk_inner.is_some() || fa.fk.is_some() || fa.o2o.is_some();
893 if fa.primary_key
894 || fa.unique
895 || relation
896 || all_indexes.iter().any(|i| i.columns.contains(&col))
897 {
898 return Err(syn::Error::new_spanned(
899 field,
900 "a primary-key, unique, indexed or foreign-key `String` needs \
901 `max_length`: MySQL cannot index an unbounded (LONGTEXT) column",
902 ));
903 }
904 }
905
906 let model_impl = model_impl_tokens(
907 struct_name,
908 &model_name,
909 &table,
910 display.as_deref(),
911 app_label.as_deref(),
912 container.admin.as_ref(),
913 &container.default_order,
914 &collected.field_schemas,
915 collected.soft_delete_column.as_deref(),
916 container.permissions,
917 audit_track_names.as_deref(),
918 &container.m2m,
919 &all_indexes,
920 &container.checks,
921 &container.excludes,
922 &container.composite_fks,
923 &container.generic_fks,
924 container.scope.as_deref(),
925 container.is_view,
926 container.verbose_name.as_deref(),
927 container.verbose_name_plural.as_deref(),
928 container.managed,
929 container.base_manager_name.as_deref(),
930 container.order_with_respect_to.as_deref(),
931 container.proxy,
932 &container.required_db_features,
933 container.required_db_vendor.as_deref(),
934 container.default_related_name.as_deref(),
935 container.db_table_comment.as_deref(),
936 container
937 .get_latest_by
938 .as_ref()
939 .map(|(c, d)| (c.as_str(), *d)),
940 &container.extra_permissions,
941 &container.default_permissions,
942 &container.global_scopes,
943 &container.reverse_has_relations,
944 &container.generic_has_relations,
945 );
946 let module_ident = column_module_ident(struct_name);
947 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
948 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
949 let track_set: Option<std::collections::HashSet<&str>> = audit
950 .track
951 .as_ref()
952 .map(|(names, _)| names.iter().map(String::as_str).collect());
953 collected
954 .column_entries
955 .iter()
956 .filter(|c| {
957 track_set
958 .as_ref()
959 .map_or(true, |s| s.contains(c.name.as_str()))
960 })
961 .collect()
962 });
963 let inherent_impl = inherent_impl_tokens(
964 struct_name,
965 &collected,
966 collected.primary_key.as_ref(),
967 &column_consts,
968 audited_fields.as_deref(),
969 upsert_unique.as_deref(),
970 &container.manager_fns,
971 );
972 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
973 let from_row_impl = from_row_impl_tokens(
974 struct_name,
975 &collected.from_row_inits,
976 &collected.from_row_inits_my,
977 );
978 let reverse_helpers = reverse_helper_tokens(
979 struct_name,
980 &collected.fk_relations,
981 container.default_related_name.as_deref(),
982 )?;
983 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
984 let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
985 // Issue #817 — `#[rustango(through(...))]` accessors.
986 let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
987 // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
988 let reverse_has_accessors =
989 reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
990 let generic_fk_accessors = generic_fk_accessor_tokens(
991 struct_name,
992 &container.generic_fks,
993 &collected.column_entries,
994 );
995
996 Ok(quote! {
997 #model_impl
998 #inherent_impl
999 #from_row_impl
1000 #column_module
1001 #reverse_helpers
1002 #m2m_accessors
1003 #generic_m2m_accessors
1004 #through_accessors
1005 #reverse_has_accessors
1006 #generic_fk_accessors
1007
1008 #root::core::inventory::submit! {
1009 // `module_path!()` evaluates at the registration site,
1010 // so a Model declared in `crate::blog::models` records
1011 // `"<crate>::blog::models"` and `resolved_app_label()`
1012 // can infer "blog" without an explicit attribute.
1013 #root::core::ModelEntry::new(
1014 <#struct_name as #root::core::Model>::SCHEMA,
1015 ::core::module_path!(),
1016 )
1017 .with_audited(
1018 <#struct_name as #root::core::Model>::__rustango_audited_update,
1019 <#struct_name as #root::core::Model>::__rustango_audited_delete,
1020 )
1021 .with_audited_create(<#struct_name as #root::core::Model>::__rustango_audited_create)
1022 .with_audited_update_record(
1023 <#struct_name as #root::core::Model>::__rustango_audited_update_record,
1024 )
1025 }
1026 })
1027}
1028
1029/// Guard before decoding a `select_related` target (#2293): a NULL nullable
1030/// FK leaves the relation unloaded; a set FK whose row is missing is an error.
1031fn join_guard_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1032 let root = rustango_root();
1033 let parent_ty = &rel.parent_type;
1034 let field_ident = &rel.field_ident;
1035 let null_fk = if rel.nullable {
1036 quote! {
1037 if #receiver.#field_ident.is_none() {
1038 return ::core::result::Result::Ok(false);
1039 }
1040 }
1041 } else {
1042 quote! {}
1043 };
1044 quote! {
1045 #null_fk
1046 #root::sql::__rustango_require_join(
1047 row, <#parent_ty as #root::core::Model>::SCHEMA, alias,
1048 )?;
1049 }
1050}
1051
1052/// A target an earlier chain already loaded (`a__b` before `a__c`) is
1053/// reused, and the deeper chain stitched into it; re-decoding it would
1054/// drop what that chain loaded.
1055fn loaded_reuse_tokens(
1056 rel: &FkRelation,
1057 receiver: &TokenStream2,
1058 load: &TokenStream2,
1059) -> TokenStream2 {
1060 let root = rustango_root();
1061 let field_ident = &rel.field_ident;
1062 let pattern = if rel.nullable {
1063 quote!(::core::option::Option::Some(#root::sql::ForeignKey::Loaded { value: __loaded, .. }))
1064 } else {
1065 quote!(#root::sql::ForeignKey::Loaded { value: __loaded, .. })
1066 };
1067 quote! {
1068 if let #pattern = &mut #receiver.#field_ident {
1069 if let ::core::option::Option::Some(__r) = __rest {
1070 let _ = #load(&mut **__loaded, row, __r, &__next_alias)?;
1071 }
1072 return ::core::result::Result::Ok(true);
1073 }
1074 }
1075}
1076
1077/// Store the decoded `_parent` on the FK field, keeping the key the row
1078/// holds: with `on = <unique column>` it is not the parent's PK.
1079fn loaded_assign_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1080 let root = rustango_root();
1081 let field_ident = &rel.field_ident;
1082 if rel.nullable {
1083 quote! {
1084 if let ::core::option::Option::Some(__fk) = &mut #receiver.#field_ident {
1085 let _pk = ::core::clone::Clone::clone(__fk.pk_ref());
1086 *__fk = #root::sql::ForeignKey::loaded(_pk, _parent);
1087 }
1088 }
1089 } else {
1090 quote! {
1091 let _pk = ::core::clone::Clone::clone(#receiver.#field_ident.pk_ref());
1092 #receiver.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1093 }
1094 }
1095}
1096
1097/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
1098/// matches `field_name` against the model's FK fields and, for a
1099/// match, decodes the FK target via the parent's macro-generated
1100/// `__rustango_from_aliased_row`, and stores `ForeignKey::Loaded` on
1101/// `self` under the key the row already holds.
1102///
1103/// Always emitted (with empty arms for FK-less models, which
1104/// return `Ok(false)` for any field name) so the `T: LoadRelated`
1105/// trait bound on `fetch_on` is universally satisfied — users
1106/// never have to think about implementing it.
1107fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1108 let root = rustango_root();
1109 let arms = fk_relations.iter().map(|rel| {
1110 let parent_ty = &rel.parent_type;
1111 let field_ident = &rel.field_ident;
1112 let fk_name = ident_name(field_ident);
1113 let fk_col = fk_name.as_str();
1114 let guard = join_guard_tokens(rel, "e!(self));
1115 let reuse = loaded_reuse_tokens(
1116 rel,
1117 "e!(self),
1118 "e!(#root::sql::LoadRelated::__rustango_load_related),
1119 );
1120 let assign = loaded_assign_tokens(rel, "e!(self));
1121 quote! {
1122 #fk_col => {
1123 #guard
1124 #reuse
1125 let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
1126 // Audit #451 — multi-hop `select_related("a__b__c")`:
1127 // stitch the deeper relation onto this parent first,
1128 // decoding it at the accumulated `__next_alias`. The
1129 // parent type also impls `LoadRelated`, so this recurses
1130 // the FK chain to arbitrary depth.
1131 if let ::core::option::Option::Some(__r) = __rest {
1132 let _ = #root::sql::LoadRelated::__rustango_load_related(
1133 &mut _parent, row, __r, &__next_alias,
1134 )?;
1135 }
1136 #assign
1137 ::core::result::Result::Ok(true)
1138 }
1139 }
1140 });
1141 quote! {
1142 #[cfg(feature = "postgres")]
1143 impl #root::sql::LoadRelated for #struct_name {
1144 #[allow(unused_variables)]
1145 fn __rustango_load_related(
1146 &mut self,
1147 row: &#root::sql::sqlx::postgres::PgRow,
1148 field_name: &str,
1149 alias: &str,
1150 ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1151 // Audit #451 — split the multi-hop path: `base` is the FK
1152 // on THIS model, `__rest` (if any) is the remaining chain
1153 // to stitch onto the loaded parent. `__next_alias` is the
1154 // accumulated join alias the parent's columns live under
1155 // (`{alias}__{next-hop}`), matching `lower_select_related`.
1156 let (__base, __rest): (&str, ::core::option::Option<&str>) =
1157 match field_name.split_once("__") {
1158 ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1159 ::core::option::Option::None => (field_name, ::core::option::Option::None),
1160 };
1161 let __next_alias: ::std::string::String = match __rest {
1162 ::core::option::Option::Some(__r) => {
1163 let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1164 ::std::format!("{}__{}", alias, __rb)
1165 }
1166 ::core::option::Option::None => ::std::string::String::new(),
1167 };
1168 match __base {
1169 #( #arms )*
1170 _ => ::core::result::Result::Ok(false),
1171 }
1172 }
1173 }
1174 }
1175}
1176
1177/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1178/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1179/// which expands to a `LoadRelatedMy` impl when rustango is built with
1180/// the `mysql` feature, and to nothing otherwise. The decoded parent
1181/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1182/// decoder, also batch8) so the dual emission is symmetric across
1183/// backends.
1184fn load_related_impl_my_tokens(
1185 struct_name: &syn::Ident,
1186 fk_relations: &[FkRelation],
1187) -> TokenStream2 {
1188 let root = rustango_root();
1189 let arms = fk_relations.iter().map(|rel| {
1190 let parent_ty = &rel.parent_type;
1191 let field_ident = &rel.field_ident;
1192 let fk_name = ident_name(field_ident);
1193 let fk_col = fk_name.as_str();
1194 let guard = join_guard_tokens(rel, "e!(__self));
1195 let reuse = loaded_reuse_tokens(
1196 rel,
1197 "e!(__self),
1198 "e!(#root::sql::LoadRelatedMy::__rustango_load_related_my),
1199 );
1200 let assign = loaded_assign_tokens(rel, "e!(__self));
1201 // `self` IS hygiene-tracked through macro_rules — emitted from
1202 // a different context than the `&mut self` parameter inside
1203 // the macro_rules-expanded fn. Pass it through as `__self`
1204 // and let the macro_rules rebind it to the receiver.
1205 quote! {
1206 #fk_col => {
1207 #guard
1208 #reuse
1209 let mut _parent: #parent_ty =
1210 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1211 // Audit #451 — multi-hop: stitch the deeper relation onto
1212 // the parent at the accumulated alias (see PG twin).
1213 if let ::core::option::Option::Some(__r) = __rest {
1214 let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1215 &mut _parent, row, __r, &__next_alias,
1216 )?;
1217 }
1218 #assign
1219 ::core::result::Result::Ok(true)
1220 }
1221 }
1222 });
1223 quote! {
1224 #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1225 #( #arms )*
1226 });
1227 }
1228}
1229
1230/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1231/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1232/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1233fn load_related_impl_sqlite_tokens(
1234 struct_name: &syn::Ident,
1235 fk_relations: &[FkRelation],
1236) -> TokenStream2 {
1237 let root = rustango_root();
1238 let arms = fk_relations.iter().map(|rel| {
1239 let parent_ty = &rel.parent_type;
1240 let field_ident = &rel.field_ident;
1241 let fk_name = ident_name(field_ident);
1242 let fk_col = fk_name.as_str();
1243 let guard = join_guard_tokens(rel, "e!(__self));
1244 let reuse = loaded_reuse_tokens(
1245 rel,
1246 "e!(__self),
1247 "e!(#root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite),
1248 );
1249 let assign = loaded_assign_tokens(rel, "e!(__self));
1250 quote! {
1251 #fk_col => {
1252 #guard
1253 #reuse
1254 let mut _parent: #parent_ty =
1255 <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1256 // Audit #451 — multi-hop: stitch the deeper relation onto
1257 // the parent at the accumulated alias (see PG twin).
1258 if let ::core::option::Option::Some(__r) = __rest {
1259 let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1260 &mut _parent, row, __r, &__next_alias,
1261 )?;
1262 }
1263 #assign
1264 ::core::result::Result::Ok(true)
1265 }
1266 }
1267 });
1268 quote! {
1269 #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1270 #( #arms )*
1271 });
1272 }
1273}
1274
1275/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1276/// matches `field_name` against the model's FK fields and returns
1277/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1278/// group children by parent PK.
1279///
1280/// Always emitted (with `_ => None` for FK-less models) so the
1281/// trait bound on `fetch_with_prefetch` is universally satisfied.
1282fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1283 let root = rustango_root();
1284 let arms = fk_relations.iter().map(|rel| {
1285 let field_ident = &rel.field_ident;
1286 let fk_name = ident_name(field_ident);
1287 let fk_col = fk_name.as_str();
1288 if rel.pk_kind == DetectedKind::I64 {
1289 // i64 FK — return the stored PK so prefetch_related can
1290 // group children by it. Nullable variant unwraps via
1291 // `as_ref().map(...)`: an unset (NULL) FK column yields
1292 // `None` and that child sits out of the grouping (correct
1293 // semantics — it has no parent to attach to).
1294 if rel.nullable {
1295 quote! {
1296 #fk_col => self.#field_ident
1297 .as_ref()
1298 .map(|fk| #root::sql::ForeignKey::pk(fk)),
1299 }
1300 } else {
1301 quote! {
1302 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1303 }
1304 }
1305 } else {
1306 // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1307 // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1308 // i64-keyed grouping path — the trait signature is
1309 // `Option<i64>` and a non-i64 PK can't lower into it.
1310 // The FK still works for everything else (CRUD, lazy
1311 // load via `.get()`, select_related JOINs); only the
1312 // bulk prefetch grouper needs the integer key.
1313 quote! {
1314 #fk_col => ::core::option::Option::None,
1315 }
1316 }
1317 });
1318 // PK-type-agnostic version: every FK arm emits an
1319 // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1320 // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1321 // opt OUT of the legacy i64 method (it returns None) but opt IN
1322 // here.
1323 let value_arms = fk_relations.iter().map(|rel| {
1324 let field_ident = &rel.field_ident;
1325 let fk_name = ident_name(field_ident);
1326 let fk_col = fk_name.as_str();
1327 if rel.nullable {
1328 quote! {
1329 #fk_col => self.#field_ident
1330 .as_ref()
1331 .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1332 #root::sql::ForeignKey::pk(fk)
1333 )),
1334 }
1335 } else {
1336 quote! {
1337 #fk_col => ::core::option::Option::Some(
1338 ::core::convert::Into::<#root::core::SqlValue>::into(
1339 self.#field_ident.pk()
1340 )
1341 ),
1342 }
1343 }
1344 });
1345 quote! {
1346 impl #root::sql::FkPkAccess for #struct_name {
1347 #[allow(unused_variables)]
1348 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1349 match field_name {
1350 #( #arms )*
1351 _ => ::core::option::Option::None,
1352 }
1353 }
1354 #[allow(unused_variables)]
1355 fn __rustango_fk_pk_value(
1356 &self,
1357 field_name: &str,
1358 ) -> ::core::option::Option<#root::core::SqlValue> {
1359 match field_name {
1360 #( #value_arms )*
1361 _ => ::core::option::Option::None,
1362 }
1363 }
1364 }
1365 }
1366}
1367
1368/// `related_name = "+"`: the FK gets no reverse accessor on its parent.
1369const NO_REVERSE_ACCESSOR: &str = "+";
1370
1371/// For every `ForeignKey<Parent>` field on `Child`, emit
1372/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1373/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
1374/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
1375/// — the canonical reverse-FK fetch. One round trip, no N+1.
1376///
1377/// **PG-only emission**: the accessor is bounded on
1378/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1379/// both of which are gated behind the `postgres` cargo feature. The
1380/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1381/// model derive itself compiles on tri-dialect / sqlite-only
1382/// downstream builds — the accessor just isn't materialised. A tri-
1383/// dialect `_set_pool` variant is a separate follow-up.
1384fn reverse_helper_tokens(
1385 child_ident: &syn::Ident,
1386 fk_relations: &[FkRelation],
1387 default_related_name: Option<&str>,
1388) -> syn::Result<TokenStream2> {
1389 let root = rustango_root();
1390 // `related_name = "+"`: no accessor, so a parent from another crate works.
1391 let fk_relations: Vec<&FkRelation> = fk_relations
1392 .iter()
1393 .filter(|rel| rel.related_name.as_deref() != Some(NO_REVERSE_ACCESSOR))
1394 .collect();
1395 if fk_relations.is_empty() {
1396 return Ok(TokenStream2::new());
1397 }
1398 // Method-name resolution per FK (issue #816 + follow-up):
1399 // 1. Field-level `#[rustango(related_name = "...")]` on the FK
1400 // itself — wins over everything else.
1401 // 2. Container-level `default_related_name = "..."` on the
1402 // child. Applies to every FK on this model that didn't
1403 // override.
1404 // 3. Fallback: `<child_snake>_set`. `Post` → `post_set`,
1405 // `BlogComment` → `blog_comment_set`. Avoids English-plural
1406 // edge cases.
1407 //
1408 // The PG-on-executor variant keeps the resolved name; the
1409 // tri-dialect `_pool` variant appends `_pool` to it (matches the
1410 // framework's convention for the `&Pool` flavor of every helper).
1411 let default_pg_suffix = default_related_name
1412 .map(str::to_owned)
1413 .unwrap_or_else(|| format!("{}_set", to_snake_case(&ident_name(child_ident))));
1414 let name_of = |rel: &FkRelation| {
1415 rel.related_name
1416 .clone()
1417 .unwrap_or_else(|| default_pg_suffix.clone())
1418 };
1419 // Two FKs to one parent would define the same method twice (E0592).
1420 for (i, rel) in fk_relations.iter().enumerate() {
1421 let parent_ty = &rel.parent_type;
1422 let parent = quote!(#parent_ty).to_string();
1423 let name = name_of(rel);
1424 if let Some(first) = fk_relations[..i].iter().find(|r| {
1425 let other = &r.parent_type;
1426 quote!(#other).to_string() == parent && name_of(r) == name
1427 }) {
1428 return Err(syn::Error::new_spanned(
1429 &rel.field_ident,
1430 format!(
1431 "`{}` and `{}` both point at `{parent}` and would both add `{parent}::{name}`; \
1432 give one `#[rustango(related_name = \"...\")]`, or `related_name = \"+\"` for none",
1433 first.field_ident, rel.field_ident
1434 ),
1435 ));
1436 }
1437 }
1438 let impls = fk_relations.iter().map(|rel| {
1439 let pg_suffix = name_of(rel);
1440 let pool_suffix = format!("{}_pool", pg_suffix);
1441 let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1442 let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1443 let parent_ty = &rel.parent_type;
1444 let fk_name = ident_name(&rel.field_ident);
1445 let fk_col = fk_name.as_str();
1446 let doc = format!(
1447 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1448 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1449 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1450 further `{child_ident}::objects()` filters via direct queryset use."
1451 );
1452 let pool_doc = format!(
1453 "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1454 [`#root::sql::Pool`] and dispatches per backend so the \
1455 reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1456 Use this from framework code that holds a `&Pool` (admin, \
1457 tenancy resolver, viewset handlers); reach for the executor- \
1458 bound variant when you already have a typed `sqlx::Executor`."
1459 );
1460 quote! {
1461 #[cfg(feature = "postgres")]
1462 impl #parent_ty {
1463 #[doc = #doc]
1464 ///
1465 /// # Errors
1466 /// Returns [`#root::sql::ExecError`] for SQL-writing
1467 /// or driver failures.
1468 pub async fn #pg_method_ident<'_c, _E>(
1469 &self,
1470 _executor: _E,
1471 ) -> ::core::result::Result<
1472 ::std::vec::Vec<#child_ident>,
1473 #root::sql::ExecError,
1474 >
1475 where
1476 _E: #root::sql::sqlx::Executor<
1477 '_c,
1478 Database = #root::sql::sqlx::Postgres,
1479 >,
1480 {
1481 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1482 #root::query::QuerySet::<#child_ident>::new()
1483 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1484 .fetch_on(_executor)
1485 .await
1486 }
1487 }
1488
1489 impl #parent_ty {
1490 #[doc = #pool_doc]
1491 ///
1492 /// # Errors
1493 /// Returns [`#root::sql::ExecError`] for SQL-writing
1494 /// or driver failures.
1495 pub async fn #pool_method_ident(
1496 &self,
1497 pool: &#root::sql::Pool,
1498 ) -> ::core::result::Result<
1499 ::std::vec::Vec<#child_ident>,
1500 #root::sql::ExecError,
1501 > {
1502 use #root::sql::FetcherPool as _;
1503 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1504 #root::query::QuerySet::<#child_ident>::new()
1505 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1506 .fetch(pool)
1507 .await
1508 }
1509 }
1510 }
1511 });
1512 Ok(quote! { #( #impls )* })
1513}
1514
1515/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1516/// relation declared on the model.
1517/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1518/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1519///
1520/// Closes #239 + #240 — `comment.content_object` /
1521/// `comment.content_object = post` ergonomics on top of the existing
1522/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1523///
1524/// `column_entries` is passed so we can resolve each `ct_column` /
1525/// `pk_column` SQL name back to its Rust field ident — the macro
1526/// only sees the column-side strings in the attribute, but the
1527/// emitted accessor needs to read the actual struct field.
1528fn generic_fk_accessor_tokens(
1529 struct_name: &syn::Ident,
1530 generic_fks: &[GenericFkAttr],
1531 column_entries: &[ColumnEntry],
1532) -> TokenStream2 {
1533 let root = rustango_root();
1534 if generic_fks.is_empty() {
1535 return TokenStream2::new();
1536 }
1537 let methods = generic_fks.iter().filter_map(|gfk| {
1538 // Resolve `ct_column` + `pk_column` to the struct's Rust
1539 // field idents. A typo (column name doesn't match any field)
1540 // emits no method for that registration — the user will see
1541 // the compiler reject the SCHEMA literal anyway, so there's
1542 // a clear error path without us double-reporting.
1543 let ct_ident = column_entries
1544 .iter()
1545 .find(|c| c.column == gfk.ct_column)
1546 .map(|c| c.ident.clone())?;
1547 let pk_ident = column_entries
1548 .iter()
1549 .find(|c| c.column == gfk.pk_column)
1550 .map(|c| c.ident.clone())?;
1551
1552 let accessor_ident =
1553 syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1554 let setter_ident =
1555 syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1556 let name_literal = gfk.name.as_str();
1557
1558 Some(quote! {
1559 #[doc = concat!(
1560 "Resolve the polymorphic `",
1561 #name_literal,
1562 "` relation. Reads `self.",
1563 stringify!(#ct_ident),
1564 "` + `self.",
1565 stringify!(#pk_ident),
1566 "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1567 "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1568 #name_literal,
1569 "\", ...))]`."
1570 )]
1571 pub async fn #accessor_ident(
1572 &self,
1573 pool: &#root::sql::Pool,
1574 ) -> ::core::result::Result<
1575 ::core::option::Option<#root::__serde_json::Value>,
1576 #root::sql::ExecError,
1577 > {
1578 let gfk = #root::contenttypes::GenericForeignKey::new(
1579 self.#ct_ident as i64,
1580 self.#pk_ident as i64,
1581 );
1582 gfk.get_object(pool).await
1583 }
1584
1585 #[doc = concat!(
1586 "Set the polymorphic `",
1587 #name_literal,
1588 "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1589 stringify!(#ct_ident),
1590 "` and `self.",
1591 stringify!(#pk_ident),
1592 "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1593 #name_literal,
1594 "\", ...))]`."
1595 )]
1596 pub async fn #setter_ident<T: #root::core::Model>(
1597 &mut self,
1598 pool: &#root::sql::Pool,
1599 target_pk: i64,
1600 ) -> ::core::result::Result<(), #root::sql::ExecError> {
1601 let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1602 pool,
1603 target_pk,
1604 ).await?;
1605 self.#ct_ident = gfk.content_type_id as _;
1606 self.#pk_ident = gfk.object_pk as _;
1607 ::core::result::Result::Ok(())
1608 }
1609 })
1610 });
1611 quote! {
1612 impl #struct_name {
1613 #( #methods )*
1614 }
1615 }
1616}
1617
1618fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1619 let root = rustango_root();
1620 if m2m_relations.is_empty() {
1621 return TokenStream2::new();
1622 }
1623 let methods = m2m_relations.iter().map(|rel| {
1624 let method_name = format!("{}_m2m", rel.name);
1625 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1626 let through = rel.through.as_str();
1627 let src_col = rel.src.as_str();
1628 let dst_col = rel.dst.as_str();
1629 quote! {
1630 pub fn #method_ident(&self) -> #root::sql::M2MManager {
1631 #root::sql::M2MManager {
1632 src_pk: self.__rustango_pk_value(),
1633 through: #through,
1634 src_col: #src_col,
1635 dst_col: #dst_col,
1636 }
1637 }
1638 }
1639 });
1640 quote! {
1641 impl #struct_name {
1642 #( #methods )*
1643 }
1644 }
1645}
1646
1647/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1648/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1649fn generic_m2m_accessor_tokens(
1650 struct_name: &syn::Ident,
1651 relations: &[GenericM2MAttr],
1652) -> TokenStream2 {
1653 let root = rustango_root();
1654 if relations.is_empty() {
1655 return TokenStream2::new();
1656 }
1657 let methods = relations.iter().map(|rel| {
1658 let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1659 let through = rel.through.as_str();
1660 let pk_col = rel.pk_column.as_str();
1661 let ct_col = rel.ct_column.as_str();
1662 let related_col = rel.related_column.as_str();
1663 quote! {
1664 pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1665 #root::sql::GenericM2MManager {
1666 src_pk: self.__rustango_pk_value(),
1667 src_schema: <Self as #root::core::Model>::SCHEMA,
1668 through: #through,
1669 pk_col: #pk_col,
1670 ct_col: #ct_col,
1671 dst_col: #related_col,
1672 }
1673 }
1674 }
1675 });
1676 // Lets an owner delete find and clear its junction rows.
1677 let entries = relations.iter().map(|rel| {
1678 let through = rel.through.as_str();
1679 let pk_col = rel.pk_column.as_str();
1680 let ct_col = rel.ct_column.as_str();
1681 quote! {
1682 #root::core::inventory::submit! {
1683 #root::core::GenericM2MEntry::new(
1684 <#struct_name as #root::core::Model>::SCHEMA,
1685 #through,
1686 #pk_col,
1687 #ct_col,
1688 )
1689 }
1690 }
1691 });
1692 quote! {
1693 impl #struct_name {
1694 #( #methods )*
1695 }
1696 #( #entries )*
1697 }
1698}
1699
1700/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1701/// associated functions for each `#[rustango(reverse_has(...))]`
1702/// attribute. Issue #830.
1703///
1704/// The two emitted functions return ready-to-use `WhereExpr` nodes
1705/// that downstream callers drop into
1706/// `QuerySet::<Self>::where_raw(...)`:
1707///
1708/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1709/// <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1710/// `whereHas` parity (without the closure-style sub-predicate
1711/// refinement — that's a follow-up; users can layer additional
1712/// predicates by constructing the SelectQuery themselves and
1713/// calling `where_raw(exists(query))` from `crate::core::subquery`).
1714/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1715/// `whereDoesntHave` parity.
1716///
1717/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1718/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1719/// machinery threads the outer-table reference through automatically
1720/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1721fn reverse_has_accessor_tokens(
1722 struct_name: &syn::Ident,
1723 reverse_has_relations: &[ReverseHasAttr],
1724) -> TokenStream2 {
1725 let root = rustango_root();
1726 if reverse_has_relations.is_empty() {
1727 return TokenStream2::new();
1728 }
1729 let methods = reverse_has_relations.iter().map(|rel| {
1730 let exists_name = format!("{}_exists_expr", rel.name);
1731 let not_exists_name = format!("{}_not_exists_expr", rel.name);
1732 let count_name = format!("{}_count", rel.name);
1733 let fetch_name = format!("{}_fetch", rel.name);
1734 let first_name = format!("{}_first", rel.name);
1735 let pluck_name = format!("{}_pluck", rel.name);
1736 let accessor_name = rel.name.as_str();
1737 let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1738 let not_exists_ident = syn::Ident::new(¬_exists_name, struct_name.span());
1739 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1740 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1741 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1742 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1743 let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1744 let child = &rel.child;
1745 let child_fk_column = rel.child_fk_column.as_str();
1746 let self_pk_column = rel.self_pk_column.as_str();
1747 let exists_doc = format!(
1748 "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1749 FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1750 Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1751 filter to {struct_name}s with at least one matching child.",
1752 );
1753 let not_exists_doc = format!(
1754 "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1755 (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1756 <outer>.{self_pk_column})`. Drop into \
1757 `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1758 {struct_name}s with **no** matching child.",
1759 );
1760 let count_doc = format!(
1761 "Eloquent `$model->{name}->count()` analog — returns \
1762 the number of `{child}` rows whose `{child_fk_column}` \
1763 matches this `{struct_name}` instance's primary key. \
1764 Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1765 {child_fk_column} = <self.pk>`.",
1766 name = rel.name,
1767 );
1768 let accessor_doc = format!(
1769 "Eloquent `$model->{name}` accessor — returns a \
1770 `QuerySet<{child}>` filtered to rows whose \
1771 `{child_fk_column}` matches this `{struct_name}` \
1772 instance's primary key. **Chainable**: compose `.filter()` \
1773 / `.order_by()` / `.limit()` etc. on top, then call \
1774 `.fetch(&pool)` (the QuerySet trait method) when \
1775 done. For the simple \"fetch all\" hot path with no \
1776 further composition, prefer the bare-name \
1777 `{name}_fetch(&pool)` companion.",
1778 name = rel.name,
1779 );
1780 let fetch_doc = format!(
1781 "Eloquent `$model->{name}->get()` — bare-name hot-path \
1782 over `{name}(&self).fetch(&pool)`. Use this when \
1783 you don't need further `.filter()` / `.order_by()` \
1784 composition; falls back to the chainable accessor when \
1785 you do. Avoids the `_pool` suffix on the most common \
1786 call-site shape.",
1787 name = rel.name,
1788 );
1789 quote! {
1790 #[doc = #accessor_doc]
1791 pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1792 #root::query::QuerySet::<#child>::new()
1793 .filter(#child_fk_column, self.__rustango_pk_value())
1794 }
1795
1796 #[doc = #fetch_doc]
1797 pub async fn #fetch_ident(
1798 &self,
1799 pool: &#root::sql::Pool,
1800 ) -> ::core::result::Result<
1801 ::std::vec::Vec<#child>,
1802 #root::sql::ExecError,
1803 > {
1804 use #root::sql::FetcherPool as _;
1805 self.#accessor_ident().fetch(pool).await
1806 }
1807
1808 /// Eloquent `$model->relation->first()` / `hasOne`
1809 /// semantics — bare-name shortcut over
1810 /// `self.<name>().first(&pool)`. Returns `None` when no
1811 /// child rows match. Useful when the relation is
1812 /// nominally many-to-one in shape but at most one row is
1813 /// expected (latest comment, primary tag, etc.).
1814 pub async fn #first_ident(
1815 &self,
1816 pool: &#root::sql::Pool,
1817 ) -> ::core::result::Result<
1818 ::core::option::Option<#child>,
1819 #root::sql::ExecError,
1820 > {
1821 self.#accessor_ident().first(pool).await
1822 }
1823
1824 /// Eloquent `$model->relation->pluck($col)` — project a
1825 /// single column from the child rows into a `Vec<U>`.
1826 /// Skips the typed `Child` decode, which is cheaper when
1827 /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1828 pub async fn #pluck_ident<U>(
1829 &self,
1830 col: &'static str,
1831 pool: &#root::sql::Pool,
1832 ) -> ::core::result::Result<
1833 ::std::vec::Vec<U>,
1834 #root::sql::ExecError,
1835 >
1836 where
1837 U: #root::sql::FlatScalar,
1838 {
1839 self.#accessor_ident()
1840 .values_list_flat(col)
1841 .fetch::<U>(pool)
1842 .await
1843 }
1844
1845 #[doc = #exists_doc]
1846 pub fn #exists_ident() -> #root::core::WhereExpr {
1847 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1848 let child_schema =
1849 <#child as #root::core::Model>::SCHEMA;
1850 let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1851 lhs: Expr::Column(#child_fk_column),
1852 op: Op::Eq,
1853 rhs: Expr::OuterRef(#self_pk_column),
1854 });
1855 WhereExpr::Exists(::std::boxed::Box::new(inner))
1856 }
1857
1858 #[doc = #not_exists_doc]
1859 pub fn #not_exists_ident() -> #root::core::WhereExpr {
1860 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1861 let child_schema =
1862 <#child as #root::core::Model>::SCHEMA;
1863 let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1864 lhs: Expr::Column(#child_fk_column),
1865 op: Op::Eq,
1866 rhs: Expr::OuterRef(#self_pk_column),
1867 });
1868 WhereExpr::NotExists(::std::boxed::Box::new(inner))
1869 }
1870
1871 #[doc = #count_doc]
1872 pub async fn #count_ident(
1873 &self,
1874 pool: &#root::sql::Pool,
1875 ) -> ::core::result::Result<
1876 i64,
1877 #root::sql::ExecError,
1878 > {
1879 use #root::sql::CounterPool as _;
1880 #root::query::QuerySet::<#child>::new()
1881 .filter(#child_fk_column, self.__rustango_pk_value())
1882 .count(pool)
1883 .await
1884 }
1885 }
1886 });
1887 quote! {
1888 impl #struct_name {
1889 #( #methods )*
1890 }
1891 }
1892}
1893
1894/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1895/// `#[rustango(through(...))]` attribute. Issue #817.
1896///
1897/// Each accessor builds a correlated subquery via
1898/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1899/// intermediate table, filters on the FK column pointing at this
1900/// model, and projects the intermediate PK. The outer queryset
1901/// filters the far model by its FK-to-intermediate column being in
1902/// that set.
1903///
1904/// The returned `QuerySet<Far>` is **chainable** — the subquery
1905/// lives inside a `where_raw` clause so the user's later
1906/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1907/// disturb it.
1908///
1909/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1910/// SQLite — no LATERAL or backend-specific syntax involved.
1911fn through_accessor_tokens(
1912 struct_name: &syn::Ident,
1913 through_relations: &[ThroughAttr],
1914) -> TokenStream2 {
1915 let root = rustango_root();
1916 if through_relations.is_empty() {
1917 return TokenStream2::new();
1918 }
1919 let methods = through_relations.iter().map(|rel| {
1920 let method_name = format!("{}_through", rel.name);
1921 let count_name = format!("{}_through_count", rel.name);
1922 let fetch_name = format!("{}_through_fetch", rel.name);
1923 let first_name = format!("{}_through_first", rel.name);
1924 let pluck_name = format!("{}_through_pluck", rel.name);
1925 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1926 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1927 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1928 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1929 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1930 let far = &rel.far;
1931 let intermediate = &rel.intermediate;
1932 let far_fk_column = rel.far_fk_column.as_str();
1933 let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1934 let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1935 let doc = format!(
1936 "Eloquent `hasManyThrough` accessor — returns a \
1937 `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1938 instance through the intermediate `{intermediate}` table. \
1939 Generated SQL shape: \
1940 `… WHERE {far_fk_column} IN (SELECT \
1941 {intermediate_pk_column} FROM <{intermediate}> WHERE \
1942 {intermediate_fk_column} = self.pk)`. Chainable like any \
1943 other QuerySet — compose `.filter()` / `.order_by()` / \
1944 `.limit()` etc. on top.",
1945 );
1946 let count_doc = format!(
1947 "Eloquent `$model->{name}->count()` analog for the \
1948 through-relation — returns the number of `{far}` rows \
1949 reachable through `{intermediate}`. Equivalent to \
1950 `self.{name}_through().count(pool)` but spelled \
1951 as a bare instance method for parity with the \
1952 `reverse_has` `<name>_count` shape.",
1953 name = rel.name,
1954 );
1955 quote! {
1956 #[doc = #doc]
1957 pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1958 use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1959 let intermediate_schema =
1960 <#intermediate as #root::core::Model>::SCHEMA;
1961 let sub = SelectQuery::new(intermediate_schema)
1962 .where_clause(WhereExpr::Predicate(Filter::new(
1963 #intermediate_fk_column,
1964 Op::Eq,
1965 self.__rustango_pk_value(),
1966 )))
1967 .projection(::std::vec![#intermediate_pk_column]);
1968 #root::query::QuerySet::<#far>::new().where_raw(
1969 WhereExpr::InSubquery {
1970 column: #far_fk_column,
1971 negated: false,
1972 subquery: ::std::boxed::Box::new(sub),
1973 },
1974 )
1975 }
1976
1977 #[doc = #count_doc]
1978 pub async fn #count_ident(
1979 &self,
1980 pool: &#root::sql::Pool,
1981 ) -> ::core::result::Result<
1982 i64,
1983 #root::sql::ExecError,
1984 > {
1985 use #root::sql::CounterPool as _;
1986 self.#method_ident().count(pool).await
1987 }
1988
1989 /// Eloquent `$model->relation->get()` for the
1990 /// through-relation — bare-name hot-path over
1991 /// `self.<name>_through().fetch(&pool)`. Use this
1992 /// when you don't need further `.filter()` /
1993 /// `.order_by()` composition; falls back to the
1994 /// chainable accessor when you do. Avoids the `_pool`
1995 /// suffix on the most common call-site shape.
1996 pub async fn #fetch_ident(
1997 &self,
1998 pool: &#root::sql::Pool,
1999 ) -> ::core::result::Result<
2000 ::std::vec::Vec<#far>,
2001 #root::sql::ExecError,
2002 > {
2003 use #root::sql::FetcherPool as _;
2004 self.#method_ident().fetch(pool).await
2005 }
2006
2007 /// Eloquent `hasOneThrough` analog — bare-name shortcut
2008 /// over `self.<name>_through().first(&pool)`. Returns
2009 /// `None` when no far rows are reachable through the
2010 /// intermediate. Useful when at most one row is
2011 /// expected (latest comment by country, primary tag,
2012 /// etc.).
2013 pub async fn #first_ident(
2014 &self,
2015 pool: &#root::sql::Pool,
2016 ) -> ::core::result::Result<
2017 ::core::option::Option<#far>,
2018 #root::sql::ExecError,
2019 > {
2020 self.#method_ident().first(pool).await
2021 }
2022
2023 /// Pluck a single column from the far rows into `Vec<U>`
2024 /// — cheaper than the typed `<Far>` decode when only one
2025 /// scalar column is needed.
2026 pub async fn #pluck_ident<U>(
2027 &self,
2028 col: &'static str,
2029 pool: &#root::sql::Pool,
2030 ) -> ::core::result::Result<
2031 ::std::vec::Vec<U>,
2032 #root::sql::ExecError,
2033 >
2034 where
2035 U: #root::sql::FlatScalar,
2036 {
2037 self.#method_ident()
2038 .values_list_flat(col)
2039 .fetch::<U>(pool)
2040 .await
2041 }
2042 }
2043 });
2044 quote! {
2045 impl #struct_name {
2046 #( #methods )*
2047 }
2048 }
2049}
2050
2051struct ColumnEntry {
2052 /// The struct field ident, used both for the inherent const name on
2053 /// the model and for the inner column type's name.
2054 ident: syn::Ident,
2055 /// The struct's field type, used as `Column::Value`.
2056 value_ty: Type,
2057 /// Rust-side field name (e.g. `"id"`).
2058 name: String,
2059 /// SQL-side column name (e.g. `"user_id"`).
2060 column: String,
2061 /// `#root::core::FieldType::I64` etc.
2062 field_type_tokens: TokenStream2,
2063 /// A bare or optional `Uuid` field; MySQL decodes it from text.
2064 uuid_column: bool,
2065}
2066
2067struct CollectedFields {
2068 field_schemas: Vec<TokenStream2>,
2069 from_row_inits: Vec<TokenStream2>,
2070 /// Aliased counterparts of `from_row_inits` — read columns via
2071 /// `format!("{prefix}__{col}")` aliases so a Model can be
2072 /// decoded from a JOINed row's projected target columns.
2073 from_aliased_row_inits: Vec<TokenStream2>,
2074 /// MySQL twins of `from_row_inits` / `from_aliased_row_inits`.
2075 from_row_inits_my: Vec<TokenStream2>,
2076 from_aliased_row_inits_my: Vec<TokenStream2>,
2077 /// Static column-name list — used by the simple insert path
2078 /// (no `Auto<T>` fields). Aligned with `insert_values`.
2079 insert_columns: Vec<TokenStream2>,
2080 /// Static `Into<SqlValue>` expressions, one per field. Aligned
2081 /// with `insert_columns`. Used by the simple insert path only.
2082 insert_values: Vec<TokenStream2>,
2083 /// Per-field push expressions for the dynamic (Auto-aware)
2084 /// insert path. Each statement either unconditionally pushes
2085 /// `(column, value)` or, for an `Auto<T>` field, conditionally
2086 /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
2087 insert_pushes: Vec<TokenStream2>,
2088 /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
2089 /// when `has_auto == false`.
2090 returning_cols: Vec<TokenStream2>,
2091 /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
2092 /// field. Run after `insert_returning` to populate the model.
2093 auto_assigns: Vec<TokenStream2>,
2094 /// `(ident, column_literal, is_uuid)` for every Auto field. Used by
2095 /// the bulk_insert codegen to rebuild assigns against `_row_mut`
2096 /// instead of `self`.
2097 auto_field_idents: Vec<(syn::Ident, String, bool)>,
2098 /// #1028 — `(ident, column)` for each `generated_as` field. Drives
2099 /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
2100 /// back into the struct after insert (MySQL has no RETURNING → the
2101 /// field stays at its placeholder until the next read).
2102 generated_field_idents: Vec<(syn::Ident, String)>,
2103 /// First `Auto<T>` field the database fills, with its inner `T`: the
2104 /// MySQL `LAST_INSERT_ID()` target. Rust-filled ones never are (#1934).
2105 first_db_auto: Option<(syn::Ident, Type)>,
2106 /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2107 /// by the all-Auto-Unset bulk path (Auto cols dropped from
2108 /// `columns`).
2109 bulk_pushes_no_auto: Vec<TokenStream2>,
2110 /// Bulk-insert per-row pushes for **all fields including Auto**.
2111 /// Used by the all-Auto-Set bulk path (Auto col included with the
2112 /// caller-supplied value).
2113 bulk_pushes_all: Vec<TokenStream2>,
2114 /// Column-name literals for non-Auto fields only (paired with
2115 /// `bulk_pushes_no_auto`).
2116 bulk_columns_no_auto: Vec<TokenStream2>,
2117 /// Column-name literals for every field including Auto (paired
2118 /// with `bulk_pushes_all`).
2119 bulk_columns_all: Vec<TokenStream2>,
2120 /// Fills each `default_uuid_v7` field of `_row` in place, so a
2121 /// mutable bulk insert hands the PKs back (#1934).
2122 bulk_fill_in_place: Vec<TokenStream2>,
2123 /// Rust-filled `Auto` fields (`default_uuid_v7`, `auto_now(_add)`), reset when `insert_or_ignore` skips (#1937).
2124 rust_filled_idents: Vec<syn::Ident>,
2125 /// `auto_now` fields: `upsert` stamps them before binding.
2126 auto_now_idents: Vec<syn::Ident>,
2127 /// `default_uuid_v7` fields: an `Unset` one gets a fresh id, so the
2128 /// upsert marks it `generated` for the MySQL key guard.
2129 uuid_v7_fields: Vec<(syn::Ident, String)>,
2130 /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2131 /// + the loop that asserts every row matches. One pair per Auto
2132 /// field. Empty when `has_auto == false`.
2133 bulk_auto_uniformity: Vec<TokenStream2>,
2134 /// Identifier of the first Auto field, used as the witness for
2135 /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2136 first_auto_ident: Option<syn::Ident>,
2137 /// `true` if any field on the struct is `Auto<T>`.
2138 has_auto: bool,
2139 /// `true` when the primary-key field's Rust type is `Auto<T>`.
2140 /// Gates `save()` codegen — only Auto PKs let us infer
2141 /// insert-vs-update from the in-memory value.
2142 pk_is_auto: bool,
2143 /// `Assignment` constructors for every non-PK column. Drives the
2144 /// UPDATE branch of `save()`.
2145 update_assignments: Vec<TokenStream2>,
2146 /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2147 /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2148 upsert_update_columns: Vec<TokenStream2>,
2149 primary_key: Option<(syn::Ident, String)>,
2150 column_entries: Vec<ColumnEntry>,
2151 /// Rust-side field names, in declaration order. Used to validate
2152 /// container attributes like `display = "…"`.
2153 field_names: Vec<String>,
2154 /// FK fields on this child model. Drives the reverse-relation
2155 /// helper emit — for each FK, the macro adds an inherent
2156 /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2157 /// method on the parent type.
2158 fk_relations: Vec<FkRelation>,
2159 /// SQL column name of the `#[rustango(soft_delete)]` field, if
2160 /// the model has one. Drives emission of the `soft_delete_on` /
2161 /// `restore_on` inherent methods. At most one such column per
2162 /// model is allowed; collect_fields rejects duplicates.
2163 soft_delete_column: Option<String>,
2164 /// Rust field ident of the `#[rustango(soft_delete)]` field —
2165 /// companion to `soft_delete_column` for emitting predicates
2166 /// that need to read the field off `&self` (e.g. `trashed()`).
2167 soft_delete_field_ident: Option<syn::Ident>,
2168}
2169
2170#[derive(Clone)]
2171struct FkRelation {
2172 /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2173 /// helper is emitted as `impl <ParentType> { … }`.
2174 parent_type: Type,
2175 /// Rust field ident of the FK. Its name is the key for select_related,
2176 /// prefetch and filters; the SQL column may differ (#1936).
2177 field_ident: syn::Ident,
2178 /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2179 /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2180 /// default `ForeignKey<T>` (no explicit K); other kinds when the
2181 /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2182 pk_kind: DetectedKind,
2183 /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2184 /// FK column). Drives the `Some(...)` wrapping in load_related
2185 /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2186 /// the codegen matches the field's declared shape.
2187 nullable: bool,
2188 /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2189 /// override. When set, the reverse helper picks this name instead
2190 /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2191 /// #816 (issue's "Related" note re: per-FK override).
2192 related_name: Option<String>,
2193}
2194
2195fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2196 let root = rustango_root();
2197 let cap = named.named.len();
2198 let mut out = CollectedFields {
2199 field_schemas: Vec::with_capacity(cap),
2200 from_row_inits: Vec::with_capacity(cap),
2201 from_aliased_row_inits: Vec::with_capacity(cap),
2202 from_row_inits_my: Vec::with_capacity(cap),
2203 from_aliased_row_inits_my: Vec::with_capacity(cap),
2204 insert_columns: Vec::with_capacity(cap),
2205 insert_values: Vec::with_capacity(cap),
2206 insert_pushes: Vec::with_capacity(cap),
2207 returning_cols: Vec::new(),
2208 auto_assigns: Vec::new(),
2209 auto_field_idents: Vec::new(),
2210 generated_field_idents: Vec::new(),
2211 first_db_auto: None,
2212 bulk_pushes_no_auto: Vec::with_capacity(cap),
2213 bulk_pushes_all: Vec::with_capacity(cap),
2214 bulk_columns_no_auto: Vec::with_capacity(cap),
2215 bulk_columns_all: Vec::with_capacity(cap),
2216 bulk_fill_in_place: Vec::new(),
2217 rust_filled_idents: Vec::new(),
2218 auto_now_idents: Vec::new(),
2219 uuid_v7_fields: Vec::new(),
2220 bulk_auto_uniformity: Vec::new(),
2221 first_auto_ident: None,
2222 has_auto: false,
2223 pk_is_auto: false,
2224 update_assignments: Vec::with_capacity(cap),
2225 upsert_update_columns: Vec::with_capacity(cap),
2226 primary_key: None,
2227 column_entries: Vec::with_capacity(cap),
2228 field_names: Vec::with_capacity(cap),
2229 fk_relations: Vec::new(),
2230 soft_delete_column: None,
2231 soft_delete_field_ident: None,
2232 };
2233
2234 for field in &named.named {
2235 let info = process_field(field, table)?;
2236 out.field_names.push(ident_name(info.ident));
2237 out.field_schemas.push(info.schema);
2238 out.from_row_inits.push(info.from_row_init);
2239 out.from_aliased_row_inits.push(info.from_aliased_row_init);
2240 out.from_row_inits_my.push(info.from_row_init_my);
2241 out.from_aliased_row_inits_my
2242 .push(info.from_aliased_row_init_my);
2243 if let Some(parent_ty) = info.fk_inner.clone() {
2244 out.fk_relations.push(FkRelation {
2245 parent_type: parent_ty,
2246 field_ident: info.ident.clone(),
2247 pk_kind: info.fk_pk_kind,
2248 nullable: info.nullable,
2249 related_name: info.related_name.clone(),
2250 });
2251 }
2252 if info.soft_delete {
2253 if out.soft_delete_column.is_some() {
2254 return Err(syn::Error::new_spanned(
2255 field,
2256 "only one field may be marked `#[rustango(soft_delete)]`",
2257 ));
2258 }
2259 out.soft_delete_column = Some(info.column.clone());
2260 out.soft_delete_field_ident = Some(info.ident.clone());
2261 }
2262 let column = info.column.as_str();
2263 let ident = info.ident;
2264 // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2265 // skip every write path — Postgres recomputes the value
2266 // from EXPR. Push only the column-entry record (so typed
2267 // column constants still exist for filtering / projection)
2268 // and the schema literal (already pushed above) and move
2269 // on. No insert_columns/values, no insert_pushes, no
2270 // bulk_*, no update_assignments, no upsert_update_columns,
2271 // no returning_cols.
2272 if info.generated_as.is_some() {
2273 out.column_entries.push(ColumnEntry {
2274 ident: ident.clone(),
2275 value_ty: info.value_ty.clone(),
2276 name: ident_name(ident),
2277 column: info.column.clone(),
2278 field_type_tokens: info.field_type_tokens,
2279 uuid_column: info.uuid_column,
2280 });
2281 // #1028 — refresh the DB-computed value after insert on
2282 // RETURNING-capable backends. The column joins `returning_cols`
2283 // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2284 // ident is recorded so the `AssignAutoPkPool` impl decodes it
2285 // back into the struct. MySQL has no `INSERT … RETURNING`, so
2286 // it keeps the placeholder until the next read.
2287 out.returning_cols.push(quote!(#column));
2288 out.generated_field_idents
2289 .push((ident.clone(), info.column.clone()));
2290 continue;
2291 }
2292 out.insert_columns.push(quote!(#column));
2293 out.insert_values.push(quote! {
2294 ::core::convert::Into::<#root::core::SqlValue>::into(
2295 ::core::clone::Clone::clone(&self.#ident)
2296 )
2297 });
2298 if info.auto {
2299 out.has_auto = true;
2300 if out.first_auto_ident.is_none() {
2301 out.first_auto_ident = Some(ident.clone());
2302 }
2303 // `default_uuid_v7` (issue #823) generates the PK Rust-side
2304 // before binding, so the value is already in
2305 // `self.#ident` after the insert_push — RETURNING is
2306 // redundant. Skip adding this column to returning_cols /
2307 // auto_assigns / auto_field_idents to avoid (a) an
2308 // unnecessary RETURNING column on every dialect, and (b)
2309 // the MySQL `LAST_INSERT_ID()` path that can only fill an
2310 // integer PK.
2311 // `auto_now_add` / `auto_now` join `default_uuid_v7` here
2312 // for the same reason: they are filled Rust-side below and
2313 // bound, so the value is already in `self` and RETURNING
2314 // would be a redundant column on every dialect.
2315 if !info.default_uuid_v7 && !info.auto_now_add && !info.auto_now {
2316 if out.first_db_auto.is_none() {
2317 out.first_db_auto = auto_inner_type(info.value_ty)
2318 .cloned()
2319 .map(|ty| (ident.clone(), ty));
2320 }
2321 out.returning_cols.push(quote!(#column));
2322 out.auto_field_idents.push((
2323 ident.clone(),
2324 info.column.clone(),
2325 info.fk_pk_kind == DetectedKind::Uuid,
2326 ));
2327 out.auto_assigns.push(quote! {
2328 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2329 });
2330 }
2331 if info.default_uuid_v7 {
2332 // Rust-side UUIDv7 generation (issue #823, Eloquent
2333 // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2334 // and bind; Auto::Set → bind the user's value. The
2335 // column is ALWAYS present in the INSERT statement —
2336 // no RETURNING / no DB DEFAULT needed.
2337 out.insert_pushes.push(quote! {
2338 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2339 self.#ident = #root::sql::Auto::Set(
2340 #root::__uuid::Uuid::now_v7(),
2341 );
2342 }
2343 if let #root::sql::Auto::Set(_v) = &self.#ident {
2344 _columns.push(#column);
2345 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2346 ::core::clone::Clone::clone(_v)
2347 ));
2348 }
2349 });
2350 } else if info.auto_now_add || info.auto_now {
2351 // Rust-side write timestamp (#1464), mirroring
2352 // `default_uuid_v7` above. `auto_now` has always bound
2353 // `Utc::now()` on UPDATE but not on INSERT, so the
2354 // first write of an `updated_at` took the DB default
2355 // while every later one took the clock — the same
2356 // column, filled two different ways.
2357 //
2358 // The default is the problem. `CREATE TABLE IF NOT
2359 // EXISTS` leaves an upgraded table's
2360 // `DEFAULT CURRENT_TIMESTAMP` in place, and SQLite's
2361 // ALTER TABLE grammar (RENAME/ADD/DROP) has no
2362 // statement that changes it — so every insert relying
2363 // on that default wrote the legacy shape forever, while
2364 // the migrate sweep converted the rows around it. The
2365 // column went permanently mixed and `ORDER BY` inverted
2366 // the admin audit log (#1616 rework review,
2367 // correctness-001 / dialects-005 / security-002).
2368 //
2369 // Binding it here makes the stale default unreachable
2370 // rather than trying to migrate it: the timestamp comes
2371 // from the application, not the database.
2372 out.insert_pushes.push(quote! {
2373 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2374 self.#ident = #root::sql::Auto::Set(
2375 #root::__chrono::Utc::now(),
2376 );
2377 }
2378 if let #root::sql::Auto::Set(_v) = &self.#ident {
2379 _columns.push(#column);
2380 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2381 ::core::clone::Clone::clone(_v)
2382 ));
2383 }
2384 });
2385 } else {
2386 out.insert_pushes.push(quote! {
2387 if let #root::sql::Auto::Set(_v) = &self.#ident {
2388 _columns.push(#column);
2389 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2390 ::core::clone::Clone::clone(_v)
2391 ));
2392 }
2393 });
2394 }
2395 // …except the timestamp columns, which must appear in BOTH
2396 // paths (#1464). The path is chosen by the *first* Auto
2397 // field — in practice the PK — so a bulk insert of rows
2398 // with an unset PK took the no-auto branch and dropped the
2399 // timestamp column, falling back to the DB default that
2400 // this change exists to stop depending on.
2401 //
2402 // `rows` is borrowed immutably here, so this fills per row
2403 // at push time rather than writing back into the struct. An
2404 // explicitly-Set value is still honoured; only `Unset`
2405 // takes the clock.
2406 // `default_uuid_v7` likewise: its column has no DB default (#1934).
2407 if info.default_uuid_v7 || info.auto_now_add || info.auto_now {
2408 out.rust_filled_idents.push(ident.clone());
2409 }
2410 if info.auto_now {
2411 out.auto_now_idents.push(ident.clone());
2412 }
2413 if info.default_uuid_v7 {
2414 out.uuid_v7_fields
2415 .push((ident.clone(), info.column.clone()));
2416 }
2417 let rust_fill = if info.default_uuid_v7 {
2418 Some(quote!(#root::__uuid::Uuid::now_v7()))
2419 } else if info.auto_now_add || info.auto_now {
2420 Some(quote!(#root::__chrono::Utc::now()))
2421 } else {
2422 None
2423 };
2424 // Bulk: Auto fields appear in the all-Set path. An unset filled
2425 // field takes its fill there too, not NULL (#1950).
2426 out.bulk_columns_all.push(quote!(#column));
2427 if let Some(fill) = &rust_fill {
2428 let push = quote! {
2429 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2430 match &_row.#ident {
2431 #root::sql::Auto::Set(_v) => ::core::clone::Clone::clone(_v),
2432 #root::sql::Auto::Unset => #fill,
2433 }
2434 ));
2435 };
2436 out.bulk_columns_no_auto.push(quote!(#column));
2437 out.bulk_pushes_no_auto.push(push.clone());
2438 out.bulk_pushes_all.push(push);
2439 } else {
2440 out.bulk_pushes_all.push(quote! {
2441 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2442 ::core::clone::Clone::clone(&_row.#ident)
2443 ));
2444 });
2445 }
2446 if info.default_uuid_v7 {
2447 out.bulk_fill_in_place.push(quote! {
2448 if matches!(&_row.#ident, #root::sql::Auto::Unset) {
2449 _row.#ident = #root::sql::Auto::Set(#root::__uuid::Uuid::now_v7());
2450 }
2451 });
2452 }
2453 // Uniformity check: every row's Auto state must match the
2454 // first row's. Mixed Set/Unset within one bulk_insert is
2455 // rejected here so the column list stays consistent.
2456 // A filled field may be unset on any row, so it is exempt.
2457 let ident_clone = ident.clone();
2458 if rust_fill.is_none() {
2459 out.bulk_auto_uniformity.push(quote! {
2460 for _r in rows.iter().skip(1) {
2461 if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2462 return ::core::result::Result::Err(
2463 #root::sql::ExecError::Sql(
2464 #root::sql::SqlError::BulkAutoMixed
2465 )
2466 );
2467 }
2468 }
2469 });
2470 }
2471 } else {
2472 out.insert_pushes.push(quote! {
2473 _columns.push(#column);
2474 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2475 ::core::clone::Clone::clone(&self.#ident)
2476 ));
2477 });
2478 // Bulk: non-Auto fields appear in BOTH paths.
2479 out.bulk_columns_no_auto.push(quote!(#column));
2480 out.bulk_columns_all.push(quote!(#column));
2481 let push_expr = quote! {
2482 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2483 ::core::clone::Clone::clone(&_row.#ident)
2484 ));
2485 };
2486 out.bulk_pushes_no_auto.push(push_expr.clone());
2487 out.bulk_pushes_all.push(push_expr);
2488 }
2489 if info.primary_key {
2490 if out.primary_key.is_some() {
2491 return Err(syn::Error::new_spanned(
2492 field,
2493 "only one field may be marked `#[rustango(primary_key)]`",
2494 ));
2495 }
2496 out.primary_key = Some((ident.clone(), info.column.clone()));
2497 if info.auto {
2498 out.pk_is_auto = true;
2499 }
2500 } else if info.auto_now_add {
2501 // Immutable post-insert: skip from UPDATE entirely.
2502 } else if info.auto_now {
2503 // `auto_now` columns: bind `chrono::Utc::now()` on every
2504 // UPDATE, and write it back so `self` matches the row.
2505 out.update_assignments.push(quote! {
2506 #root::core::Assignment::new(
2507 #column,
2508 {
2509 let _now = #root::__chrono::Utc::now();
2510 self.#ident = #root::sql::Auto::Set(_now);
2511 ::core::convert::Into::<#root::core::SqlValue>::into(_now)
2512 },
2513 )
2514 });
2515 out.upsert_update_columns.push(quote!(#column));
2516 } else {
2517 out.update_assignments.push(quote! {
2518 #root::core::Assignment::new(
2519 #column,
2520 ::core::convert::Into::<#root::core::SqlValue>::into(
2521 ::core::clone::Clone::clone(&self.#ident)
2522 ),
2523 )
2524 });
2525 out.upsert_update_columns.push(quote!(#column));
2526 }
2527 out.column_entries.push(ColumnEntry {
2528 ident: ident.clone(),
2529 value_ty: info.value_ty.clone(),
2530 name: ident_name(ident),
2531 column: info.column.clone(),
2532 field_type_tokens: info.field_type_tokens,
2533 uuid_column: info.uuid_column,
2534 });
2535 }
2536 Ok(out)
2537}
2538
2539/// A derived index name within PG's 63 bytes (MySQL takes 64); a longer one
2540/// keeps a prefix plus a hash of the whole, so two long names stay distinct.
2541fn capped_index_name(name: String) -> String {
2542 const MAX: usize = 63;
2543 if name.len() <= MAX {
2544 return name;
2545 }
2546 // FNV-1a: stable across builds, unlike `DefaultHasher`.
2547 let hash = name.bytes().fold(0x811c_9dc5_u32, |h, b| {
2548 (h ^ u32::from(b)).wrapping_mul(0x0100_0193)
2549 });
2550 let mut cut = MAX - 9;
2551 while !name.is_char_boundary(cut) {
2552 cut -= 1;
2553 }
2554 format!("{}_{hash:08x}", &name[..cut])
2555}
2556
2557fn model_impl_tokens(
2558 struct_name: &syn::Ident,
2559 model_name: &str,
2560 table: &str,
2561 display: Option<&str>,
2562 app_label: Option<&str>,
2563 admin: Option<&AdminAttrs>,
2564 default_order: &[(String, bool, proc_macro2::Span)],
2565 field_schemas: &[TokenStream2],
2566 soft_delete_column: Option<&str>,
2567 permissions: bool,
2568 audit_track: Option<&[String]>,
2569 m2m_relations: &[M2MAttr],
2570 indexes: &[IndexAttr],
2571 checks: &[CheckAttr],
2572 excludes: &[ExcludeAttr],
2573 composite_fks: &[CompositeFkAttr],
2574 generic_fks: &[GenericFkAttr],
2575 scope: Option<&str>,
2576 is_view: bool,
2577 verbose_name: Option<&str>,
2578 verbose_name_plural: Option<&str>,
2579 managed: bool,
2580 base_manager_name: Option<&str>,
2581 order_with_respect_to: Option<&str>,
2582 proxy: bool,
2583 required_db_features: &[String],
2584 required_db_vendor: Option<&str>,
2585 default_related_name: Option<&str>,
2586 db_table_comment: Option<&str>,
2587 get_latest_by: Option<(&str, bool)>,
2588 extra_permissions: &[(String, String)],
2589 default_permissions: &[String],
2590 global_scopes: &[GlobalScopeAttr],
2591 reverse_has_relations: &[ReverseHasAttr],
2592 generic_has_relations: &[GenericHasAttr],
2593) -> TokenStream2 {
2594 let root = rustango_root();
2595 let display_tokens = if let Some(name) = display {
2596 quote!(::core::option::Option::Some(#name))
2597 } else {
2598 quote!(::core::option::Option::None)
2599 };
2600 let app_label_tokens = if let Some(name) = app_label {
2601 quote!(::core::option::Option::Some(#name))
2602 } else {
2603 quote!(::core::option::Option::None)
2604 };
2605 let soft_delete_tokens = if let Some(col) = soft_delete_column {
2606 quote!(::core::option::Option::Some(#col))
2607 } else {
2608 quote!(::core::option::Option::None)
2609 };
2610 let audit_track_tokens = match audit_track {
2611 None => quote!(::core::option::Option::None),
2612 Some(names) => {
2613 let lits = names.iter().map(|n| n.as_str());
2614 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2615 }
2616 };
2617 let admin_tokens = admin_config_tokens(admin);
2618 // Default `tenant` so single-tenant projects (no `scope` attr
2619 // anywhere) keep the v0.24.x behavior. Container-attr parser
2620 // already validated the value is "registry" or "tenant".
2621 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2622 Some("registry") => quote!(#root::core::ModelScope::Registry),
2623 _ => quote!(#root::core::ModelScope::Tenant),
2624 };
2625 let verbose_name_tokens = optional_str(verbose_name);
2626 let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2627 let base_manager_name_tokens = optional_str(base_manager_name);
2628 let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2629 let required_db_features_lits: Vec<&str> =
2630 required_db_features.iter().map(String::as_str).collect();
2631 let required_db_vendor_tokens = optional_str(required_db_vendor);
2632 let default_related_name_tokens = optional_str(default_related_name);
2633 let db_table_comment_tokens = optional_str(db_table_comment);
2634 let get_latest_by_tokens = match get_latest_by {
2635 Some((col, desc)) => {
2636 quote!(::core::option::Option::Some((#col, #desc)))
2637 }
2638 None => quote!(::core::option::Option::None),
2639 };
2640 let extra_permission_tokens: Vec<_> = extra_permissions
2641 .iter()
2642 .map(|(c, l)| quote!((#c, #l)))
2643 .collect();
2644 let default_permission_tokens: Vec<_> = default_permissions
2645 .iter()
2646 .map(|action| quote!(#action))
2647 .collect();
2648 let indexes_tokens = indexes.iter().map(|idx| {
2649 // When no explicit `name = "..."` was given, derive a stable,
2650 // collision-free name from the table + columns
2651 // (`<table>_<col>_<col>_idx`) instead of a shared literal that
2652 // would clash the moment a model declares two unnamed indexes.
2653 let derived_name = idx
2654 .name
2655 .clone()
2656 .unwrap_or_else(|| capped_index_name(format!("{table}_{}_idx", idx.columns.join("_"))));
2657 let name = derived_name.as_str();
2658 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2659 let unique = idx.unique;
2660 // Map the parsed method string onto the IndexMethod enum
2661 // variant — kept at the codegen layer so the IR doesn't
2662 // carry the string form.
2663 let method_variant = match idx.method.as_str() {
2664 "gin" => quote!(#root::core::IndexMethod::Gin),
2665 "gist" => quote!(#root::core::IndexMethod::Gist),
2666 "brin" => quote!(#root::core::IndexMethod::Brin),
2667 "spgist" => quote!(#root::core::IndexMethod::SpGist),
2668 "hash" => quote!(#root::core::IndexMethod::Hash),
2669 "bloom" => quote!(#root::core::IndexMethod::Bloom),
2670 _ => quote!(#root::core::IndexMethod::BTree),
2671 };
2672 let where_clause = match &idx.where_clause {
2673 Some(s) => quote!(::core::option::Option::Some(#s)),
2674 None => quote!(::core::option::Option::None),
2675 };
2676 let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2677 quote! {{
2678 let mut i = #root::core::IndexSchema::new(#name, &[ #(#cols),* ]);
2679 i.unique = #unique;
2680 i.method = #method_variant;
2681 i.where_clause = #where_clause;
2682 i.include = &[ #(#include_lits),* ];
2683 i
2684 }}
2685 });
2686 let checks_tokens = checks.iter().map(|c| {
2687 let name = c.name.as_str();
2688 let expr = c.expr.as_str();
2689 quote!(#root::core::CheckConstraint::new(#name, #expr))
2690 });
2691 let excludes_tokens = excludes.iter().map(|e| {
2692 let name = e.name.as_str();
2693 let using = e.using.as_str();
2694 let element_tokens = e.elements.iter().map(|(col, op)| {
2695 let col_s = col.as_str();
2696 let op_s = op.as_str();
2697 quote!((#col_s, #op_s))
2698 });
2699 let where_tokens = match e.where_clause.as_deref() {
2700 Some(w) => quote!(::core::option::Option::Some(#w)),
2701 None => quote!(::core::option::Option::None),
2702 };
2703 quote! {{
2704 let mut e = #root::core::ExclusionConstraint::new(
2705 #name,
2706 #using,
2707 &[ #(#element_tokens),* ],
2708 );
2709 e.where_clause = #where_tokens;
2710 e
2711 }}
2712 });
2713 let composite_fk_tokens = composite_fks.iter().map(|rel| {
2714 let name = rel.name.as_str();
2715 let to = rel.to.as_str();
2716 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2717 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2718 quote! {
2719 #root::core::CompositeFkRelation::new(
2720 #name,
2721 #to,
2722 &[ #(#from_cols),* ],
2723 &[ #(#on_cols),* ],
2724 )
2725 }
2726 });
2727 let generic_fk_tokens = generic_fks.iter().map(|rel| {
2728 let name = rel.name.as_str();
2729 let ct_col = rel.ct_column.as_str();
2730 let pk_col = rel.pk_column.as_str();
2731 quote!(#root::core::GenericRelation::new(#name, #ct_col, #pk_col))
2732 });
2733 // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2734 // no `#[rustango(default_order = "...")]` attribute was supplied.
2735 let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2736 let col_lit = col.as_str();
2737 quote! { (#col_lit, #desc) }
2738 });
2739
2740 // Issue #820 — `global_scopes` slice literal. Empty when no
2741 // `#[rustango(global_scope(...))]` attribute was supplied. The
2742 // `apply` path is re-emitted verbatim so it resolves in the
2743 // consumer's scope; the name is stored as a string literal.
2744 let global_scope_tokens = global_scopes.iter().map(|s| {
2745 let name = s.name.as_str();
2746 // The slice sits in a nested `const`, where `Self` does not resolve.
2747 let mut apply = s.apply.clone();
2748 if let Some(first) = apply.segments.first_mut() {
2749 if first.ident == "Self" {
2750 first.ident = struct_name.clone();
2751 }
2752 }
2753 quote!(#root::core::GlobalScope::new(#name, #apply))
2754 });
2755
2756 let m2m_tokens = m2m_relations.iter().map(|rel| {
2757 let name = rel.name.as_str();
2758 let to = rel.to.as_str();
2759 let through = rel.through.as_str();
2760 let src = rel.src.as_str();
2761 let dst = rel.dst.as_str();
2762 let auto_create = rel.auto_create;
2763 quote! {{
2764 let mut m = #root::core::M2MRelation::new(#name, #to, #through, #src, #dst);
2765 m.auto_create = #auto_create;
2766 m
2767 }}
2768 });
2769 // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2770 // when the model declares `#[rustango(reverse_has(...))]`. Each
2771 // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2772 // a const expression. Models without reverse_has fall through to
2773 // the trait's empty default — no override emitted.
2774 let reverse_relations_override = if reverse_has_relations.is_empty() {
2775 quote!()
2776 } else {
2777 let entries = reverse_has_relations.iter().map(|rel| {
2778 let name = rel.name.as_str();
2779 let child = &rel.child;
2780 let child_fk_column = rel.child_fk_column.as_str();
2781 let self_pk_column = rel.self_pk_column.as_str();
2782 quote! {
2783 #root::core::ReverseRelation::new(
2784 #name,
2785 <#child as #root::core::Model>::SCHEMA,
2786 #child_fk_column,
2787 #self_pk_column,
2788 )
2789 }
2790 });
2791 quote! {
2792 fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2793 const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2794 RELS
2795 }
2796 }
2797 };
2798 // Issue #830 — `Model::generic_reverse_relations()` override for
2799 // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2800 let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2801 quote!()
2802 } else {
2803 let entries = generic_has_relations.iter().map(|rel| {
2804 let name = rel.name.as_str();
2805 let child = &rel.child;
2806 let ct_column = rel.ct_column.as_str();
2807 let pk_column = rel.pk_column.as_str();
2808 let self_pk_column = rel.self_pk_column.as_str();
2809 quote! {
2810 #root::core::GenericReverseRelation::new(
2811 #name,
2812 <#child as #root::core::Model>::SCHEMA,
2813 #ct_column,
2814 #pk_column,
2815 #self_pk_column,
2816 )
2817 }
2818 });
2819 quote! {
2820 fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2821 const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2822 RELS
2823 }
2824 }
2825 };
2826 let fields_slice = const_slice(
2827 "e!(#root::core::FieldSchema),
2828 field_schemas.iter().cloned(),
2829 );
2830 let m2m_slice = const_slice("e!(#root::core::M2MRelation), m2m_tokens);
2831 let indexes_slice = const_slice("e!(#root::core::IndexSchema), indexes_tokens);
2832 let checks_slice = const_slice("e!(#root::core::CheckConstraint), checks_tokens);
2833 let excludes_slice = const_slice("e!(#root::core::ExclusionConstraint), excludes_tokens);
2834 let composite_fk_slice = const_slice(
2835 "e!(#root::core::CompositeFkRelation),
2836 composite_fk_tokens,
2837 );
2838 let generic_fk_slice = const_slice("e!(#root::core::GenericRelation), generic_fk_tokens);
2839 let audited_update_override = if audit_track.is_some() {
2840 quote! {
2841 fn __rustango_audited_update() -> ::core::option::Option<#root::audit::AuditedUpdate> {
2842 ::core::option::Option::Some(Self::__rustango_update_audited)
2843 }
2844 fn __rustango_audited_delete() -> ::core::option::Option<#root::audit::AuditedDelete> {
2845 ::core::option::Option::Some(Self::__rustango_delete_audited)
2846 }
2847 fn __rustango_audited_create() -> ::core::option::Option<#root::audit::AuditedCreate> {
2848 ::core::option::Option::Some(Self::__rustango_create_audited)
2849 }
2850 fn __rustango_audited_update_record(
2851 ) -> ::core::option::Option<#root::audit::AuditedUpdateRecord> {
2852 ::core::option::Option::Some(Self::__rustango_update_record_audited)
2853 }
2854 }
2855 } else {
2856 quote!()
2857 };
2858 let global_scope_slice = const_slice("e!(#root::core::GlobalScope), global_scope_tokens);
2859 quote! {
2860 impl #root::core::Model for #struct_name {
2861 const SCHEMA: &'static #root::core::ModelSchema = &{
2862 #root::core::ModelSchema::from_parts(#root::core::ModelSchemaParts {
2863 name: #model_name,
2864 table: #table,
2865 fields: #fields_slice,
2866 display: #display_tokens,
2867 app_label: #app_label_tokens,
2868 admin: #admin_tokens,
2869 soft_delete_column: #soft_delete_tokens,
2870 permissions: #permissions,
2871 audit_track: #audit_track_tokens,
2872 m2m: #m2m_slice,
2873 indexes: #indexes_slice,
2874 check_constraints: #checks_slice,
2875 exclusion_constraints: #excludes_slice,
2876 composite_relations: #composite_fk_slice,
2877 generic_relations: #generic_fk_slice,
2878 scope: #scope_tokens,
2879 default_order: &[ #(#default_order_tokens),* ],
2880 is_view: #is_view,
2881 verbose_name: #verbose_name_tokens,
2882 verbose_name_plural: #verbose_name_plural_tokens,
2883 managed: #managed,
2884 base_manager_name: #base_manager_name_tokens,
2885 order_with_respect_to: #order_with_respect_to_tokens,
2886 proxy: #proxy,
2887 required_db_features: &[ #(#required_db_features_lits),* ],
2888 required_db_vendor: #required_db_vendor_tokens,
2889 default_related_name: #default_related_name_tokens,
2890 db_table_comment: #db_table_comment_tokens,
2891 get_latest_by: #get_latest_by_tokens,
2892 extra_permissions: &[ #(#extra_permission_tokens),* ],
2893 default_permissions: &[ #(#default_permission_tokens),* ],
2894 global_scopes: #global_scope_slice,
2895 })
2896 };
2897
2898 #reverse_relations_override
2899 #generic_reverse_relations_override
2900 #audited_update_override
2901 }
2902 }
2903}
2904
2905/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2906/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2907/// otherwise a static reference to a populated `AdminConfig`.
2908fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2909 let root = rustango_root();
2910 let Some(admin) = admin else {
2911 return quote!(::core::option::Option::None);
2912 };
2913
2914 let list_display = admin
2915 .list_display
2916 .as_ref()
2917 .map(|(v, _)| v.as_slice())
2918 .unwrap_or(&[]);
2919 let list_display_lits = list_display.iter().map(|s| s.as_str());
2920
2921 let search_fields = admin
2922 .search_fields
2923 .as_ref()
2924 .map(|(v, _)| v.as_slice())
2925 .unwrap_or(&[]);
2926 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2927
2928 let readonly_fields = admin
2929 .readonly_fields
2930 .as_ref()
2931 .map(|(v, _)| v.as_slice())
2932 .unwrap_or(&[]);
2933 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2934
2935 let list_filter = admin
2936 .list_filter
2937 .as_ref()
2938 .map(|(v, _)| v.as_slice())
2939 .unwrap_or(&[]);
2940 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2941
2942 let actions = admin
2943 .actions
2944 .as_ref()
2945 .map(|(v, _)| v.as_slice())
2946 .unwrap_or(&[]);
2947 let actions_lits = actions.iter().map(|s| s.as_str());
2948
2949 let fieldsets = admin
2950 .fieldsets
2951 .as_ref()
2952 .map(|(v, _)| v.as_slice())
2953 .unwrap_or(&[]);
2954 let fieldset_tokens = const_slice(
2955 "e!(#root::core::Fieldset),
2956 fieldsets.iter().map(|(title, fields)| {
2957 let title = title.as_str();
2958 let field_lits = fields.iter().map(String::as_str);
2959 quote!(#root::core::Fieldset::new(#title, &[ #( #field_lits ),* ]))
2960 }),
2961 );
2962
2963 let list_display_links = admin
2964 .list_display_links
2965 .as_ref()
2966 .map(|(v, _)| v.as_slice())
2967 .unwrap_or(&[]);
2968 let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2969
2970 let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2971 let actions_on_top = admin.actions_on_top.unwrap_or(true);
2972 let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2973 let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2974
2975 let prepopulated = admin
2976 .prepopulated_fields
2977 .as_ref()
2978 .map(|(v, _)| v.as_slice())
2979 .unwrap_or(&[]);
2980 let prepopulated_tokens = const_slice(
2981 "e!(#root::core::PrepopulatedField),
2982 prepopulated.iter().map(|(target, sources)| {
2983 let target = target.as_str();
2984 let source_lits = sources.iter().map(String::as_str);
2985 quote!(#root::core::PrepopulatedField::new(#target, &[ #( #source_lits ),* ]))
2986 }),
2987 );
2988
2989 let raw_id_fields = admin
2990 .raw_id_fields
2991 .as_ref()
2992 .map(|(v, _)| v.as_slice())
2993 .unwrap_or(&[]);
2994 let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2995
2996 let autocomplete_fields = admin
2997 .autocomplete_fields
2998 .as_ref()
2999 .map(|(v, _)| v.as_slice())
3000 .unwrap_or(&[]);
3001 let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
3002
3003 // #352 — list_select_related accepts "all" | "none" | "field, field, …".
3004 let list_select_related_tokens = match admin.list_select_related.as_deref() {
3005 None | Some("all") => quote!(#root::core::ListSelectRelated::All),
3006 Some("none") => quote!(#root::core::ListSelectRelated::None),
3007 Some(raw) => {
3008 let names: Vec<&str> = raw
3009 .split(',')
3010 .map(str::trim)
3011 .filter(|s| !s.is_empty())
3012 .collect();
3013 quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
3014 }
3015 };
3016
3017 // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
3018 // a Vec<(String, String)>. Empty / unset → no overrides.
3019 let formfield_pairs: Vec<(&str, &str)> = admin
3020 .formfield_overrides
3021 .as_ref()
3022 .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
3023 .unwrap_or_default();
3024 let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
3025 let field = *field;
3026 let widget = *widget;
3027 quote!((#field, #widget))
3028 });
3029
3030 let list_per_page = admin.list_per_page.unwrap_or(0);
3031
3032 let ordering_pairs = admin
3033 .ordering
3034 .as_ref()
3035 .map(|(v, _)| v.as_slice())
3036 .unwrap_or(&[]);
3037 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
3038 let name = name.as_str();
3039 let desc = *desc;
3040 quote!((#name, #desc))
3041 });
3042
3043 quote! {
3044 ::core::option::Option::Some({
3045 const ADMIN: &#root::core::AdminConfig = &{
3046 #root::core::AdminConfig::from_parts(#root::core::AdminConfigParts {
3047 list_display: &[ #( #list_display_lits ),* ],
3048 search_fields: &[ #( #search_fields_lits ),* ],
3049 list_per_page: #list_per_page,
3050 ordering: &[ #( #ordering_tokens ),* ],
3051 readonly_fields: &[ #( #readonly_fields_lits ),* ],
3052 list_filter: &[ #( #list_filter_lits ),* ],
3053 actions: &[ #( #actions_lits ),* ],
3054 fieldsets: #fieldset_tokens,
3055 list_display_links: &[ #( #list_display_links_lits ),* ],
3056 search_help_text: #search_help_text,
3057 actions_on_top: #actions_on_top,
3058 actions_on_bottom: #actions_on_bottom,
3059 date_hierarchy: #date_hierarchy,
3060 prepopulated_fields: #prepopulated_tokens,
3061 raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
3062 autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
3063 list_select_related: #list_select_related_tokens,
3064 formfield_overrides: &[ #( #formfield_tokens ),* ],
3065 })
3066 };
3067 ADMIN
3068 })
3069 }
3070}
3071
3072fn inherent_impl_tokens(
3073 struct_name: &syn::Ident,
3074 fields: &CollectedFields,
3075 primary_key: Option<&(syn::Ident, String)>,
3076 column_consts: &TokenStream2,
3077 audited_fields: Option<&[&ColumnEntry]>,
3078 upsert_unique: Option<&[String]>,
3079 manager_fns: &[syn::Ident],
3080) -> TokenStream2 {
3081 let root = rustango_root();
3082 // Audit-emit fragments threaded into write paths. Non-empty only
3083 // when the model carries `#[rustango(audit(...))]`. They reborrow
3084 // `_executor` (a `&mut PgConnection` for audited models — the
3085 // macro switches the signature below) so the data write and the
3086 // audit INSERT both run on the same caller-supplied connection.
3087 let executor_passes_to_data_write = if audited_fields.is_some() {
3088 quote!(&mut *_executor)
3089 } else {
3090 quote!(_executor)
3091 };
3092 let executor_param = if audited_fields.is_some() {
3093 quote!(_executor: &mut #root::sql::sqlx::PgConnection)
3094 } else {
3095 quote!(_executor: _E)
3096 };
3097 let executor_generics = if audited_fields.is_some() {
3098 quote!()
3099 } else {
3100 quote!(<'_c, _E>)
3101 };
3102 let executor_where = if audited_fields.is_some() {
3103 quote!()
3104 } else {
3105 quote! {
3106 where
3107 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
3108 }
3109 };
3110 // Audited `_on` methods take `&mut PgConnection`; `&PgPool` is an Executor for the rest.
3111 // One transaction: the before-image's FOR UPDATE must hold through
3112 // the UPDATE and its audit row.
3113 let pool_to_save_on = if audited_fields.is_some() {
3114 // Any failure, COMMIT included, rolls the INSERT back: put the pk and
3115 // stamps back too, or a retry takes the UPDATE path and writes nothing.
3116 let restored: Vec<&syn::Ident> = primary_key
3117 .map(|(pk, _)| pk)
3118 .into_iter()
3119 .chain(&fields.auto_now_idents)
3120 .collect();
3121 let prev: Vec<syn::Ident> = (0..restored.len())
3122 .map(|i| quote::format_ident!("__rustango_save_prev_{}", i))
3123 .collect();
3124 quote! {
3125 #( let #prev = ::core::clone::Clone::clone(&self.#restored); )*
3126 let _saved: ::core::result::Result<u64, #root::sql::ExecError> = async {
3127 let mut _tx = pool.begin().await?;
3128 let _affected = self.save_on(&mut *_tx).await?;
3129 _tx.commit().await?;
3130 ::core::result::Result::Ok(_affected)
3131 }
3132 .await;
3133 if _saved.is_err() {
3134 #( self.#restored = #prev; )*
3135 }
3136 _saved
3137 }
3138 } else {
3139 quote!(self.save_on(pool).await)
3140 };
3141 // One transaction so the write and its audit row commit together.
3142 let pool_to_insert_on = if audited_fields.is_some() {
3143 // A failed COMMIT leaves the RETURNING values on `self`; put them back.
3144 let assigned: Vec<syn::Ident> = fields
3145 .auto_field_idents
3146 .iter()
3147 .map(|(ident, _, _)| ident.clone())
3148 .collect();
3149 restore_auto_now_unless(
3150 &assigned,
3151 "e!(::core::result::Result<(), #root::sql::ExecError>),
3152 "e!(_saved.is_ok()),
3153 "e! {
3154 let mut _tx = pool.begin().await?;
3155 self.insert_on(&mut *_tx).await?;
3156 _tx.commit().await?;
3157 ::core::result::Result::Ok(())
3158 },
3159 )
3160 } else {
3161 quote!(self.insert_on(pool).await)
3162 };
3163 let pool_to_delete_on = if audited_fields.is_some() {
3164 quote! {
3165 let mut _tx = pool.begin().await?;
3166 let _affected = self.delete_on(&mut *_tx).await?;
3167 _tx.commit().await?;
3168 ::core::result::Result::Ok(_affected)
3169 }
3170 } else {
3171 quote!(self.delete_on(pool).await)
3172 };
3173 let pool_to_bulk_insert_on = if audited_fields.is_some() {
3174 quote! {
3175 let mut _tx = pool.begin().await?;
3176 Self::bulk_insert_on(rows, &mut *_tx).await?;
3177 _tx.commit().await?;
3178 ::core::result::Result::Ok(())
3179 }
3180 } else {
3181 quote!(Self::bulk_insert_on(rows, pool).await)
3182 };
3183 // Pre-existing bug surfaced by batch 22's first audited Auto<T>
3184 // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
3185 // directly, but `upsert_on` for audited models takes
3186 // `&mut PgConnection` (the audit emit needs a real connection).
3187 // Add the missing acquire shim to keep audited Auto-PK upsert
3188 // compiling.
3189 let pool_to_upsert_on = if audited_fields.is_some() {
3190 quote! {
3191 let mut _tx = pool.begin().await?;
3192 self.upsert_on(&mut *_tx).await?;
3193 _tx.commit().await?;
3194 ::core::result::Result::Ok(())
3195 }
3196 } else {
3197 quote!(self.upsert_on(pool).await)
3198 };
3199
3200 // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
3201 // (audit-on-connection over &Pool needs a bi-dialect transaction
3202 // helper, deferred). Two body shapes:
3203 // - has_auto: build InsertQuery skipping Auto::Unset columns,
3204 // request Auto cols in `returning`, dispatch via
3205 // `insert_returning_pool`, then on the returned `PgRow` /
3206 // `MySqlAutoId(id)` enum — pull each Auto field from the PG
3207 // row OR drop the single i64 into the first Auto field on MySQL
3208 // (multi-Auto models on MySQL error at runtime since
3209 // `LAST_INSERT_ID()` only reports one)
3210 // - non-Auto: build InsertQuery with explicit columns/values and
3211 // call `insert_pool` (no returning needed)
3212 // pool_insert_method body for the audited Auto-PK case is moved
3213 // to after audit_pair_tokens / audit_pk_to_string (they live
3214 // ~150 lines below). This block keeps the non-audited and
3215 // non-Auto branches in place — the audited Auto-PK arm is
3216 // computed below and merged via the dispatch helper variable.
3217 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
3218 // Audited models with explicit (non-Auto) PKs go through
3219 // the non-Auto insert path below — the audit emit is one
3220 // round-trip after the INSERT inside the same tx via
3221 // audit::save_one_with_audit? No, INSERT semantics
3222 // differ. For non-Auto PK + audited, route through a
3223 // dedicated insert + audit emit on the same tx, but defer
3224 // the macro emission to the audit-bundle-aware block below
3225 // — this `quote!()` placeholder gets overwritten there.
3226 quote!()
3227 } else if audited_fields.is_some() && fields.has_auto {
3228 // Audited Auto-PK insert_pool — assembled after the audit
3229 // bundles. Placeholder; real emission below.
3230 quote!()
3231 } else if fields.has_auto {
3232 let pushes = &fields.insert_pushes;
3233 let returning_cols = &fields.returning_cols;
3234 // A row not inserted was never saved, so a Rust-filled value must not stay (#1937).
3235 let filled = &fields.rust_filled_idents;
3236 let was_unset: Vec<syn::Ident> = filled
3237 .iter()
3238 .map(|i| quote::format_ident!("__{}_was_unset", ident_name(i)))
3239 .collect();
3240 let insert_or_ignore_call = quote! {
3241 #( let #was_unset = matches!(&self.#filled, #root::sql::Auto::Unset); )*
3242 let mut _columns: ::std::vec::Vec<&'static str> =
3243 ::std::vec::Vec::new();
3244 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3245 ::std::vec::Vec::new();
3246 #( #pushes )*
3247 let _query = #root::core::InsertQuery::new(
3248 <Self as #root::core::Model>::SCHEMA,
3249 _columns,
3250 _values,
3251 );
3252 let _result = #root::sql::insert_or_ignore(pool, &_query).await;
3253 if !matches!(_result, ::core::result::Result::Ok(true)) {
3254 #( if #was_unset { self.#filled = #root::sql::Auto::Unset; } )*
3255 }
3256 _result
3257 };
3258 // When every `Auto<T>` field is filled Rust-side
3259 // (`default_uuid_v7`, issue #823), there is no column to read
3260 // back from the database — `returning_cols` is empty. Route
3261 // through plain `insert_pool` instead of
3262 // `insert_returning_pool` to skip the redundant RETURNING /
3263 // LAST_INSERT_ID round-trip.
3264 if fields.returning_cols.is_empty() {
3265 quote! {
3266 /// Insert this row against either backend. Every
3267 /// `Auto<T>` PK on this model is filled Rust-side
3268 /// (e.g. `default_uuid_v7`) before binding, so no
3269 /// RETURNING round-trip is needed.
3270 ///
3271 /// # Errors
3272 /// As [`Self::insert`].
3273 pub async fn insert_pool(
3274 &mut self,
3275 pool: &#root::sql::Pool,
3276 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3277 let mut _columns: ::std::vec::Vec<&'static str> =
3278 ::std::vec::Vec::new();
3279 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3280 ::std::vec::Vec::new();
3281 #( #pushes )*
3282 let _query = #root::core::InsertQuery::new(
3283 <Self as #root::core::Model>::SCHEMA,
3284 _columns,
3285 _values,
3286 );
3287 #root::sql::insert_pool(pool, &_query).await
3288 }
3289
3290 /// Eloquent `Model::insertOrIgnore()` — same shape
3291 /// as the auto-PK branch above. Returns `Ok(true)`
3292 /// when inserted, `Ok(false)` when a conflict caused
3293 /// the INSERT to silently skip.
3294 ///
3295 /// # Errors
3296 /// As [`Self::insert`].
3297 pub async fn insert_or_ignore(
3298 &mut self,
3299 pool: &#root::sql::Pool,
3300 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3301 #insert_or_ignore_call
3302 }
3303 }
3304 } else {
3305 quote! {
3306 /// Insert this row against either backend, populating any
3307 /// `Auto<T>` PK from the auto-assigned value.
3308 ///
3309 /// # Errors
3310 /// As [`Self::insert`].
3311 pub async fn insert_pool(
3312 &mut self,
3313 pool: &#root::sql::Pool,
3314 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3315 let mut _columns: ::std::vec::Vec<&'static str> =
3316 ::std::vec::Vec::new();
3317 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3318 ::std::vec::Vec::new();
3319 #( #pushes )*
3320 let _query = #root::core::InsertQuery::new(
3321 <Self as #root::core::Model>::SCHEMA,
3322 _columns,
3323 _values,
3324 )
3325 .returning(::std::vec![ #( #returning_cols ),* ]);
3326 let _result = #root::sql::insert_returning_pool(
3327 pool, &_query,
3328 ).await?;
3329 #root::sql::apply_auto_pk(_result, self)
3330 }
3331
3332 /// Eloquent `Model::insertOrIgnore()` — INSERT this
3333 /// row or silently skip on unique-constraint
3334 /// violation. Returns `Ok(true)` when a row was
3335 /// inserted, `Ok(false)` when a conflict caused the
3336 /// INSERT to silently skip.
3337 ///
3338 /// **Caveat on auto-PK models**: when the row is
3339 /// skipped (conflict), this instance's `Auto<T>`
3340 /// fields stay `Unset` — no PK is back-populated
3341 /// because the server didn't auto-assign one. For
3342 /// "insert then read back the PK or the existing
3343 /// row's PK", use the `upsert` family or
3344 /// `get_or_create`.
3345 ///
3346 /// # Errors
3347 /// As [`Self::insert`].
3348 pub async fn insert_or_ignore(
3349 &mut self,
3350 pool: &#root::sql::Pool,
3351 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3352 #insert_or_ignore_call
3353 }
3354 }
3355 }
3356 } else {
3357 let insert_columns = &fields.insert_columns;
3358 let insert_values = &fields.insert_values;
3359 quote! {
3360 /// Insert this row into its table against either backend.
3361 /// Equivalent to [`Self::insert`] but takes
3362 /// [`#root::sql::Pool`].
3363 ///
3364 /// # Errors
3365 /// As [`Self::insert`].
3366 pub async fn insert_pool(
3367 &self,
3368 pool: &#root::sql::Pool,
3369 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3370 let _query = #root::core::InsertQuery::new(
3371 <Self as #root::core::Model>::SCHEMA,
3372 ::std::vec![ #( #insert_columns ),* ],
3373 ::std::vec![ #( #insert_values ),* ],
3374 );
3375 #root::sql::insert_pool(pool, &_query).await
3376 }
3377
3378 /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3379 /// or silently skip on unique-constraint violation. Maps
3380 /// to per-dialect "INSERT ... DO NOTHING on conflict":
3381 /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3382 /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL an
3383 /// `ON DUPLICATE KEY UPDATE` that leaves the row as is.
3384 ///
3385 /// Returns `Ok(true)` when a row was inserted,
3386 /// `Ok(false)` when a conflict caused the INSERT to
3387 /// silently skip.
3388 ///
3389 /// Use for "create-if-absent" patterns where you don't
3390 /// need the row back. For "find-or-create with the row
3391 /// returned", use the queryset-level
3392 /// `crate::sql::get_or_create` free function.
3393 ///
3394 /// # Errors
3395 /// As [`Self::insert`], plus any dialect-specific
3396 /// translation error from the ConflictClause writer.
3397 pub async fn insert_or_ignore(
3398 &self,
3399 pool: &#root::sql::Pool,
3400 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3401 let _query = #root::core::InsertQuery::new(
3402 <Self as #root::core::Model>::SCHEMA,
3403 ::std::vec![ #( #insert_columns ),* ],
3404 ::std::vec![ #( #insert_values ),* ],
3405 );
3406 #root::sql::insert_or_ignore(pool, &_query).await
3407 }
3408 }
3409 };
3410
3411 // The tracked (column, JSON value) pairs, spliced only into
3412 // `__rustango_audit_pairs`. Empty when the model isn't audited.
3413 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3414 .map(|tracked| {
3415 tracked
3416 .iter()
3417 .map(|c| {
3418 let column_lit = c.column.as_str();
3419 let ident = &c.ident;
3420 quote! {
3421 (
3422 #column_lit,
3423 #root::core::__json_value(&self.#ident),
3424 )
3425 }
3426 })
3427 .collect()
3428 })
3429 .unwrap_or_default();
3430 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3431 if fields.pk_is_auto {
3432 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3433 } else {
3434 quote!(::std::format!("{}", &self.#pk_ident))
3435 }
3436 } else {
3437 quote!(::std::string::String::new())
3438 };
3439 // `__rustango_audit_entry`: the one place a snapshot `PendingEntry`
3440 // is built, so every audited write path records the same shape.
3441 let delete_audited_body = if primary_key.is_some() {
3442 quote! {
3443 ::std::boxed::Box::pin(#root::audit::delete_many_with_audit::<Self>(
3444 pool,
3445 query,
3446 |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3447 ))
3448 }
3449 } else {
3450 quote! {
3451 let _ = (pool, query);
3452 ::std::boxed::Box::pin(async {
3453 ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3454 table: <Self as #root::core::Model>::SCHEMA.table,
3455 })
3456 })
3457 }
3458 };
3459 let update_audited_body = if primary_key.is_some() {
3460 quote! {
3461 ::std::boxed::Box::pin(#root::audit::update_many_with_audit::<Self>(
3462 pool,
3463 query,
3464 move |_r: &Self| _r.__rustango_audit_entry(op),
3465 ))
3466 }
3467 } else {
3468 quote! {
3469 let _ = (pool, query, op);
3470 ::std::boxed::Box::pin(async {
3471 ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3472 table: <Self as #root::core::Model>::SCHEMA.table,
3473 })
3474 })
3475 }
3476 };
3477 let audit_entry_method = if audited_fields.is_some() {
3478 let pairs = audit_pair_tokens.iter();
3479 let pk_str = audit_pk_to_string.clone();
3480 quote! {
3481 /// This row's tracked `(column, value)` pairs.
3482 #[doc(hidden)]
3483 pub fn __rustango_audit_pairs(
3484 &self,
3485 ) -> ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> {
3486 ::std::vec![ #( #pairs ),* ]
3487 }
3488
3489 /// This row's audit PK.
3490 #[doc(hidden)]
3491 pub fn __rustango_audit_pk(&self) -> ::std::string::String {
3492 #pk_str
3493 }
3494
3495 /// Snapshot audit entry for `operation` on this row.
3496 #[doc(hidden)]
3497 pub fn __rustango_audit_entry(
3498 &self,
3499 operation: #root::audit::AuditOp,
3500 ) -> #root::audit::PendingEntry {
3501 #root::audit::PendingEntry {
3502 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3503 entity_pk: self.__rustango_audit_pk(),
3504 operation,
3505 source: #root::audit::current_source(),
3506 changes: #root::audit::snapshot_changes(&self.__rustango_audit_pairs()),
3507 }
3508 }
3509
3510 /// Audited bulk `UPDATE`, behind `Model::__rustango_audited_update`.
3511 #[doc(hidden)]
3512 pub fn __rustango_update_audited<'a>(
3513 pool: &'a #root::sql::Pool,
3514 query: &'a #root::core::UpdateQuery,
3515 op: #root::audit::AuditOp,
3516 ) -> ::std::pin::Pin<::std::boxed::Box<
3517 dyn ::core::future::Future<
3518 Output = ::core::result::Result<u64, #root::sql::ExecError>,
3519 > + ::core::marker::Send + 'a,
3520 >> {
3521 #update_audited_body
3522 }
3523
3524 /// Audited `create` entry for a new row, behind `Model::__rustango_audited_create`.
3525 #[doc(hidden)]
3526 pub fn __rustango_create_audited<'a, 't>(
3527 tx: &'a mut #root::sql::PoolTx<'t>,
3528 pk: #root::core::SqlValue,
3529 ) -> ::std::pin::Pin<::std::boxed::Box<
3530 dyn ::core::future::Future<
3531 Output = ::core::result::Result<(), #root::sql::ExecError>,
3532 > + ::core::marker::Send + 'a,
3533 >> {
3534 ::std::boxed::Box::pin(#root::audit::record_create::<Self>(
3535 tx,
3536 <Self as #root::core::Model>::SCHEMA,
3537 pk,
3538 |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Create),
3539 ))
3540 }
3541
3542 /// Audited one-row `update` entry, behind `Model::__rustango_audited_update_record`.
3543 #[doc(hidden)]
3544 pub fn __rustango_update_record_audited<'a, 't>(
3545 tx: &'a mut #root::sql::PoolTx<'t>,
3546 pk: #root::core::SqlValue,
3547 ) -> ::std::pin::Pin<::std::boxed::Box<
3548 dyn ::core::future::Future<
3549 Output = ::core::result::Result<(), #root::sql::ExecError>,
3550 > + ::core::marker::Send + 'a,
3551 >> {
3552 ::std::boxed::Box::pin(#root::audit::record_update::<Self>(
3553 tx,
3554 <Self as #root::core::Model>::SCHEMA,
3555 pk,
3556 |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Update),
3557 ))
3558 }
3559
3560 /// Audited bulk `DELETE`, behind `Model::__rustango_audited_delete`.
3561 #[doc(hidden)]
3562 pub fn __rustango_delete_audited<'a>(
3563 pool: &'a #root::sql::Pool,
3564 query: &'a #root::core::DeleteQuery,
3565 ) -> ::std::pin::Pin<::std::boxed::Box<
3566 dyn ::core::future::Future<
3567 Output = ::core::result::Result<u64, #root::sql::ExecError>,
3568 > + ::core::marker::Send + 'a,
3569 >> {
3570 #delete_audited_body
3571 }
3572 }
3573 } else {
3574 quote!()
3575 };
3576 // Builds `_audit_entry`; `written` is the soft-delete column's new
3577 // value, recorded in place of the stale `self` field.
3578 let make_entry = |op_path: TokenStream2, written: Option<TokenStream2>| -> TokenStream2 {
3579 if let (Some(v), Some(col)) = (written, fields.soft_delete_column.as_deref()) {
3580 quote! {
3581 let mut _audit_entry = self.__rustango_audit_entry(#op_path);
3582 _audit_entry.set_tracked(#col, #v);
3583 }
3584 } else {
3585 quote!(let _audit_entry = self.__rustango_audit_entry(#op_path);)
3586 }
3587 };
3588 let soft_deleted_json = quote!(#root::__serde_json::to_value(&_now)
3589 .unwrap_or(#root::__serde_json::Value::Null));
3590 let restored_json = quote!(#root::__serde_json::Value::Null);
3591 // `_on` emit; `affected` paths skip the row when nothing changed.
3592 let make_op_emit =
3593 |op_path: TokenStream2, written: Option<TokenStream2>, affected: bool| -> TokenStream2 {
3594 if audited_fields.is_none() {
3595 return quote!();
3596 }
3597 let entry = make_entry(op_path, written);
3598 let emit = quote! {
3599 #entry
3600 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3601 };
3602 if affected {
3603 quote!(if _affected > 0 { #emit })
3604 } else {
3605 emit
3606 }
3607 };
3608 let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create), None, false);
3609 let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete), None, true);
3610 let audit_softdelete_emit = make_op_emit(
3611 quote!(#root::audit::AuditOp::SoftDelete),
3612 Some(soft_deleted_json.clone()),
3613 true,
3614 );
3615 let audit_restore_emit = make_op_emit(
3616 quote!(#root::audit::AuditOp::Restore),
3617 Some(restored_json.clone()),
3618 true,
3619 );
3620 // `&Pool` update runner for soft_delete / restore: one tx with the
3621 // audit row when audited, a plain UPDATE otherwise.
3622 let make_pool_update = |op_path: TokenStream2, written: TokenStream2| -> TokenStream2 {
3623 if audited_fields.is_some() {
3624 let entry = make_entry(op_path, Some(written));
3625 quote! {
3626 #entry
3627 #root::audit::save_one_with_audit(pool, &_query, &_audit_entry).await
3628 }
3629 } else {
3630 quote!(#root::sql::update_pool(pool, &_query).await)
3631 }
3632 };
3633 let pool_softdelete_run =
3634 make_pool_update(quote!(#root::audit::AuditOp::SoftDelete), soft_deleted_json);
3635 let pool_restore_run = make_pool_update(quote!(#root::audit::AuditOp::Restore), restored_json);
3636
3637 // `save_pool(&Pool)` — emitted for every model with a PK. The
3638 // audited `save_pool` is built below on the diff path; both share
3639 // `save_partial_methods`, which differ only in how they run `_query`.
3640 let save_ret = quote!(::core::result::Result<u64, #root::sql::ExecError>);
3641 let save_keep = quote!(matches!(_saved, ::core::result::Result::Ok(n) if n > 0));
3642 let save_partial_methods = |pk_ident: &syn::Ident,
3643 pk_column_lit: &str,
3644 run: TokenStream2|
3645 -> TokenStream2 {
3646 let assignments = &fields.update_assignments;
3647 // Pushed in step with `update_assignments`: one column each.
3648 let assignment_cols = &fields.upsert_update_columns;
3649 let write = restore_auto_now_unless(
3650 &fields.auto_now_idents,
3651 &save_ret,
3652 &save_keep,
3653 "e! {
3654 // Build only the wanted assignments, so an unnamed
3655 // `auto_now` field is not stamped.
3656 let mut _filtered: ::std::vec::Vec<#root::core::Assignment> =
3657 ::std::vec::Vec::with_capacity(_wanted_cols.len());
3658 #(
3659 if _wanted_cols.contains(#assignment_cols) {
3660 _filtered.push(#assignments);
3661 }
3662 )*
3663 if _filtered.is_empty() {
3664 // All field names valid, but they all map to
3665 // non-assignable slots (PK column, computed/
3666 // virtual fields, relations without an
3667 // assignment). Same no-op as an empty list.
3668 #root::__tracing::warn!(
3669 target: "rustango::save_partial",
3670 model = _schema.name,
3671 "save_partial: every named field maps to a non-assignable column — no-op"
3672 );
3673 return ::core::result::Result::Ok(0);
3674 }
3675 let _query = #root::core::UpdateQuery::new(
3676 _schema,
3677 _filtered,
3678 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3679 #pk_column_lit,
3680 #root::core::Op::Eq,
3681 ::core::convert::Into::<#root::core::SqlValue>::into(
3682 ::core::clone::Clone::clone(&self.#pk_ident)
3683 ),
3684 )),
3685 );
3686 #run
3687 },
3688 );
3689 quote! {
3690 /// Save (UPDATE) only the listed Rust-side fields,
3691 /// leaving every other column untouched. Issue #66.
3692 ///
3693 /// `fields` are Rust-side struct field names; the macro
3694 /// resolves each to its SQL column. Unknown field
3695 /// names return [`#root::core::QueryError::UnknownField`]
3696 /// wrapped in `ExecError::Query`. An empty list is a
3697 /// no-op (returns `Ok(())` and logs a `tracing::warn!`).
3698 /// On an audited model the audit diff covers only these fields.
3699 ///
3700 /// Use this when:
3701 /// * you only mutated a couple of fields on a wide row
3702 /// (avoid re-writing every column on every save), or
3703 /// * two writers diverged after their initial read and
3704 /// you want to preserve the other writer's changes to
3705 /// columns you didn't touch.
3706 ///
3707 /// Auto-PK models with an unset PK return
3708 /// [`#root::core::QueryError::UnknownField`] with
3709 /// field name `<pk>` — `save_partial` is an
3710 /// UPDATE-only path. Call [`Self::insert_pool`]
3711 /// (or [`Self::save_pool`] which dispatches based on
3712 /// PK state) for the INSERT case.
3713 ///
3714 /// # Errors
3715 /// As [`Self::save_pool`], plus `UnknownField` for
3716 /// unknown / empty / Auto-Unset cases.
3717 pub async fn save_partial(
3718 &mut self,
3719 fields: &[&str],
3720 pool: &#root::sql::Pool,
3721 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3722 if fields.is_empty() {
3723 #root::__tracing::warn!(
3724 target: "rustango::save_partial",
3725 model = <Self as #root::core::Model>::SCHEMA.name,
3726 "save_partial called with empty field list — no-op"
3727 );
3728 return ::core::result::Result::Ok(0);
3729 }
3730 let _schema = <Self as #root::core::Model>::SCHEMA;
3731 // Validate field names against the schema.
3732 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3733 ::std::collections::HashSet::with_capacity(fields.len());
3734 for f in fields {
3735 match _schema.field(f) {
3736 ::core::option::Option::Some(fs) => {
3737 _wanted_cols.insert(fs.column);
3738 }
3739 ::core::option::Option::None => {
3740 return ::core::result::Result::Err(
3741 #root::sql::ExecError::Query(
3742 #root::core::QueryError::UnknownField {
3743 model: _schema.name,
3744 field: (*f).to_owned(),
3745 }
3746 )
3747 );
3748 }
3749 }
3750 }
3751 #write
3752 }
3753
3754 /// Typed-column counterpart of [`Self::save_partial`] —
3755 /// issue #67. `fields` is a tuple of [`Column`]
3756 /// constants whose `Model` matches `Self`; typos and
3757 /// model mismatches surface at *compile time*
3758 /// (`Author::name` inside a `Post::save_partial_typed`
3759 /// call is a type error, no runtime check).
3760 ///
3761 /// ```ignore
3762 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3763 /// ```
3764 ///
3765 /// Lowers to [`Self::save_partial`] under the hood — the
3766 /// tuple is reduced to a `&[&str]` slice of Rust-side
3767 /// field names and forwarded.
3768 ///
3769 /// [`Column`]: #root::core::Column
3770 ///
3771 /// # Errors
3772 /// As [`Self::save_partial`].
3773 pub async fn save_partial_typed<
3774 L: #root::core::TypedFieldList<Self>,
3775 >(
3776 &mut self,
3777 fields: L,
3778 pool: &#root::sql::Pool,
3779 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3780 let _names = fields.rust_field_names();
3781 let _refs: ::std::vec::Vec<&str> =
3782 _names.iter().copied().collect();
3783 self.save_partial(&_refs, pool).await
3784 }
3785 }
3786 };
3787 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3788 let pk_column_lit = pk_col.as_str();
3789 let assignments = &fields.update_assignments;
3790 if audited_fields.is_some() {
3791 quote!()
3792 } else {
3793 let partial = save_partial_methods(
3794 pk_ident,
3795 pk_column_lit,
3796 quote!(#root::sql::update_pool(pool, &_query).await),
3797 );
3798 let dispatch_unset = if fields.pk_is_auto {
3799 quote! {
3800 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3801 return self.insert_pool(pool).await.map(|()| 1u64);
3802 }
3803 }
3804 } else {
3805 quote!()
3806 };
3807 let write = restore_auto_now_unless(
3808 &fields.auto_now_idents,
3809 &save_ret,
3810 &save_keep,
3811 "e! {
3812 let _query = #root::core::UpdateQuery::new(
3813 <Self as #root::core::Model>::SCHEMA,
3814 ::std::vec![ #( #assignments ),* ],
3815 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3816 #pk_column_lit,
3817 #root::core::Op::Eq,
3818 ::core::convert::Into::<#root::core::SqlValue>::into(
3819 ::core::clone::Clone::clone(&self.#pk_ident)
3820 ),
3821 )),
3822 );
3823 let _affected = #root::sql::update_pool(pool, &_query).await?;
3824 ::core::result::Result::Ok(_affected)
3825 },
3826 );
3827 quote! {
3828 /// Save this row to its table against either backend.
3829 /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3830 /// `UPDATE` keyed on the PK; global scopes do not apply.
3831 ///
3832 /// # Errors
3833 /// As [`Self::save`].
3834 pub async fn save_pool(
3835 &mut self,
3836 pool: &#root::sql::Pool,
3837 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3838 #dispatch_unset
3839 #write
3840 }
3841
3842 #partial
3843 }
3844 }
3845 } else {
3846 quote!()
3847 };
3848
3849 // Audited `insert_pool` (overrides the placeholder set higher up
3850 // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3851 // audited models get insert_pool routing through
3852 // audit::insert_one_with_audit (per-backend tx wraps INSERT
3853 // + auto-PK readback + audit emit). Snapshot-style audit (the
3854 // PendingEntry's `changes` carries post-write field values).
3855 let pool_insert_method = if audited_fields.is_some() {
3856 if let Some(_) = primary_key {
3857 let pushes = if fields.has_auto {
3858 fields.insert_pushes.clone()
3859 } else {
3860 // For non-Auto-PK models, the macro normally builds
3861 // {columns, values} from fields.insert_columns +
3862 // fields.insert_values rather than insert_pushes.
3863 // Map those into the pushes shape.
3864 fields
3865 .insert_columns
3866 .iter()
3867 .zip(&fields.insert_values)
3868 .map(|(col, val)| {
3869 quote! {
3870 _columns.push(#col);
3871 _values.push(#val);
3872 }
3873 })
3874 .collect()
3875 };
3876 // Rust-filled Auto PKs (`default_uuid_v7`) leave the list empty (#1934).
3877 let returning_cols: Vec<proc_macro2::TokenStream> = if !fields.returning_cols.is_empty()
3878 {
3879 fields.returning_cols.clone()
3880 } else {
3881 // Non-Auto-PK: still need RETURNING something for the
3882 // audit helper's contract (it errors on empty
3883 // returning). Return the PK column so the audit row
3884 // can carry the assigned PK back. Some non-Auto PKs
3885 // are server-side-default (e.g. UUIDv4 default), so
3886 // RETURNING is genuinely useful.
3887 primary_key
3888 .map(|(_, col)| {
3889 let lit = col.as_str();
3890 vec![quote!(#lit)]
3891 })
3892 .unwrap_or_default()
3893 };
3894 quote! {
3895 /// Insert this row against either backend with audit
3896 /// emission inside the same transaction. Bi-dialect
3897 /// counterpart of [`Self::insert`] for audited models.
3898 ///
3899 /// Snapshot-style audit (post-write field values).
3900 ///
3901 /// # Errors
3902 /// As [`Self::insert`].
3903 pub async fn insert_pool(
3904 &mut self,
3905 pool: &#root::sql::Pool,
3906 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3907 let mut _columns: ::std::vec::Vec<&'static str> =
3908 ::std::vec::Vec::new();
3909 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3910 ::std::vec::Vec::new();
3911 #( #pushes )*
3912 let _query = #root::core::InsertQuery::new(
3913 <Self as #root::core::Model>::SCHEMA,
3914 _columns,
3915 _values,
3916 )
3917 .returning(::std::vec![ #( #returning_cols ),* ]);
3918 #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self| {
3919 _m.__rustango_audit_entry(#root::audit::AuditOp::Create)
3920 })
3921 .await
3922 }
3923 }
3924 } else {
3925 quote!()
3926 }
3927 } else {
3928 // Keep the non-audited pool_insert_method we built earlier.
3929 pool_insert_method
3930 };
3931
3932 // Update audited save_pool: now that insert_pool is wired for
3933 // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3934 // insert_pool. Non-audited save_pool already does this.
3935 // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3936 // Replaces the snapshot-only emission with a per-backend transaction
3937 // body that:
3938 // 1. SELECTs the tracked columns by PK (typed Row::try_get per
3939 // column), capturing BEFORE values
3940 // 2. compiles the UPDATE via pool.dialect() and runs it on the tx
3941 // 3. builds AFTER pairs from &self
3942 // 4. diffs BEFORE/AFTER, emits one PendingEntry with
3943 // AuditOp::Update + diff_changes(...) on the same tx connection
3944 // 5. commits
3945 //
3946 // Per-backend arms inline the SQL string + placeholder shape, then
3947 // share the `audit_before_pair_tokens` decoder block (Row::try_get
3948 // is polymorphic over Row type — the same tokens work against
3949 // PgRow and MySqlRow as long as the field's Rust type implements
3950 // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3951 // primitives + chrono/uuid/serde_json::Value all do).
3952 let pool_save_method = if let Some(tracked) = audited_fields {
3953 if let Some((pk_ident, pk_col)) = primary_key {
3954 let pk_column_lit = pk_col.as_str();
3955 let partial = save_partial_methods(
3956 pk_ident,
3957 pk_column_lit,
3958 quote! {
3959 self.__rustango_save_with_diff(
3960 pool,
3961 &_query,
3962 ::core::option::Option::Some(&_wanted_cols),
3963 )
3964 .await
3965 },
3966 );
3967 // Per-tracked-column BEFORE-pair token list. Each entry
3968 // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3969 // The Row alias resolves to PgRow / MySqlRow per call site,
3970 // so the same template generates both the PG and MySQL bodies.
3971 let mk_before_pairs =
3972 |getter: &dyn Fn(&ColumnEntry) -> TokenStream2| -> Vec<TokenStream2> {
3973 tracked
3974 .iter()
3975 .map(|c| {
3976 let getter = getter(c);
3977 let column_lit = c.column.as_str();
3978 let value_ty = &c.value_ty;
3979 quote! {
3980 (
3981 #column_lit,
3982 match #getter::<#value_ty>(
3983 _audit_before_row, #column_lit,
3984 ) {
3985 ::core::result::Result::Ok(v) => {
3986 #root::core::__json_value(&v)
3987 }
3988 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3989 },
3990 )
3991 }
3992 })
3993 .collect()
3994 };
3995 let before_pairs_pg = mk_before_pairs(&|_| quote!(#root::sql::try_get_returning));
3996 let before_pairs_my = mk_before_pairs(&|c| {
3997 if c.uuid_column {
3998 quote!(#root::sql::try_get_flat_my)
3999 } else {
4000 quote!(#root::sql::try_get_returning_my)
4001 }
4002 });
4003 let before_pairs_sqlite =
4004 mk_before_pairs(&|_| quote!(#root::sql::try_get_returning_sqlite));
4005 let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
4006 let pk_value_for_bind = if fields.pk_is_auto {
4007 quote!(self.#pk_ident.get().copied().unwrap_or_default())
4008 } else {
4009 quote!(::core::clone::Clone::clone(&self.#pk_ident))
4010 };
4011 let assignments = &fields.update_assignments;
4012 let unset_dispatch = if fields.has_auto {
4013 quote! {
4014 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
4015 return self.insert_pool(pool).await.map(|()| 1u64);
4016 }
4017 }
4018 } else {
4019 quote!()
4020 };
4021 let write = restore_auto_now_unless(
4022 &fields.auto_now_idents,
4023 &save_ret,
4024 &save_keep,
4025 "e! {
4026 let _query = #root::core::UpdateQuery::new(
4027 <Self as #root::core::Model>::SCHEMA,
4028 ::std::vec![ #( #assignments ),* ],
4029 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
4030 #pk_column_lit,
4031 #root::core::Op::Eq,
4032 ::core::convert::Into::<#root::core::SqlValue>::into(
4033 ::core::clone::Clone::clone(&self.#pk_ident)
4034 ),
4035 )),
4036 );
4037 self.__rustango_save_with_diff(pool, &_query, ::core::option::Option::None)
4038 .await
4039 },
4040 );
4041 quote! {
4042 /// Save this row against either backend with audit
4043 /// emission (diff-style: BEFORE+AFTER) inside the
4044 /// same transaction. Auto::Unset PK routes to
4045 /// insert_pool. Bi-dialect counterpart of
4046 /// [`Self::save`] for audited models.
4047 ///
4048 /// The audit row's `changes` JSON contains one
4049 /// `{ "field": { "before": …, "after": … } }` entry
4050 /// per tracked column whose value actually changed
4051 /// — same shape as the existing &PgPool save() emits.
4052 /// Keyed on the PK; global scopes do not apply.
4053 ///
4054 /// # Errors
4055 /// As [`Self::save`].
4056 pub async fn save_pool(
4057 &mut self,
4058 pool: &#root::sql::Pool,
4059 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4060 #unset_dispatch
4061 #write
4062 }
4063
4064 /// Run `query` and write its diff entry, limited to `only` columns.
4065 #[doc(hidden)]
4066 async fn __rustango_save_with_diff(
4067 &self,
4068 pool: &#root::sql::Pool,
4069 query: &#root::core::UpdateQuery,
4070 only: ::core::option::Option<&::std::collections::HashSet<&'static str>>,
4071 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4072 let mut _after_pairs = self.__rustango_audit_pairs();
4073 if let ::core::option::Option::Some(only) = only {
4074 _after_pairs.retain(|(col, _)| only.contains(col));
4075 }
4076 let _before_query = #root::audit::before_image_query(
4077 <Self as #root::core::Model>::SCHEMA,
4078 #pk_column_lit,
4079 ::core::convert::Into::<#root::core::SqlValue>::into(
4080 #pk_value_for_bind,
4081 ),
4082 &[ #( #tracked_cols ),* ],
4083 );
4084 #root::audit::save_one_with_diff(
4085 pool,
4086 query,
4087 &_before_query,
4088 <Self as #root::core::Model>::SCHEMA.table,
4089 self.__rustango_audit_pk(),
4090 _after_pairs,
4091 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
4092 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
4093 |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
4094 ).await
4095 }
4096
4097 #partial
4098 }
4099 } else {
4100 quote!()
4101 }
4102 } else {
4103 pool_save_method
4104 };
4105
4106 // `delete_pool(&Pool)` — emitted for every model with a PK. Two
4107 // body shapes:
4108 // - non-audited: simple dispatch through `sql::delete_pool`
4109 // - audited: routes through `audit::delete_one_with_audit`,
4110 // which opens a per-backend transaction wrapping DELETE +
4111 // audit emit so the data write and audit row commit atomically.
4112 let pool_delete_method = {
4113 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
4114 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
4115 if let Some(pk_ident) = pk_ident_for_pool {
4116 if audited_fields.is_some() {
4117 quote! {
4118 /// Delete this row against either backend with audit
4119 /// emission inside the same transaction. Bi-dialect
4120 /// counterpart of [`Self::delete`] for audited models.
4121 /// Keyed on the PK; global scopes do not apply.
4122 ///
4123 /// # Errors
4124 /// As [`Self::delete`].
4125 pub async fn delete_pool(
4126 &self,
4127 pool: &#root::sql::Pool,
4128 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4129 let _query = #root::core::DeleteQuery::by_pk(
4130 <Self as #root::core::Model>::SCHEMA,
4131 #pk_column_lit,
4132 ::core::convert::Into::<#root::core::SqlValue>::into(
4133 ::core::clone::Clone::clone(&self.#pk_ident)
4134 ),
4135 );
4136 let _audit_entry =
4137 self.__rustango_audit_entry(#root::audit::AuditOp::Delete);
4138 #root::audit::delete_one_with_audit(
4139 pool, &_query, &_audit_entry,
4140 ).await
4141 }
4142 }
4143 } else {
4144 quote! {
4145 /// Delete the row identified by this instance's primary key
4146 /// against either backend. Equivalent to [`Self::delete`] but
4147 /// takes [`#root::sql::Pool`] and dispatches per backend.
4148 /// Global scopes do not apply.
4149 ///
4150 /// # Errors
4151 /// As [`Self::delete`].
4152 pub async fn delete_pool(
4153 &self,
4154 pool: &#root::sql::Pool,
4155 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4156 let _query = #root::core::DeleteQuery::by_pk(
4157 <Self as #root::core::Model>::SCHEMA,
4158 #pk_column_lit,
4159 ::core::convert::Into::<#root::core::SqlValue>::into(
4160 ::core::clone::Clone::clone(&self.#pk_ident)
4161 ),
4162 );
4163 #root::sql::delete_pool(pool, &_query).await
4164 }
4165 }
4166 }
4167 } else {
4168 quote!()
4169 }
4170 };
4171
4172 // Bulk writers on audited models write one audit row per affected
4173 // row, in the write's transaction (#1747).
4174 let (bulk_delete_run, bulk_update_run, truncate_run) = if audited_fields.is_some() {
4175 (
4176 quote!(#root::audit::delete_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4177 _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4178 })
4179 .await),
4180 quote!(#root::audit::update_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4181 _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
4182 })
4183 .await),
4184 quote! {
4185 let _table = <Self as #root::core::Model>::SCHEMA.table;
4186 let _sql = #root::sql::truncate_sql(pool.dialect(), _table);
4187 #root::audit::truncate_with_audit(pool, _table, &_sql).await
4188 },
4189 )
4190 } else {
4191 (
4192 quote!(#root::sql::delete_pool(pool, &_query).await),
4193 quote!(#root::sql::update_pool(pool, &_query).await),
4194 quote!(#root::sql::truncate(pool, <Self as #root::core::Model>::SCHEMA).await),
4195 )
4196 };
4197
4198 // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
4199 // matching this instance's PK and overwrite the in-memory state
4200 // with the freshly-fetched columns.
4201 // Issue #825. Only emitted when the model declares a PK; non-PK
4202 // models can't address a specific row.
4203 //
4204 // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
4205 // every field from `self`; resets the PK to `Auto::Unset` when
4206 // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
4207 // a fresh autoincrement value. Non-Auto PKs preserve the source
4208 // PK — the caller must overwrite before insert. Pure-Rust, no
4209 // I/O, no dialect surface.
4210 let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
4211 let other_field_clones: Vec<TokenStream2> = fields
4212 .column_entries
4213 .iter()
4214 .filter(|c| &c.ident != pk_ident)
4215 .map(|c| {
4216 let ident = &c.ident;
4217 quote! {
4218 #ident: ::core::clone::Clone::clone(&self.#ident)
4219 }
4220 })
4221 .collect();
4222 let pk_clone_token = if fields.pk_is_auto {
4223 quote! { #pk_ident: #root::sql::Auto::Unset }
4224 } else {
4225 quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
4226 };
4227 let replicate_doc = if fields.pk_is_auto {
4228 quote! {
4229 /// Eloquent-style `replicate()` — return a clone of this
4230 /// row with the primary key reset to [`Auto::Unset`] so
4231 /// the copy is ready for `insert_pool` / `save_pool` to
4232 /// allocate a fresh autoincrement value. Every other
4233 /// field is `Clone`d verbatim. Issue #825.
4234 ///
4235 /// `auto_now_add` / `auto_now` timestamp fields are
4236 /// **not** reset (Eloquent's `replicate` doesn't reset
4237 /// them either) — pass them through the normal insert
4238 /// path if you want fresh values, or assign them
4239 /// explicitly after the call.
4240 }
4241 } else {
4242 quote! {
4243 /// Eloquent-style `replicate()` — clone this row
4244 /// verbatim. Because the primary key is **not** an
4245 /// `Auto<T>`, the clone keeps the source PK; the
4246 /// caller must overwrite `copy.<pk>` before inserting
4247 /// to avoid a unique-key violation. Issue #825.
4248 }
4249 };
4250 // 2026-06-07 — field-name / shortcut collision guard.
4251 //
4252 // The macro emits both `pub const <field>: <field>_col = ...`
4253 // (per-field typed-column const, used by the typed-builder
4254 // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
4255 // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
4256 // model has a field named e.g. `count`, both items would land
4257 // in the same inherent impl with the same name, and the
4258 // compiler rejects the derive with "duplicate definitions".
4259 //
4260 // Drop the conflicting shortcut for that model. Callers can
4261 // still reach the same behavior via
4262 // `QuerySet::<Model>::default().count(&pool)`.
4263 let column_names: ::std::collections::HashSet<String> = fields
4264 .column_entries
4265 .iter()
4266 .map(|c| ident_name(&c.ident))
4267 .collect();
4268 let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
4269 if column_names.contains(name) {
4270 quote! {}
4271 } else {
4272 tokens
4273 }
4274 };
4275 let count_method = emit_if_no_field_collision(
4276 "count",
4277 quote! {
4278 /// Count rows of this model — `SELECT COUNT(*) FROM
4279 /// <table>`. Eloquent `Model::count()` parity.
4280 ///
4281 /// Skipped on models that already declare a field named
4282 /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
4283 /// in that case.
4284 ///
4285 /// # Errors
4286 /// As [`CounterPool::count`].
4287 ///
4288 /// [`CounterPool::count`]: rustango::sql::CounterPool::count
4289 pub async fn count(
4290 pool: &#root::sql::Pool,
4291 ) -> ::core::result::Result<i64, #root::sql::ExecError> {
4292 use #root::sql::CounterPool as _;
4293 #root::query::QuerySet::<Self>::default()
4294 .count(pool)
4295 .await
4296 }
4297 },
4298 );
4299 let value_method = emit_if_no_field_collision(
4300 "value",
4301 quote! {
4302 /// Pluck a single scalar from the first row.
4303 /// Eloquent `Model::query()->value($col)` parity.
4304 ///
4305 /// Skipped on models that already declare a field named
4306 /// `value`. Drop into
4307 /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4308 ///
4309 /// # Errors
4310 /// As `ValuesFlatQuerySet::first`.
4311 pub async fn value<U>(
4312 col: &str,
4313 pool: &#root::sql::Pool,
4314 ) -> ::core::result::Result<
4315 ::core::option::Option<U>,
4316 #root::sql::ExecError,
4317 >
4318 where
4319 U: #root::sql::FlatScalar,
4320 {
4321 let _col_static: &'static str = Self::__resolve_col(col)?;
4322 #root::query::QuerySet::<Self>::default()
4323 .values_list_flat(_col_static)
4324 .first::<U>(pool)
4325 .await
4326 }
4327 },
4328 );
4329 let sum_method = emit_if_no_field_collision(
4330 "sum",
4331 quote! {
4332 /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4333 /// Skipped on models that already declare a field named
4334 /// `sum`.
4335 ///
4336 /// # Errors
4337 /// As [`#root::sql::fetch_aggregate_pool`].
4338 pub async fn sum<U>(
4339 col: &str,
4340 pool: &#root::sql::Pool,
4341 ) -> ::core::result::Result<
4342 ::core::option::Option<U>,
4343 #root::sql::ExecError,
4344 >
4345 where
4346 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4347 + #root::sql::MaybeMyFromRow
4348 + #root::sql::MaybeSqliteFromRow
4349 + ::core::marker::Send
4350 + ::core::marker::Unpin,
4351 {
4352 #root::query::QuerySet::<Self>::default()
4353 .sum::<U>(col, pool)
4354 .await
4355 }
4356 },
4357 );
4358 let avg_method = emit_if_no_field_collision(
4359 "avg",
4360 quote! {
4361 /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4362 /// models that already declare a field named `avg`.
4363 ///
4364 /// # Errors
4365 /// As [`#root::sql::fetch_aggregate_pool`].
4366 pub async fn avg<U>(
4367 col: &str,
4368 pool: &#root::sql::Pool,
4369 ) -> ::core::result::Result<
4370 ::core::option::Option<U>,
4371 #root::sql::ExecError,
4372 >
4373 where
4374 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4375 + #root::sql::MaybeMyFromRow
4376 + #root::sql::MaybeSqliteFromRow
4377 + ::core::marker::Send
4378 + ::core::marker::Unpin,
4379 {
4380 #root::query::QuerySet::<Self>::default()
4381 .avg::<U>(col, pool)
4382 .await
4383 }
4384 },
4385 );
4386 let min_method = emit_if_no_field_collision(
4387 "min",
4388 quote! {
4389 /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4390 /// models that already declare a field named `min`.
4391 ///
4392 /// # Errors
4393 /// As [`#root::sql::fetch_aggregate_pool`].
4394 pub async fn min<U>(
4395 col: &str,
4396 pool: &#root::sql::Pool,
4397 ) -> ::core::result::Result<
4398 ::core::option::Option<U>,
4399 #root::sql::ExecError,
4400 >
4401 where
4402 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4403 + #root::sql::MaybeMyFromRow
4404 + #root::sql::MaybeSqliteFromRow
4405 + ::core::marker::Send
4406 + ::core::marker::Unpin,
4407 {
4408 #root::query::QuerySet::<Self>::default()
4409 .min::<U>(col, pool)
4410 .await
4411 }
4412 },
4413 );
4414 let max_method = emit_if_no_field_collision(
4415 "max",
4416 quote! {
4417 /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4418 /// models that already declare a field named `max`.
4419 ///
4420 /// # Errors
4421 /// As [`#root::sql::fetch_aggregate_pool`].
4422 pub async fn max<U>(
4423 col: &str,
4424 pool: &#root::sql::Pool,
4425 ) -> ::core::result::Result<
4426 ::core::option::Option<U>,
4427 #root::sql::ExecError,
4428 >
4429 where
4430 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4431 + #root::sql::MaybeMyFromRow
4432 + #root::sql::MaybeSqliteFromRow
4433 + ::core::marker::Send
4434 + ::core::marker::Unpin,
4435 {
4436 #root::query::QuerySet::<Self>::default()
4437 .max::<U>(col, pool)
4438 .await
4439 }
4440 },
4441 );
4442 let first_method = emit_if_no_field_collision(
4443 "first",
4444 quote! {
4445 /// First row of this model. Eloquent `Model::first()`.
4446 /// Skipped on models that already declare a field named
4447 /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4448 ///
4449 /// # Errors
4450 /// As `QuerySet::first`.
4451 pub async fn first(
4452 pool: &#root::sql::Pool,
4453 ) -> ::core::result::Result<
4454 ::core::option::Option<Self>,
4455 #root::sql::ExecError,
4456 > {
4457 #root::query::QuerySet::<Self>::default()
4458 .first(pool)
4459 .await
4460 }
4461 },
4462 );
4463 let last_method = emit_if_no_field_collision(
4464 "last",
4465 quote! {
4466 /// Last row of this model by primary-key DESC.
4467 /// Eloquent `Model::query()->latest('id')->first()`
4468 /// parity — fetches the highest-PK row without
4469 /// requiring the caller to spell the PK column.
4470 /// Returns `None` on an empty table.
4471 ///
4472 /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4473 /// Skipped on models that already declare a field
4474 /// named `last`.
4475 ///
4476 /// # Errors
4477 /// As `QuerySet::last`.
4478 pub async fn last(
4479 pool: &#root::sql::Pool,
4480 ) -> ::core::result::Result<
4481 ::core::option::Option<Self>,
4482 #root::sql::ExecError,
4483 > {
4484 #root::query::QuerySet::<Self>::default()
4485 .last(pool)
4486 .await
4487 }
4488 },
4489 );
4490 quote! {
4491 /// Re-SELECT this row by its primary key and overwrite
4492 /// every in-memory field with the freshly-fetched value.
4493 /// Issue #825.
4494 ///
4495 /// Use this when the row may have been modified by another
4496 /// process / connection / job since you read it — e.g. after
4497 /// a queued task callback, or to re-sync stale UI state
4498 /// before re-saving.
4499 ///
4500 /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4501 /// when the primary key no longer matches any row (e.g.
4502 /// the row was deleted concurrently).
4503 ///
4504 /// # Errors
4505 /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4506 /// the PK no longer exists.
4507 ///
4508 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4509 pub async fn refresh_from_db(
4510 &mut self,
4511 pool: &#root::sql::Pool,
4512 ) -> ::core::result::Result<(), #root::sql::ExecError> {
4513 use #root::sql::FetcherPool as _;
4514 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4515 ::core::clone::Clone::clone(&self.#pk_ident),
4516 );
4517 let mut _rows: ::std::vec::Vec<Self> =
4518 #root::query::QuerySet::<Self>::default()
4519 .filter(::core::stringify!(#pk_ident), _pk_val)
4520 .limit(1)
4521 .fetch(pool)
4522 .await?;
4523 match _rows.into_iter().next() {
4524 ::core::option::Option::Some(_fresh) => {
4525 *self = _fresh;
4526 ::core::result::Result::Ok(())
4527 }
4528 ::core::option::Option::None => ::core::result::Result::Err(
4529 #root::sql::ExecError::Driver(
4530 #root::sql::sqlx::Error::RowNotFound,
4531 ),
4532 ),
4533 }
4534 }
4535
4536 /// Atomically increment the integer column `col` by
4537 /// `by` for this row. Equivalent to
4538 /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4539 ///
4540 /// **Doesn't mutate `self`** — the in-memory copy is now
4541 /// stale; call [`Self::refresh_from_db`] /
4542 /// [`Self::fresh`] to re-sync. Returns the rows-
4543 /// affected count (0 when the PK doesn't match any row,
4544 /// 1 on success).
4545 ///
4546 /// `col` is the Rust field name as a string; unknown
4547 /// fields surface as `UnknownField` at runtime. Negative
4548 /// `by` values atomically decrement (see also
4549 /// [`Self::decrement`]).
4550 ///
4551 /// # Errors
4552 /// As [`UpdaterPool::execute_pool`].
4553 ///
4554 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4555 pub async fn increment(
4556 &self,
4557 col: &str,
4558 by: i64,
4559 pool: &#root::sql::Pool,
4560 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4561 Self::__increment_one(self, col, by, pool).await
4562 }
4563
4564 /// Sibling of [`Self::increment`] — atomically
4565 /// decrement this row's `col` by `by`. Eloquent
4566 /// `$model->decrement($col, $by)` parity. Equivalent to
4567 /// `self.increment(col, -by, &pool)`; the separate name
4568 /// keeps call sites readable.
4569 ///
4570 /// # Errors
4571 /// As [`Self::increment`].
4572 pub async fn decrement(
4573 &self,
4574 col: &str,
4575 by: i64,
4576 pool: &#root::sql::Pool,
4577 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4578 Self::__increment_one(self, col, -by, pool).await
4579 }
4580
4581 /// Bulk-increment: add `by` to `col` on every row of the
4582 /// table. Eloquent `Model::query()->increment($col, $by)`
4583 /// parity. Use for counters, score adjustments, view
4584 /// rollups.
4585 ///
4586 /// # Errors
4587 /// As [`UpdaterPool::execute_pool`].
4588 ///
4589 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4590 pub async fn increment_each(
4591 col: &str,
4592 by: i64,
4593 pool: &#root::sql::Pool,
4594 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4595 Self::__increment_all(col, by, pool).await
4596 }
4597
4598 /// Sibling of [`Self::increment_each`] — bulk-decrement.
4599 ///
4600 /// # Errors
4601 /// As [`Self::increment_each`].
4602 pub async fn decrement_each(
4603 col: &str,
4604 by: i64,
4605 pool: &#root::sql::Pool,
4606 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4607 Self::__increment_all(col, -by, pool).await
4608 }
4609
4610 /// Internal: forward to
4611 /// [`#root::sql::model_shortcuts::increment_one_pool`].
4612 /// One-line wrapper kept as a per-Model method so the
4613 /// macro's emitted `increment` / `decrement` instance
4614 /// calls don't have to thread `Self` through manually.
4615 #[doc(hidden)]
4616 pub async fn __increment_one(
4617 this: &Self,
4618 col: &str,
4619 by: i64,
4620 pool: &#root::sql::Pool,
4621 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4622 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4623 ::core::clone::Clone::clone(&this.#pk_ident),
4624 );
4625 #root::sql::model_shortcuts::increment_one_pool::<Self>(
4626 ::core::stringify!(#pk_ident),
4627 _pk_val,
4628 col,
4629 by,
4630 pool,
4631 )
4632 .await
4633 }
4634
4635 /// Internal: run
4636 /// [`#root::sql::model_shortcuts::increment_all_query`].
4637 #[doc(hidden)]
4638 pub async fn __increment_all(
4639 col: &str,
4640 by: i64,
4641 pool: &#root::sql::Pool,
4642 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4643 let _query =
4644 #root::sql::model_shortcuts::increment_all_query::<Self>(col, by)?;
4645 #bulk_update_run
4646 }
4647
4648 /// Internal: forward to
4649 /// [`#root::sql::model_shortcuts::resolve_col`].
4650 #[doc(hidden)]
4651 pub fn __resolve_col(
4652 col: &str,
4653 ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4654 #root::sql::model_shortcuts::resolve_col::<Self>(col)
4655 }
4656
4657 /// Internal: forward to
4658 /// [`#root::sql::model_shortcuts::add_signed_expr`].
4659 #[doc(hidden)]
4660 #[must_use]
4661 pub fn __add_signed_expr(
4662 col_static: &'static str,
4663 signed_by: i64,
4664 ) -> #root::core::Expr {
4665 #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4666 }
4667
4668 /// Re-SELECT this row by its primary key and return a
4669 /// **new** instance with the freshly-fetched fields.
4670 /// Eloquent `Model::fresh()` parity — non-mutating
4671 /// counterpart of [`Self::refresh_from_db`].
4672 ///
4673 /// Returns `Ok(None)` when the row was deleted
4674 /// concurrently — vs [`Self::refresh_from_db`]
4675 /// which surfaces that as `RowNotFound` because
4676 /// in-place mutation has nothing to write to.
4677 ///
4678 /// Useful when you want to compare the in-memory
4679 /// instance against the persisted state (audit-style
4680 /// diffs, conflict detection) without mutating the
4681 /// reference you already hold.
4682 ///
4683 /// # Errors
4684 /// As [`FetcherPool::fetch`].
4685 ///
4686 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4687 pub async fn fresh(
4688 &self,
4689 pool: &#root::sql::Pool,
4690 ) -> ::core::result::Result<
4691 ::core::option::Option<Self>,
4692 #root::sql::ExecError,
4693 > {
4694 use #root::sql::FetcherPool as _;
4695 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4696 ::core::clone::Clone::clone(&self.#pk_ident),
4697 );
4698 let _rows: ::std::vec::Vec<Self> =
4699 #root::query::QuerySet::<Self>::default()
4700 .filter(::core::stringify!(#pk_ident), _pk_val)
4701 .limit(1)
4702 .fetch(pool)
4703 .await?;
4704 ::core::result::Result::Ok(_rows.into_iter().next())
4705 }
4706
4707 #replicate_doc
4708 #[must_use]
4709 pub fn replicate(&self) -> Self {
4710 Self {
4711 #pk_clone_token,
4712 #( #other_field_clones, )*
4713 }
4714 }
4715
4716 #first_method
4717
4718 #last_method
4719
4720 /// Throwing counterpart of [`Self::first`] —
4721 /// errors with `RowNotFound` when the table is empty.
4722 /// Eloquent `Model::firstOrFail()` parity.
4723 ///
4724 /// # Errors
4725 /// As [`Self::first`]; additionally
4726 /// [`sqlx::Error::RowNotFound`] on empty tables.
4727 ///
4728 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4729 pub async fn first_or_fail(
4730 pool: &#root::sql::Pool,
4731 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4732 // Route through the queryset rather than `Self::first`,
4733 // which is suppressed on models with a field named
4734 // `first` (field/shortcut collision guard).
4735 match #root::query::QuerySet::<Self>::default().first(pool).await? {
4736 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4737 ::core::option::Option::None => ::core::result::Result::Err(
4738 #root::sql::ExecError::Driver(
4739 #root::sql::sqlx::Error::RowNotFound,
4740 ),
4741 ),
4742 }
4743 }
4744
4745 /// Single-column projection — `SELECT <col> FROM
4746 /// <table>`. Returns `Vec<U>` where each element is the
4747 /// decoded value of the column. Eloquent
4748 /// `Model::pluck($column)` parity.
4749 ///
4750 /// Thin wrapper over `QuerySet::<Self>::default()
4751 /// .values_list_flat(col).fetch::<U>(pool)`. `U` is a
4752 /// `FlatScalar`; use `Option<_>` for a nullable column.
4753 ///
4754 /// # Errors
4755 /// As `ValuesFlatQuerySet::fetch`.
4756 pub async fn pluck<U>(
4757 col: &'static str,
4758 pool: &#root::sql::Pool,
4759 ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4760 where
4761 U: #root::sql::FlatScalar,
4762 {
4763 #root::query::QuerySet::<Self>::default()
4764 .values_list_flat(col)
4765 .fetch::<U>(pool)
4766 .await
4767 }
4768
4769 /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4770 /// stream every row of this model in batches of `n`,
4771 /// invoking the callback once per batch. Stable PK-ASC
4772 /// ordering so the LIMIT/OFFSET pagination is
4773 /// deterministic across drivers.
4774 ///
4775 /// The callback is async — it can do further DB work
4776 /// (writes, related-row lookups, queue dispatch) per
4777 /// batch. Return `Err(...)` from the callback to abort
4778 /// the iteration early; the error bubbles up.
4779 ///
4780 /// **When to use**: bulk processing flows that can't
4781 /// fit the whole table in memory — sending newsletters,
4782 /// running data migrations, computing summary
4783 /// statistics. For small / known-bounded tables, plain
4784 /// `Self::all(&pool)` is simpler.
4785 ///
4786 /// **Caveat**: ascending OFFSET pagination is O(N²) on
4787 /// large tables. For multi-million-row scans prefer
4788 /// keyset-by-PK (the standard "WHERE id > last_seen"
4789 /// shape) over `chunk(...)`.
4790 ///
4791 /// Skipped on models without a primary key — chunking
4792 /// needs a stable order to avoid skipping / repeating
4793 /// rows across batches.
4794 pub async fn chunk<F, Fut>(
4795 n: i64,
4796 pool: &#root::sql::Pool,
4797 mut cb: F,
4798 ) -> ::core::result::Result<(), #root::sql::ExecError>
4799 where
4800 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4801 Fut: ::core::future::Future<
4802 Output = ::core::result::Result<(), #root::sql::ExecError>,
4803 >,
4804 {
4805 use #root::sql::FetcherPool as _;
4806 let pk_col = match Self::primary_key_column() {
4807 ::core::option::Option::Some(c) => c,
4808 ::core::option::Option::None => {
4809 return ::core::result::Result::Ok(());
4810 }
4811 };
4812 let mut offset: i64 = 0;
4813 loop {
4814 let rows: ::std::vec::Vec<Self> =
4815 #root::query::QuerySet::<Self>::default()
4816 .order_by(&[(pk_col, false)])
4817 .limit(n)
4818 .offset(offset)
4819 .fetch(pool)
4820 .await?;
4821 if rows.is_empty() {
4822 return ::core::result::Result::Ok(());
4823 }
4824 let len = rows.len() as i64;
4825 cb(rows).await?;
4826 if len < n {
4827 return ::core::result::Result::Ok(());
4828 }
4829 offset += n;
4830 }
4831 }
4832
4833 /// Eloquent `Model::chunkById($n, fn (...))` — same
4834 /// per-batch callback shape as [`Self::chunk`], but uses
4835 /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4836 /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4837 /// — the right choice for multi-million-row sweeps.
4838 ///
4839 /// Requires the primary key to be a signed integer type
4840 /// (`i64` / `i32`); the keyset comparison rides on
4841 /// `__rustango_pk_value()` lowering through
4842 /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4843 /// non-integer PKs (UUID, String) — those should use
4844 /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4845 /// keyset loop.
4846 ///
4847 /// Callback errors abort iteration; the error bubbles up
4848 /// unchanged. Empty table → callback invoked zero times.
4849 pub async fn chunk_by_id<F, Fut>(
4850 n: i64,
4851 pool: &#root::sql::Pool,
4852 mut cb: F,
4853 ) -> ::core::result::Result<(), #root::sql::ExecError>
4854 where
4855 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4856 Fut: ::core::future::Future<
4857 Output = ::core::result::Result<(), #root::sql::ExecError>,
4858 >,
4859 {
4860 use #root::sql::FetcherPool as _;
4861 let pk_col = match Self::primary_key_column() {
4862 ::core::option::Option::Some(c) => c,
4863 ::core::option::Option::None => {
4864 return ::core::result::Result::Ok(());
4865 }
4866 };
4867 // Track the largest PK seen so the next batch picks
4868 // up from there. `i64::MIN` as the sentinel — the
4869 // very first iteration's `> MIN` matches every row,
4870 // so the loop entry is uniform with subsequent
4871 // iterations.
4872 let mut last_seen: i64 = i64::MIN;
4873 loop {
4874 let key = ::std::format!("{}__gt", pk_col);
4875 let rows: ::std::vec::Vec<Self> =
4876 #root::query::QuerySet::<Self>::default()
4877 .filter(key.as_str(), last_seen)
4878 .order_by(&[(pk_col, false)])
4879 .limit(n)
4880 .fetch(pool)
4881 .await?;
4882 if rows.is_empty() {
4883 return ::core::result::Result::Ok(());
4884 }
4885 let len = rows.len() as i64;
4886 // Capture the last row's PK BEFORE moving rows
4887 // into the callback.
4888 let max_pk = match rows
4889 .last()
4890 .map(|r| r.__rustango_pk_value())
4891 {
4892 ::core::option::Option::Some(
4893 #root::core::SqlValue::I64(v),
4894 ) => v,
4895 ::core::option::Option::Some(
4896 #root::core::SqlValue::I32(v),
4897 ) => i64::from(v),
4898 _ => return ::core::result::Result::Ok(()),
4899 };
4900 cb(rows).await?;
4901 if len < n {
4902 return ::core::result::Result::Ok(());
4903 }
4904 last_seen = max_pk;
4905 }
4906 }
4907
4908 /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4909 /// per-row callback companion to [`Self::chunk`].
4910 /// Streams every row in keyset-paginated batches of
4911 /// `batch` size, calling `cb` once per row.
4912 ///
4913 /// Inherits the keyset-paginated scan of
4914 /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4915 /// non-integer PKs are silently a no-op).
4916 ///
4917 /// ```ignore
4918 /// Post::each(500, &pool, |p| async move {
4919 /// reindex(p).await?;
4920 /// Ok(())
4921 /// }).await?;
4922 /// ```
4923 ///
4924 /// Return `Err(...)` from the callback to abort
4925 /// iteration; the error bubbles up unchanged.
4926 pub async fn each<F, Fut>(
4927 batch: i64,
4928 pool: &#root::sql::Pool,
4929 mut cb: F,
4930 ) -> ::core::result::Result<(), #root::sql::ExecError>
4931 where
4932 F: ::core::ops::FnMut(Self) -> Fut,
4933 Fut: ::core::future::Future<
4934 Output = ::core::result::Result<(), #root::sql::ExecError>,
4935 >,
4936 {
4937 use #root::sql::FetcherPool as _;
4938 let pk_col = match Self::primary_key_column() {
4939 ::core::option::Option::Some(c) => c,
4940 ::core::option::Option::None => {
4941 return ::core::result::Result::Ok(());
4942 }
4943 };
4944 let mut last_seen: i64 = i64::MIN;
4945 loop {
4946 let key = ::std::format!("{}__gt", pk_col);
4947 let rows: ::std::vec::Vec<Self> =
4948 #root::query::QuerySet::<Self>::default()
4949 .filter(key.as_str(), last_seen)
4950 .order_by(&[(pk_col, false)])
4951 .limit(batch)
4952 .fetch(pool)
4953 .await?;
4954 if rows.is_empty() {
4955 return ::core::result::Result::Ok(());
4956 }
4957 let len = rows.len() as i64;
4958 let max_pk = match rows
4959 .last()
4960 .map(|r| r.__rustango_pk_value())
4961 {
4962 ::core::option::Option::Some(
4963 #root::core::SqlValue::I64(v),
4964 ) => v,
4965 ::core::option::Option::Some(
4966 #root::core::SqlValue::I32(v),
4967 ) => i64::from(v),
4968 _ => return ::core::result::Result::Ok(()),
4969 };
4970 for row in rows {
4971 cb(row).await?;
4972 }
4973 if len < batch {
4974 return ::core::result::Result::Ok(());
4975 }
4976 last_seen = max_pk;
4977 }
4978 }
4979
4980 /// Delete every row of this model — `TRUNCATE TABLE
4981 /// <table> RESTART IDENTITY CASCADE` on Postgres,
4982 /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4983 /// support `TRUNCATE` inside foreign-key constraints
4984 /// or — for SQLite — at all). Eloquent `Model::truncate()`
4985 /// parity.
4986 ///
4987 /// **Use only in tests / fixture-reset flows.** Production
4988 /// writes through this would silently bypass the
4989 /// `pre_delete` / `post_delete` signals (no per-row hooks
4990 /// fire on a TRUNCATE / bulk DELETE FROM). An audited model
4991 /// gets one bulk audit entry, not one per row. Global scopes
4992 /// do not apply: every row goes, generic M2M links too.
4993 ///
4994 /// # Errors
4995 /// As [`truncate`].
4996 ///
4997 /// [`truncate`]: rustango::sql::truncate
4998 pub async fn truncate(
4999 pool: &#root::sql::Pool,
5000 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5001 #truncate_run
5002 }
5003
5004 /// Bulk-delete every row whose primary key is in
5005 /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
5006 /// Returns the affected row count.
5007 ///
5008 /// Eloquent `Model::destroy([1, 2, 3])` parity.
5009 /// Empty `pks` is a no-op (returns 0).
5010 ///
5011 /// Accepts any iterable whose elements are
5012 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
5013 /// `[i64; N]`, etc.
5014 ///
5015 /// # Errors
5016 /// As `delete_pool`.
5017 pub async fn destroy<V>(
5018 pks: impl ::core::iter::IntoIterator<Item = V>,
5019 pool: &#root::sql::Pool,
5020 ) -> ::core::result::Result<u64, #root::sql::ExecError>
5021 where
5022 V: ::core::convert::Into<#root::core::SqlValue>,
5023 {
5024 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5025 pks.into_iter().map(::core::convert::Into::into).collect();
5026 if _values.is_empty() {
5027 return ::core::result::Result::Ok(0);
5028 }
5029 let _pk = <Self as #root::core::Model>::SCHEMA
5030 .primary_key()
5031 .ok_or_else(|| {
5032 #root::sql::ExecError::Sql(#root::sql::SqlError::MissingPrimaryKey)
5033 })?;
5034 let _query = #root::query::QuerySet::<Self>::default()
5035 .filter_op(
5036 _pk.name,
5037 #root::core::Op::In,
5038 #root::core::SqlValue::List(_values),
5039 )
5040 .compile_delete()?;
5041 #bulk_delete_run
5042 }
5043
5044 /// Fetch every row where `<col> = <val>`. Eloquent
5045 /// `Model::where($col, $val)->get()` parity.
5046 ///
5047 /// Thin wrapper over `QuerySet::<Self>::default()
5048 /// .filter(col, val).fetch(pool)`. For one row,
5049 /// use [`Self::first_where`]; for a chain that
5050 /// needs further `.filter()` / `.order_by()` /
5051 /// `.limit()`, drop down to `Self::query().filter(...)`
5052 /// directly.
5053 ///
5054 /// `val` accepts any value `Into<SqlValue>` so plain
5055 /// strings, ints, UUIDs, etc. all work.
5056 ///
5057 /// # Errors
5058 /// As [`FetcherPool::fetch`].
5059 ///
5060 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5061 pub async fn where_(
5062 col: &str,
5063 val: impl ::core::convert::Into<#root::core::SqlValue>,
5064 pool: &#root::sql::Pool,
5065 ) -> ::core::result::Result<
5066 ::std::vec::Vec<Self>,
5067 #root::sql::ExecError,
5068 > {
5069 use #root::sql::FetcherPool as _;
5070 #root::query::QuerySet::<Self>::default()
5071 .filter(col, val)
5072 .fetch(pool)
5073 .await
5074 }
5075
5076 /// Fetch every row where `<col> IN (vals)`. Eloquent
5077 /// `Model::whereIn($col, $vals)->get()` parity. Empty
5078 /// `vals` returns no rows (matches SQL's empty-IN
5079 /// semantics).
5080 ///
5081 /// `vals` accepts any iterable whose items are
5082 /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
5083 /// etc.
5084 ///
5085 /// # Errors
5086 /// As [`FetcherPool::fetch`].
5087 ///
5088 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5089 pub async fn where_in<V>(
5090 col: &str,
5091 vals: impl ::core::iter::IntoIterator<Item = V>,
5092 pool: &#root::sql::Pool,
5093 ) -> ::core::result::Result<
5094 ::std::vec::Vec<Self>,
5095 #root::sql::ExecError,
5096 >
5097 where
5098 V: ::core::convert::Into<#root::core::SqlValue>,
5099 {
5100 use #root::sql::FetcherPool as _;
5101 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5102 vals.into_iter().map(::core::convert::Into::into).collect();
5103 if _values.is_empty() {
5104 return ::core::result::Result::Ok(::std::vec::Vec::new());
5105 }
5106 let _key = ::std::format!("{}__in", col);
5107 #root::query::QuerySet::<Self>::default()
5108 .filter(&_key, #root::core::SqlValue::List(_values))
5109 .fetch(pool)
5110 .await
5111 }
5112
5113 /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
5114 /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
5115 /// `vals` returns every row (matches SQL's empty-NOT-IN
5116 /// semantics — vacuously true for every row).
5117 ///
5118 /// # Errors
5119 /// As [`FetcherPool::fetch`].
5120 ///
5121 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5122 pub async fn where_not_in<V>(
5123 col: &str,
5124 vals: impl ::core::iter::IntoIterator<Item = V>,
5125 pool: &#root::sql::Pool,
5126 ) -> ::core::result::Result<
5127 ::std::vec::Vec<Self>,
5128 #root::sql::ExecError,
5129 >
5130 where
5131 V: ::core::convert::Into<#root::core::SqlValue>,
5132 {
5133 use #root::sql::FetcherPool as _;
5134 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5135 vals.into_iter().map(::core::convert::Into::into).collect();
5136 if _values.is_empty() {
5137 return #root::query::QuerySet::<Self>::default()
5138 .fetch(pool)
5139 .await;
5140 }
5141 let _key = ::std::format!("{}__not_in", col);
5142 #root::query::QuerySet::<Self>::default()
5143 .filter(&_key, #root::core::SqlValue::List(_values))
5144 .fetch(pool)
5145 .await
5146 }
5147
5148 /// Fetch every row where `<col> IS NULL`. Eloquent
5149 /// `Model::whereNull($col)->get()` parity.
5150 ///
5151 /// # Errors
5152 /// As [`FetcherPool::fetch`].
5153 ///
5154 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5155 pub async fn where_null(
5156 col: &str,
5157 pool: &#root::sql::Pool,
5158 ) -> ::core::result::Result<
5159 ::std::vec::Vec<Self>,
5160 #root::sql::ExecError,
5161 > {
5162 use #root::sql::FetcherPool as _;
5163 let _key = ::std::format!("{}__isnull", col);
5164 #root::query::QuerySet::<Self>::default()
5165 .filter(&_key, #root::core::SqlValue::Bool(true))
5166 .fetch(pool)
5167 .await
5168 }
5169
5170 /// Fetch every row where `<col> IS NOT NULL`. Eloquent
5171 /// `Model::whereNotNull($col)->get()` parity.
5172 ///
5173 /// # Errors
5174 /// As [`FetcherPool::fetch`].
5175 ///
5176 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5177 pub async fn where_not_null(
5178 col: &str,
5179 pool: &#root::sql::Pool,
5180 ) -> ::core::result::Result<
5181 ::std::vec::Vec<Self>,
5182 #root::sql::ExecError,
5183 > {
5184 use #root::sql::FetcherPool as _;
5185 let _key = ::std::format!("{}__isnull", col);
5186 #root::query::QuerySet::<Self>::default()
5187 .filter(&_key, #root::core::SqlValue::Bool(false))
5188 .fetch(pool)
5189 .await
5190 }
5191
5192 /// Fetch up to `n` rows in random order. Eloquent
5193 /// `Model::inRandomOrder()->limit($n)->get()` /
5194 /// `Model::query()->inRandomOrder()->get()->take($n)`
5195 /// parity. **Performance caveat**: random ordering
5196 /// forces a full table scan + per-row random key sort;
5197 /// the optimizer cannot use an index. Prefer a
5198 /// `pk >= rand_offset LIMIT N` walk for huge tables.
5199 ///
5200 /// # Errors
5201 /// As [`FetcherPool::fetch`].
5202 ///
5203 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5204 pub async fn random_n(
5205 n: i64,
5206 pool: &#root::sql::Pool,
5207 ) -> ::core::result::Result<
5208 ::std::vec::Vec<Self>,
5209 #root::sql::ExecError,
5210 > {
5211 use #root::sql::FetcherPool as _;
5212 #root::query::QuerySet::<Self>::default()
5213 .order_random()
5214 .limit(n)
5215 .fetch(pool)
5216 .await
5217 }
5218
5219 /// Fetch one row in random order. Eloquent
5220 /// `Model::inRandomOrder()->first()` parity. Same
5221 /// performance caveat as [`Self::random_n`].
5222 ///
5223 /// # Errors
5224 /// As [`FetcherPool::fetch`].
5225 ///
5226 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5227 pub async fn random(
5228 pool: &#root::sql::Pool,
5229 ) -> ::core::result::Result<
5230 ::core::option::Option<Self>,
5231 #root::sql::ExecError,
5232 > {
5233 ::core::result::Result::Ok(
5234 Self::random_n(1, pool).await?.into_iter().next(),
5235 )
5236 }
5237
5238 /// Fetch every row ordered ASC by `field`. Eloquent
5239 /// `Model::oldest($field)->get()` parity — the multi-row
5240 /// counterpart of [`Self::earliest`].
5241 ///
5242 /// # Errors
5243 /// As [`FetcherPool::fetch`].
5244 ///
5245 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5246 pub async fn oldest(
5247 field: &str,
5248 pool: &#root::sql::Pool,
5249 ) -> ::core::result::Result<
5250 ::std::vec::Vec<Self>,
5251 #root::sql::ExecError,
5252 > {
5253 use #root::sql::FetcherPool as _;
5254 #root::query::QuerySet::<Self>::default()
5255 .order_by(&[(field, false)])
5256 .fetch(pool)
5257 .await
5258 }
5259
5260 /// Fetch every row ordered DESC by `field`. Eloquent
5261 /// `Model::latest($field)->get()` parity — the multi-row
5262 /// counterpart of [`Self::latest`].
5263 ///
5264 /// # Errors
5265 /// As [`FetcherPool::fetch`].
5266 ///
5267 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5268 pub async fn newest(
5269 field: &str,
5270 pool: &#root::sql::Pool,
5271 ) -> ::core::result::Result<
5272 ::std::vec::Vec<Self>,
5273 #root::sql::ExecError,
5274 > {
5275 use #root::sql::FetcherPool as _;
5276 #root::query::QuerySet::<Self>::default()
5277 .order_by(&[(field, true)])
5278 .fetch(pool)
5279 .await
5280 }
5281
5282 /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5283 /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5284 /// Routes through the existing `__year` lookup suffix
5285 /// (issue #829).
5286 ///
5287 /// # Errors
5288 /// As [`FetcherPool::fetch`].
5289 ///
5290 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5291 pub async fn where_year(
5292 col: &str,
5293 year: i64,
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!("{}__year", col);
5301 #root::query::QuerySet::<Self>::default()
5302 .filter(&_key, #root::core::SqlValue::I64(year))
5303 .fetch(pool)
5304 .await
5305 }
5306
5307 /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5308 /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5309 /// `month` is 1–12.
5310 ///
5311 /// # Errors
5312 /// As [`FetcherPool::fetch`].
5313 ///
5314 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5315 pub async fn where_month(
5316 col: &str,
5317 month: i64,
5318 pool: &#root::sql::Pool,
5319 ) -> ::core::result::Result<
5320 ::std::vec::Vec<Self>,
5321 #root::sql::ExecError,
5322 > {
5323 use #root::sql::FetcherPool as _;
5324 let _key = ::std::format!("{}__month", col);
5325 #root::query::QuerySet::<Self>::default()
5326 .filter(&_key, #root::core::SqlValue::I64(month))
5327 .fetch(pool)
5328 .await
5329 }
5330
5331 /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5332 /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5333 /// `day` is 1–31.
5334 ///
5335 /// # Errors
5336 /// As [`FetcherPool::fetch`].
5337 ///
5338 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5339 pub async fn where_day(
5340 col: &str,
5341 day: i64,
5342 pool: &#root::sql::Pool,
5343 ) -> ::core::result::Result<
5344 ::std::vec::Vec<Self>,
5345 #root::sql::ExecError,
5346 > {
5347 use #root::sql::FetcherPool as _;
5348 let _key = ::std::format!("{}__day", col);
5349 #root::query::QuerySet::<Self>::default()
5350 .filter(&_key, #root::core::SqlValue::I64(day))
5351 .fetch(pool)
5352 .await
5353 }
5354
5355 /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5356 /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5357 /// `hour` is 0–23.
5358 ///
5359 /// # Errors
5360 /// As [`FetcherPool::fetch`].
5361 ///
5362 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5363 pub async fn where_hour(
5364 col: &str,
5365 hour: i64,
5366 pool: &#root::sql::Pool,
5367 ) -> ::core::result::Result<
5368 ::std::vec::Vec<Self>,
5369 #root::sql::ExecError,
5370 > {
5371 use #root::sql::FetcherPool as _;
5372 let _key = ::std::format!("{}__hour", col);
5373 #root::query::QuerySet::<Self>::default()
5374 .filter(&_key, #root::core::SqlValue::I64(hour))
5375 .fetch(pool)
5376 .await
5377 }
5378
5379 /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5380 /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5381 /// `minute` is 0–59.
5382 ///
5383 /// # Errors
5384 /// As [`FetcherPool::fetch`].
5385 ///
5386 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5387 pub async fn where_minute(
5388 col: &str,
5389 minute: i64,
5390 pool: &#root::sql::Pool,
5391 ) -> ::core::result::Result<
5392 ::std::vec::Vec<Self>,
5393 #root::sql::ExecError,
5394 > {
5395 use #root::sql::FetcherPool as _;
5396 let _key = ::std::format!("{}__minute", col);
5397 #root::query::QuerySet::<Self>::default()
5398 .filter(&_key, #root::core::SqlValue::I64(minute))
5399 .fetch(pool)
5400 .await
5401 }
5402
5403 /// Fetch every row where `<col> LIKE <pattern>` —
5404 /// caller-supplied pattern (must include `%` / `_`
5405 /// wildcards manually). Eloquent `Model::whereLike`
5406 /// parity.
5407 ///
5408 /// # Errors
5409 /// As [`FetcherPool::fetch`].
5410 ///
5411 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5412 pub async fn where_like(
5413 col: &str,
5414 pattern: impl ::core::convert::Into<::std::string::String>,
5415 pool: &#root::sql::Pool,
5416 ) -> ::core::result::Result<
5417 ::std::vec::Vec<Self>,
5418 #root::sql::ExecError,
5419 > {
5420 use #root::sql::FetcherPool as _;
5421 let _key = ::std::format!("{}__like", col);
5422 #root::query::QuerySet::<Self>::default()
5423 .filter(
5424 &_key,
5425 #root::core::SqlValue::String(pattern.into()),
5426 )
5427 .fetch(pool)
5428 .await
5429 }
5430
5431 /// Fetch every row where `<col> ILIKE <pattern>` —
5432 /// case-insensitive LIKE (PG native, MySQL/SQLite
5433 /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5434 ///
5435 /// # Errors
5436 /// As [`FetcherPool::fetch`].
5437 ///
5438 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5439 pub async fn where_ilike(
5440 col: &str,
5441 pattern: impl ::core::convert::Into<::std::string::String>,
5442 pool: &#root::sql::Pool,
5443 ) -> ::core::result::Result<
5444 ::std::vec::Vec<Self>,
5445 #root::sql::ExecError,
5446 > {
5447 use #root::sql::FetcherPool as _;
5448 let _key = ::std::format!("{}__ilike", col);
5449 #root::query::QuerySet::<Self>::default()
5450 .filter(
5451 &_key,
5452 #root::core::SqlValue::String(pattern.into()),
5453 )
5454 .fetch(pool)
5455 .await
5456 }
5457
5458 /// Fetch every row where `<col>` starts with `prefix`
5459 /// (auto-appends `%`). Eloquent
5460 /// `whereLike("col", "$prefix%")` parity.
5461 ///
5462 /// # Errors
5463 /// As [`FetcherPool::fetch`].
5464 ///
5465 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5466 pub async fn where_starts_with(
5467 col: &str,
5468 prefix: impl ::core::convert::Into<::std::string::String>,
5469 pool: &#root::sql::Pool,
5470 ) -> ::core::result::Result<
5471 ::std::vec::Vec<Self>,
5472 #root::sql::ExecError,
5473 > {
5474 use #root::sql::FetcherPool as _;
5475 let _key = ::std::format!("{}__startswith", col);
5476 #root::query::QuerySet::<Self>::default()
5477 .filter(
5478 &_key,
5479 #root::core::SqlValue::String(prefix.into()),
5480 )
5481 .fetch(pool)
5482 .await
5483 }
5484
5485 /// Fetch every row where `<col>` ends with `suffix`
5486 /// (auto-prepends `%`). Eloquent
5487 /// `whereLike("col", "%$suffix")` parity.
5488 ///
5489 /// # Errors
5490 /// As [`FetcherPool::fetch`].
5491 ///
5492 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5493 pub async fn where_ends_with(
5494 col: &str,
5495 suffix: impl ::core::convert::Into<::std::string::String>,
5496 pool: &#root::sql::Pool,
5497 ) -> ::core::result::Result<
5498 ::std::vec::Vec<Self>,
5499 #root::sql::ExecError,
5500 > {
5501 use #root::sql::FetcherPool as _;
5502 let _key = ::std::format!("{}__endswith", col);
5503 #root::query::QuerySet::<Self>::default()
5504 .filter(
5505 &_key,
5506 #root::core::SqlValue::String(suffix.into()),
5507 )
5508 .fetch(pool)
5509 .await
5510 }
5511
5512 /// Fetch every row where `<col>` contains `substr`
5513 /// (auto-wraps with `%`).
5514 /// Eloquent `whereLike("col", "%$substr%")` parity.
5515 ///
5516 /// # Errors
5517 /// As [`FetcherPool::fetch`].
5518 ///
5519 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5520 pub async fn where_contains(
5521 col: &str,
5522 substr: impl ::core::convert::Into<::std::string::String>,
5523 pool: &#root::sql::Pool,
5524 ) -> ::core::result::Result<
5525 ::std::vec::Vec<Self>,
5526 #root::sql::ExecError,
5527 > {
5528 use #root::sql::FetcherPool as _;
5529 let _key = ::std::format!("{}__contains", col);
5530 #root::query::QuerySet::<Self>::default()
5531 .filter(
5532 &_key,
5533 #root::core::SqlValue::String(substr.into()),
5534 )
5535 .fetch(pool)
5536 .await
5537 }
5538
5539 /// Fetch every row where `<col> > val`. Eloquent
5540 /// `Model::where($col, ">", $val)->get()` parity.
5541 ///
5542 /// # Errors
5543 /// As [`FetcherPool::fetch`].
5544 ///
5545 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5546 pub async fn where_gt(
5547 col: &str,
5548 val: impl ::core::convert::Into<#root::core::SqlValue>,
5549 pool: &#root::sql::Pool,
5550 ) -> ::core::result::Result<
5551 ::std::vec::Vec<Self>,
5552 #root::sql::ExecError,
5553 > {
5554 use #root::sql::FetcherPool as _;
5555 let _key = ::std::format!("{}__gt", col);
5556 #root::query::QuerySet::<Self>::default()
5557 .filter(&_key, val)
5558 .fetch(pool)
5559 .await
5560 }
5561
5562 /// Fetch every row where `<col> >= val`. Eloquent
5563 /// `Model::where($col, ">=", $val)->get()` parity.
5564 ///
5565 /// # Errors
5566 /// As [`FetcherPool::fetch`].
5567 ///
5568 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5569 pub async fn where_gte(
5570 col: &str,
5571 val: impl ::core::convert::Into<#root::core::SqlValue>,
5572 pool: &#root::sql::Pool,
5573 ) -> ::core::result::Result<
5574 ::std::vec::Vec<Self>,
5575 #root::sql::ExecError,
5576 > {
5577 use #root::sql::FetcherPool as _;
5578 let _key = ::std::format!("{}__gte", col);
5579 #root::query::QuerySet::<Self>::default()
5580 .filter(&_key, val)
5581 .fetch(pool)
5582 .await
5583 }
5584
5585 /// Fetch every row where `<col> < val`. Eloquent
5586 /// `Model::where($col, "<", $val)->get()` parity.
5587 ///
5588 /// # Errors
5589 /// As [`FetcherPool::fetch`].
5590 ///
5591 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5592 pub async fn where_lt(
5593 col: &str,
5594 val: impl ::core::convert::Into<#root::core::SqlValue>,
5595 pool: &#root::sql::Pool,
5596 ) -> ::core::result::Result<
5597 ::std::vec::Vec<Self>,
5598 #root::sql::ExecError,
5599 > {
5600 use #root::sql::FetcherPool as _;
5601 let _key = ::std::format!("{}__lt", col);
5602 #root::query::QuerySet::<Self>::default()
5603 .filter(&_key, val)
5604 .fetch(pool)
5605 .await
5606 }
5607
5608 /// Fetch every row where `<col> <= val`. Eloquent
5609 /// `Model::where($col, "<=", $val)->get()` parity.
5610 ///
5611 /// # Errors
5612 /// As [`FetcherPool::fetch`].
5613 ///
5614 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5615 pub async fn where_lte(
5616 col: &str,
5617 val: impl ::core::convert::Into<#root::core::SqlValue>,
5618 pool: &#root::sql::Pool,
5619 ) -> ::core::result::Result<
5620 ::std::vec::Vec<Self>,
5621 #root::sql::ExecError,
5622 > {
5623 use #root::sql::FetcherPool as _;
5624 let _key = ::std::format!("{}__lte", col);
5625 #root::query::QuerySet::<Self>::default()
5626 .filter(&_key, val)
5627 .fetch(pool)
5628 .await
5629 }
5630
5631 /// Fetch every row where `<col> <> val`. Eloquent
5632 /// `Model::where($col, "!=", $val)->get()` parity.
5633 ///
5634 /// # Errors
5635 /// As [`FetcherPool::fetch`].
5636 ///
5637 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5638 pub async fn where_ne(
5639 col: &str,
5640 val: impl ::core::convert::Into<#root::core::SqlValue>,
5641 pool: &#root::sql::Pool,
5642 ) -> ::core::result::Result<
5643 ::std::vec::Vec<Self>,
5644 #root::sql::ExecError,
5645 > {
5646 use #root::sql::FetcherPool as _;
5647 let _key = ::std::format!("{}__ne", col);
5648 #root::query::QuerySet::<Self>::default()
5649 .filter(&_key, val)
5650 .fetch(pool)
5651 .await
5652 }
5653
5654 /// Fetch every row matching `<col> = val` for ANY of the
5655 /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5656 /// parity. Empty `cols` returns no rows.
5657 ///
5658 /// Resolves each `&str` column to its SCHEMA-registered
5659 /// `&'static str` once and builds a single OR-composed
5660 /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5661 ///
5662 /// # Errors
5663 /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5664 /// when any column is not declared on the model.
5665 ///
5666 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5667 pub async fn where_any(
5668 cols: &[&str],
5669 val: impl ::core::convert::Into<#root::core::SqlValue>,
5670 pool: &#root::sql::Pool,
5671 ) -> ::core::result::Result<
5672 ::std::vec::Vec<Self>,
5673 #root::sql::ExecError,
5674 > {
5675 Self::__where_multi(cols, val, false, pool).await
5676 }
5677
5678 /// Fetch every row matching `<col> = val` for ALL listed
5679 /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5680 /// Empty `cols` returns every row (vacuous AND).
5681 ///
5682 /// # Errors
5683 /// As [`Self::where_any`].
5684 pub async fn where_all(
5685 cols: &[&str],
5686 val: impl ::core::convert::Into<#root::core::SqlValue>,
5687 pool: &#root::sql::Pool,
5688 ) -> ::core::result::Result<
5689 ::std::vec::Vec<Self>,
5690 #root::sql::ExecError,
5691 > {
5692 Self::__where_multi(cols, val, true, pool).await
5693 }
5694
5695 /// Internal: build a Q expression composing `cols` via
5696 /// AND (`all`) or OR (`!all`), then fetch. Backs
5697 /// `where_any` / `where_all`.
5698 #[doc(hidden)]
5699 pub async fn __where_multi(
5700 cols: &[&str],
5701 val: impl ::core::convert::Into<#root::core::SqlValue>,
5702 all: bool,
5703 pool: &#root::sql::Pool,
5704 ) -> ::core::result::Result<
5705 ::std::vec::Vec<Self>,
5706 #root::sql::ExecError,
5707 > {
5708 #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5709 .await
5710 }
5711
5712 /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5713 /// parity. PK-ordered
5714 /// is NOT guaranteed without an explicit `order_by` —
5715 /// drop into `Self::query()` for that.
5716 ///
5717 /// # Errors
5718 /// As [`FetcherPool::fetch`].
5719 ///
5720 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5721 pub async fn take(
5722 n: i64,
5723 pool: &#root::sql::Pool,
5724 ) -> ::core::result::Result<
5725 ::std::vec::Vec<Self>,
5726 #root::sql::ExecError,
5727 > {
5728 use #root::sql::FetcherPool as _;
5729 #root::query::QuerySet::<Self>::default()
5730 .limit(n)
5731 .fetch(pool)
5732 .await
5733 }
5734
5735 /// Fetch the page-th window of `per_page` rows
5736 /// (1-indexed). Eloquent
5737 /// `Model::query()->forPage($page, $perPage)->get()`
5738 /// parity. The DB scans an `OFFSET (page - 1) * per_page
5739 /// LIMIT per_page`; for large offsets this is O(N) —
5740 /// prefer keyset pagination via PK on hot paths.
5741 ///
5742 /// # Errors
5743 /// As [`FetcherPool::fetch`].
5744 ///
5745 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5746 pub async fn for_page(
5747 page: i64,
5748 per_page: i64,
5749 pool: &#root::sql::Pool,
5750 ) -> ::core::result::Result<
5751 ::std::vec::Vec<Self>,
5752 #root::sql::ExecError,
5753 > {
5754 use #root::sql::FetcherPool as _;
5755 let _offset = #root::list_params::page_offset(page, per_page);
5756 #root::query::QuerySet::<Self>::default()
5757 .limit(per_page)
5758 .offset(_offset)
5759 .fetch(pool)
5760 .await
5761 }
5762
5763 /// Eloquent `Model::paginate($per_page, $page)` — fetch
5764 /// one page of rows AND the total row count in one
5765 /// call. Returns `(rows, total)`. Two queries: a
5766 /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5767 /// for the total.
5768 ///
5769 /// Useful for paginated UIs that need both the visible
5770 /// rows AND a "Page X of Y" / total-count widget. For
5771 /// large tables prefer keyset pagination + cached count;
5772 /// every call to `paginate` re-counts the full table.
5773 ///
5774 /// Same 1-indexed `page` convention as [`Self::for_page`].
5775 ///
5776 /// # Errors
5777 /// As [`Self::for_page`] and [`Self::count`].
5778 pub async fn paginate(
5779 page: i64,
5780 per_page: i64,
5781 pool: &#root::sql::Pool,
5782 ) -> ::core::result::Result<
5783 (::std::vec::Vec<Self>, i64),
5784 #root::sql::ExecError,
5785 > {
5786 // Route through the queryset rather than `Self::count`:
5787 // the `count` inherent method is suppressed on models
5788 // that declare a field named `count` (field/shortcut
5789 // collision guard), and `paginate` must still compile
5790 // for those models.
5791 let total = {
5792 use #root::sql::CounterPool as _;
5793 #root::query::QuerySet::<Self>::default()
5794 .count(pool)
5795 .await?
5796 };
5797 let rows = Self::for_page(page, per_page, pool).await?;
5798 ::core::result::Result::Ok((rows, total))
5799 }
5800
5801 /// Bulk-update — set `set_col = set_val` on every row
5802 /// matching `where_col = where_val`. Returns affected row
5803 /// count. Eloquent
5804 /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5805 /// parity.
5806 ///
5807 /// For multi-column updates drop into the queryset
5808 /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5809 ///
5810 /// # Errors
5811 /// As [`UpdaterPool::execute_pool`].
5812 ///
5813 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5814 pub async fn update_where(
5815 where_col: &str,
5816 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5817 set_col: &str,
5818 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5819 pool: &#root::sql::Pool,
5820 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5821 let _query = #root::query::QuerySet::<Self>::default()
5822 .filter(where_col, where_val)
5823 .update()
5824 .set(set_col, set_val)
5825 .compile()?;
5826 #bulk_update_run
5827 }
5828
5829 /// Bulk-delete — remove every row matching
5830 /// `where_col = where_val`. Returns affected row count.
5831 /// Eloquent
5832 /// `Model::where($where_col, $where_val)->delete()` parity.
5833 ///
5834 /// # Errors
5835 /// As [`#root::sql::delete_pool`].
5836 pub async fn delete_where(
5837 where_col: &str,
5838 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5839 pool: &#root::sql::Pool,
5840 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5841 let _query = #root::query::QuerySet::<Self>::default()
5842 .filter_op(where_col, #root::core::Op::Eq, where_val)
5843 .compile_delete()?;
5844 #bulk_delete_run
5845 }
5846
5847 /// Bulk-update — set `set_col = set_val` on EVERY row of
5848 /// this model's table. **No WHERE clause** — use with
5849 /// care. Eloquent `Model::query()->update([$col => $val])`
5850 /// parity.
5851 ///
5852 /// Use for backfills, one-shot reset flows, etc. The
5853 /// returned count is rows affected.
5854 ///
5855 /// # Errors
5856 /// As [`UpdaterPool::execute_pool`].
5857 ///
5858 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5859 pub async fn update_all(
5860 set_col: &str,
5861 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5862 pool: &#root::sql::Pool,
5863 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5864 let _query = #root::query::QuerySet::<Self>::default()
5865 .update()
5866 .set(set_col, set_val)
5867 .compile()?;
5868 #bulk_update_run
5869 }
5870
5871 /// Fetch every row where `<col> NOT LIKE <pattern>`.
5872 /// Eloquent `Model::whereNotLike` parity. Pattern is
5873 /// passed verbatim — caller controls `%` / `_`.
5874 ///
5875 /// # Errors
5876 /// As [`FetcherPool::fetch`].
5877 ///
5878 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5879 pub async fn where_not_like(
5880 col: &str,
5881 pattern: impl ::core::convert::Into<::std::string::String>,
5882 pool: &#root::sql::Pool,
5883 ) -> ::core::result::Result<
5884 ::std::vec::Vec<Self>,
5885 #root::sql::ExecError,
5886 > {
5887 use #root::sql::FetcherPool as _;
5888 let _key = ::std::format!("{}__not_like", col);
5889 #root::query::QuerySet::<Self>::default()
5890 .filter(
5891 &_key,
5892 #root::core::SqlValue::String(pattern.into()),
5893 )
5894 .fetch(pool)
5895 .await
5896 }
5897
5898 /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5899 /// case-insensitive `NOT LIKE` (PG native, MySQL /
5900 /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5901 ///
5902 /// # Errors
5903 /// As [`FetcherPool::fetch`].
5904 ///
5905 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5906 pub async fn where_not_ilike(
5907 col: &str,
5908 pattern: impl ::core::convert::Into<::std::string::String>,
5909 pool: &#root::sql::Pool,
5910 ) -> ::core::result::Result<
5911 ::std::vec::Vec<Self>,
5912 #root::sql::ExecError,
5913 > {
5914 use #root::sql::FetcherPool as _;
5915 let _key = ::std::format!("{}__not_ilike", col);
5916 #root::query::QuerySet::<Self>::default()
5917 .filter(
5918 &_key,
5919 #root::core::SqlValue::String(pattern.into()),
5920 )
5921 .fetch(pool)
5922 .await
5923 }
5924
5925 /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5926 /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5927 /// parity.
5928 ///
5929 /// # Errors
5930 /// As [`FetcherPool::fetch`].
5931 ///
5932 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5933 pub async fn where_not_between(
5934 col: &str,
5935 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5936 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5937 pool: &#root::sql::Pool,
5938 ) -> ::core::result::Result<
5939 ::std::vec::Vec<Self>,
5940 #root::sql::ExecError,
5941 > {
5942 use #root::sql::FetcherPool as _;
5943 let _key = ::std::format!("{}__not_between", col);
5944 let _vals = #root::core::SqlValue::List(::std::vec![
5945 ::core::convert::Into::into(lo),
5946 ::core::convert::Into::into(hi),
5947 ]);
5948 #root::query::QuerySet::<Self>::default()
5949 .filter(&_key, _vals)
5950 .fetch(pool)
5951 .await
5952 }
5953
5954 /// Returns the SQL table name for this model. Eloquent
5955 /// `$model->getTable()` parity.
5956 #[must_use]
5957 pub fn table_name() -> &'static str {
5958 <Self as #root::core::Model>::SCHEMA.table
5959 }
5960
5961 /// Returns the SQL column name of this model's primary
5962 /// key, or `None` when the model has no
5963 /// `#[rustango(primary_key)]`. Eloquent
5964 /// `$model->getKeyName()` parity.
5965 #[must_use]
5966 pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5967 <Self as #root::core::Model>::SCHEMA
5968 .primary_key()
5969 .map(|f| f.column)
5970 }
5971
5972 /// Fetch every row where `<col> BETWEEN lo AND hi`
5973 /// (inclusive on both ends — same as SQL). Eloquent
5974 /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5975 ///
5976 /// # Errors
5977 /// As [`FetcherPool::fetch`].
5978 ///
5979 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5980 pub async fn where_between(
5981 col: &str,
5982 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5983 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5984 pool: &#root::sql::Pool,
5985 ) -> ::core::result::Result<
5986 ::std::vec::Vec<Self>,
5987 #root::sql::ExecError,
5988 > {
5989 use #root::sql::FetcherPool as _;
5990 let _key = ::std::format!("{}__between", col);
5991 let _vals = #root::core::SqlValue::List(::std::vec![
5992 ::core::convert::Into::into(lo),
5993 ::core::convert::Into::into(hi),
5994 ]);
5995 #root::query::QuerySet::<Self>::default()
5996 .filter(&_key, _vals)
5997 .fetch(pool)
5998 .await
5999 }
6000
6001 /// Fetch the first row where `<col> = <val>`. Returns
6002 /// `Ok(None)` when no row matches. Eloquent
6003 /// `Model::firstWhere($col, $val)` parity.
6004 ///
6005 /// Thin wrapper over `QuerySet::<Self>::default()
6006 /// .filter(col, val).first(pool)`. Use this when you
6007 /// want one row identified by a non-PK column (e.g.
6008 /// `User::first_where_pool("email", "x@y.com", &pool)`).
6009 ///
6010 /// `val` accepts any value `Into<SqlValue>` so plain
6011 /// strings, ints, UUIDs, etc. all work.
6012 ///
6013 /// # Errors
6014 /// As `QuerySet::first`.
6015 pub async fn first_where(
6016 col: &str,
6017 val: impl ::core::convert::Into<#root::core::SqlValue>,
6018 pool: &#root::sql::Pool,
6019 ) -> ::core::result::Result<
6020 ::core::option::Option<Self>,
6021 #root::sql::ExecError,
6022 > {
6023 #root::query::QuerySet::<Self>::default()
6024 .filter(col, val)
6025 .first(pool)
6026 .await
6027 }
6028
6029 #value_method
6030
6031 /// Fetch the row with the largest `field` value —
6032 /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
6033 /// `Ok(None)` for an empty table. Eloquent
6034 /// `Model::latest($field)->first()` parity.
6035 /// Thin wrapper over `QuerySet::<Self>::default()
6036 /// .latest(field, pool)`.
6037 ///
6038 /// **Field name** is the Rust field ident as a string
6039 /// (not the SQL column). It is a runtime `&str`, so an
6040 /// unknown one compiles and surfaces as
6041 /// `ExecError::Query(QueryError::UnknownField)` when the
6042 /// query runs.
6043 ///
6044 /// # Errors
6045 /// As `QuerySet::latest`.
6046 pub async fn latest(
6047 field: &str,
6048 pool: &#root::sql::Pool,
6049 ) -> ::core::result::Result<
6050 ::core::option::Option<Self>,
6051 #root::sql::ExecError,
6052 > {
6053 #root::query::QuerySet::<Self>::default()
6054 .latest(field, pool)
6055 .await
6056 }
6057
6058 /// Sibling of [`Self::latest`] — fetches the row
6059 /// with the smallest `field` value (`ORDER BY <field>
6060 /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
6061 /// ->first()` parity.
6062 ///
6063 /// # Errors
6064 /// As [`Self::latest`].
6065 pub async fn earliest(
6066 field: &str,
6067 pool: &#root::sql::Pool,
6068 ) -> ::core::result::Result<
6069 ::core::option::Option<Self>,
6070 #root::sql::ExecError,
6071 > {
6072 #root::query::QuerySet::<Self>::default()
6073 .earliest(field, pool)
6074 .await
6075 }
6076
6077 #count_method
6078
6079 /// `true` when the table contains at least one row.
6080 /// Eloquent `Model::query()->exists()` parity. Thin wrapper
6081 /// over `QuerySet::<Self>::default().exists(pool)`.
6082 ///
6083 /// # Errors
6084 /// As [`ExistsPool::exists`].
6085 ///
6086 /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
6087 pub async fn exists(
6088 pool: &#root::sql::Pool,
6089 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6090 use #root::sql::ExistsPool as _;
6091 #root::query::QuerySet::<Self>::default()
6092 .exists(pool)
6093 .await
6094 }
6095
6096 /// Inverse of [`Self::exists`] — returns `true` when
6097 /// the table has zero rows. Eloquent
6098 /// `Model::doesntExist()` parity.
6099 ///
6100 /// # Errors
6101 /// As [`Self::exists`].
6102 pub async fn doesnt_exist(
6103 pool: &#root::sql::Pool,
6104 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6105 Self::exists(pool).await.map(|e| !e)
6106 }
6107
6108 /// Eloquent `Model::query()->whereKey($pk)->exists()` —
6109 /// `true` when a row with primary key `pk` exists in
6110 /// the table. Sugar over
6111 /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
6112 ///
6113 /// Differs from [`Self::exists`] (which checks "any row
6114 /// in the table") by checking a specific PK existence.
6115 /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
6116 /// because the row is never materialized — the SQL is
6117 /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
6118 ///
6119 /// # Errors
6120 /// As [`#root::sql::ExistsPool::contains_pk`].
6121 pub async fn contains_pk(
6122 pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
6123 pool: &#root::sql::Pool,
6124 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6125 use #root::sql::ExistsPool as _;
6126 #root::query::QuerySet::<Self>::default()
6127 .contains_pk(pool, pk)
6128 .await
6129 }
6130
6131 #sum_method
6132 #avg_method
6133 #min_method
6134 #max_method
6135
6136 /// Fetch every row of this model from `pool`. Eloquent
6137 /// `Model::all()` parity — a thin wrapper over
6138 /// `QuerySet::<Self>::default().fetch(pool)`.
6139 ///
6140 /// **Use with care on large tables** — there's no
6141 /// pagination or limit; the entire table is materialized
6142 /// into memory. For anything beyond fixture / lookup
6143 /// tables, page through `QuerySet::<Self>::default()
6144 /// .order_by(...).limit(N).offset(M).fetch(pool)`
6145 /// or stream via `.iterator(chunk_size)`.
6146 ///
6147 /// # Errors
6148 /// As [`FetcherPool::fetch`].
6149 ///
6150 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6151 pub async fn all(
6152 pool: &#root::sql::Pool,
6153 ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
6154 {
6155 use #root::sql::FetcherPool as _;
6156 #root::query::QuerySet::<Self>::default()
6157 .fetch(pool)
6158 .await
6159 }
6160
6161 /// Look up every row whose primary key is in `pks`.
6162 /// Returns the matching rows in **inventory** order — NOT
6163 /// the order of `pks`. Empty `pks` returns an empty
6164 /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
6165 /// with a list) parity.
6166 ///
6167 /// Thin wrapper over `QuerySet::<Self>::default()
6168 /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
6169 /// Caller-supplied PKs that don't match a row are
6170 /// silently skipped (the returned vec is shorter than
6171 /// the input list). For an order-preserving / "fail
6172 /// when any missing" variant, build the queryset
6173 /// explicitly with `in_bulk` instead.
6174 ///
6175 /// Accepts any iterable whose elements are
6176 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
6177 /// `Vec<Uuid>`, etc.
6178 ///
6179 /// # Errors
6180 /// As [`FetcherPool::fetch`].
6181 ///
6182 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6183 pub async fn find_many<V>(
6184 pks: impl ::core::iter::IntoIterator<Item = V>,
6185 pool: &#root::sql::Pool,
6186 ) -> ::core::result::Result<
6187 ::std::vec::Vec<Self>,
6188 #root::sql::ExecError,
6189 >
6190 where
6191 V: ::core::convert::Into<#root::core::SqlValue>,
6192 {
6193 use #root::sql::FetcherPool as _;
6194 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6195 pks.into_iter().map(::core::convert::Into::into).collect();
6196 if _values.is_empty() {
6197 return ::core::result::Result::Ok(::std::vec::Vec::new());
6198 }
6199 let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
6200 #root::query::QuerySet::<Self>::default()
6201 .filter(&_key, #root::core::SqlValue::List(_values))
6202 .fetch(pool)
6203 .await
6204 }
6205
6206 /// Look up the row whose primary key equals `pk`. Returns
6207 /// `Ok(None)` when no row matches, rather than erroring.
6208 /// Eloquent `Model::find`
6209 /// shape — accepts any value `Into<SqlValue>`.
6210 ///
6211 /// One-liner shortcut for the common
6212 /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6213 /// .limit(1).fetch(pool).await?.into_iter().next()`
6214 /// dance.
6215 ///
6216 /// # Errors
6217 /// As [`FetcherPool::fetch`].
6218 ///
6219 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6220 pub async fn find(
6221 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6222 pool: &#root::sql::Pool,
6223 ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6224 {
6225 use #root::sql::FetcherPool as _;
6226 let _pk_val: #root::core::SqlValue = pk.into();
6227 let mut _rows: ::std::vec::Vec<Self> =
6228 #root::query::QuerySet::<Self>::default()
6229 .filter(::core::stringify!(#pk_ident), _pk_val)
6230 .limit(1)
6231 .fetch(pool)
6232 .await?;
6233 ::core::result::Result::Ok(_rows.into_iter().next())
6234 }
6235
6236 /// Look up the row whose primary key equals `pk`. Errors
6237 /// when no row matches — the throwing counterpart of
6238 /// [`Self::find`]. Eloquent `Model::findOrFail`
6239 /// parity.
6240 ///
6241 /// Translates the miss into
6242 /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6243 /// so callers can `?`-bubble straight through the typical
6244 /// `ExecError` error chain.
6245 ///
6246 /// # Errors
6247 /// As [`Self::find`]; additionally
6248 /// [`sqlx::Error::RowNotFound`] when no row matches.
6249 ///
6250 /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6251 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6252 pub async fn find_or_fail(
6253 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6254 pool: &#root::sql::Pool,
6255 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6256 match Self::find(pk, pool).await? {
6257 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6258 ::core::option::Option::None => ::core::result::Result::Err(
6259 #root::sql::ExecError::Driver(
6260 #root::sql::sqlx::Error::RowNotFound,
6261 ),
6262 ),
6263 }
6264 }
6265
6266 /// Look up multiple rows by primary key, **failing** if
6267 /// any requested PK is missing. Eloquent
6268 /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6269 /// inventory order (NOT request order — same as
6270 /// [`Self::find_many`]).
6271 ///
6272 /// Empty `pks` returns an empty `Vec` (no rows requested
6273 /// → nothing to fail on).
6274 ///
6275 /// Differs from [`Self::find_many`] in that this method
6276 /// requires every PK to resolve. If even one is missing,
6277 /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6278 /// in `ExecError::Driver`). Rows are deduped at the SQL
6279 /// layer, so passing the same PK twice in `pks` counts as
6280 /// one expected row.
6281 ///
6282 /// # Errors
6283 /// As [`Self::find_many`], plus `RowNotFound` when the
6284 /// returned row count is less than the count of distinct
6285 /// requested PKs.
6286 pub async fn find_many_or_fail<V>(
6287 pks: impl ::core::iter::IntoIterator<Item = V>,
6288 pool: &#root::sql::Pool,
6289 ) -> ::core::result::Result<
6290 ::std::vec::Vec<Self>,
6291 #root::sql::ExecError,
6292 >
6293 where
6294 V: ::core::convert::Into<#root::core::SqlValue>,
6295 {
6296 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6297 pks.into_iter().map(::core::convert::Into::into).collect();
6298 if _values.is_empty() {
6299 return ::core::result::Result::Ok(::std::vec::Vec::new());
6300 }
6301 // Dedup the requested PK list before counting so
6302 // duplicate-PK requests don't false-fail (the SQL
6303 // `IN (...)` clause naturally dedups too).
6304 let mut _seen: ::std::collections::HashSet<
6305 ::std::string::String,
6306 > = ::std::collections::HashSet::new();
6307 for v in &_values {
6308 _seen.insert(v.to_display_string());
6309 }
6310 let _expected = _seen.len();
6311 let _rows = Self::find_many(_values, pool).await?;
6312 if _rows.len() < _expected {
6313 return ::core::result::Result::Err(
6314 #root::sql::ExecError::Driver(
6315 #root::sql::sqlx::Error::RowNotFound,
6316 ),
6317 );
6318 }
6319 ::core::result::Result::Ok(_rows)
6320 }
6321
6322 /// Find by primary key or run `fallback` to produce a
6323 /// default row to return. Eloquent
6324 /// `Model::findOr($pk, fn() => …)` parity.
6325 ///
6326 /// Unlike [`Self::find_or_fail`] (which raises on
6327 /// miss), this is the "give me something sensible"
6328 /// branch: typical use is "fetch the user's row, else
6329 /// fall back to an anonymous/guest stub".
6330 ///
6331 /// `fallback` runs only when no row matches — the DB
6332 /// round-trip happens unconditionally.
6333 ///
6334 /// # Errors
6335 /// As [`Self::find`].
6336 pub async fn find_or<F>(
6337 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6338 pool: &#root::sql::Pool,
6339 fallback: F,
6340 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6341 where
6342 F: ::core::ops::FnOnce() -> Self,
6343 {
6344 ::core::result::Result::Ok(
6345 Self::find(pk, pool).await?.unwrap_or_else(fallback),
6346 )
6347 }
6348
6349 /// Same as [`Self::find_or`] but also returns a `bool`
6350 /// indicating whether the row was found in the DB
6351 /// (`true`) or freshly built from `fallback` (`false`).
6352 /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6353 /// in PHP the returned model also exposes `->exists`;
6354 /// here that's surfaced as the second tuple element.
6355 ///
6356 /// Useful for edit-or-create form handlers where the
6357 /// caller needs to know whether to PATCH or POST when
6358 /// the user submits the form.
6359 ///
6360 /// # Errors
6361 /// As [`Self::find`].
6362 pub async fn find_or_new<F>(
6363 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6364 pool: &#root::sql::Pool,
6365 fallback: F,
6366 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6367 where
6368 F: ::core::ops::FnOnce() -> Self,
6369 {
6370 match Self::find(pk, pool).await? {
6371 ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6372 ::core::option::Option::None => {
6373 ::core::result::Result::Ok((fallback(), false))
6374 }
6375 }
6376 }
6377
6378 /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6379 /// [`Self::find_or_new`] but **persists** the new row
6380 /// when the PK isn't found. Returns `(row, exists: bool)`
6381 /// — `exists=true` when the PK was found,
6382 /// `false` when a fresh row was inserted.
6383 ///
6384 /// ```ignore
6385 /// let (post, found) = Post::find_or_insert(
6386 /// pk,
6387 /// &pool,
6388 /// || Post { id: Auto::default(), title: "new".into() },
6389 /// ).await?;
6390 /// // `found` true → returned existing row;
6391 /// // `found` false → fallback was inserted, `post.id` populated.
6392 /// ```
6393 ///
6394 /// **Caveat**: two concurrent calls that both miss the
6395 /// find can both INSERT, violating uniqueness. Wrap in a
6396 /// transaction or rely on a UNIQUE constraint + handle
6397 /// the conflict error if you need race-free semantics.
6398 ///
6399 /// # Errors
6400 /// As [`Self::find`] and [`Self::save_pool`].
6401 pub async fn find_or_insert<F>(
6402 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6403 pool: &#root::sql::Pool,
6404 fallback: F,
6405 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6406 where
6407 F: ::core::ops::FnOnce() -> Self,
6408 {
6409 if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6410 return ::core::result::Result::Ok((_row, true));
6411 }
6412 let mut _new = fallback();
6413 _new.save_pool(pool).await?;
6414 ::core::result::Result::Ok((_new, false))
6415 }
6416
6417 /// Fetch the first row of the table, or run `fallback`
6418 /// when the table is empty. Eloquent
6419 /// `Model::firstOr(fn() => …)` parity.
6420 ///
6421 /// # Errors
6422 /// As [`Self::first`].
6423 pub async fn first_or<F>(
6424 pool: &#root::sql::Pool,
6425 fallback: F,
6426 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6427 where
6428 F: ::core::ops::FnOnce() -> Self,
6429 {
6430 // Route through the queryset rather than `Self::first`,
6431 // which is suppressed on models with a field named
6432 // `first` (field/shortcut collision guard).
6433 ::core::result::Result::Ok(
6434 #root::query::QuerySet::<Self>::default()
6435 .first(pool)
6436 .await?
6437 .unwrap_or_else(fallback),
6438 )
6439 }
6440
6441 /// Fetch exactly one row matching `<col> = <val>`. Errors
6442 /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6443 /// or more than one matches
6444 /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6445 /// Eloquent `Model::sole($col, $val)` parity.
6446 ///
6447 /// # Errors
6448 /// As [`Self::where_`] plus the explicit
6449 /// `RowNotFound` / `MultipleRowsReturned` cases above.
6450 pub async fn sole(
6451 col: &str,
6452 val: impl ::core::convert::Into<#root::core::SqlValue>,
6453 pool: &#root::sql::Pool,
6454 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6455 let mut _rows = Self::where_(col, val, pool).await?;
6456 match _rows.len() {
6457 0 => ::core::result::Result::Err(
6458 #root::sql::ExecError::Driver(
6459 #root::sql::sqlx::Error::RowNotFound,
6460 ),
6461 ),
6462 1 => ::core::result::Result::Ok(_rows.remove(0)),
6463 n => ::core::result::Result::Err(
6464 #root::sql::ExecError::MultipleRowsReturned {
6465 op: "sole",
6466 table: <Self as #root::core::Model>::SCHEMA.name,
6467 count: n,
6468 },
6469 ),
6470 }
6471 }
6472 }
6473 } else {
6474 quote!()
6475 };
6476
6477 // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6478 // the non-audited `_pool` methods but execute against an open
6479 // `PoolTx` so the writes participate in the caller's transaction.
6480 // Auditing inside TX is deferred; these always use the plain
6481 // executor primitives regardless of whether the model is audited.
6482 let tx_insert_method = if fields.has_auto {
6483 let pushes = &fields.insert_pushes;
6484 let returning_cols = &fields.returning_cols;
6485 quote! {
6486 /// Insert this row inside an open transaction, populating
6487 /// any `Auto<T>` PK from the auto-assigned value. Works
6488 /// against any backend that `tx` wraps.
6489 ///
6490 /// # Errors
6491 /// As [`Self::insert_pool`].
6492 pub async fn insert_tx(
6493 &mut self,
6494 tx: &mut #root::sql::PoolTx<'_>,
6495 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6496 let mut _columns: ::std::vec::Vec<&'static str> =
6497 ::std::vec::Vec::new();
6498 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6499 ::std::vec::Vec::new();
6500 #( #pushes )*
6501 let _query = #root::core::InsertQuery::new(
6502 <Self as #root::core::Model>::SCHEMA,
6503 _columns,
6504 _values,
6505 )
6506 .returning(::std::vec![ #( #returning_cols ),* ]);
6507 let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6508 #root::sql::apply_auto_pk(_result, self)
6509 }
6510 }
6511 } else {
6512 let insert_columns = &fields.insert_columns;
6513 let insert_values = &fields.insert_values;
6514 quote! {
6515 /// Insert this row inside an open transaction.
6516 ///
6517 /// # Errors
6518 /// As [`Self::insert_pool`].
6519 pub async fn insert_tx(
6520 &self,
6521 tx: &mut #root::sql::PoolTx<'_>,
6522 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6523 let _query = #root::core::InsertQuery::new(
6524 <Self as #root::core::Model>::SCHEMA,
6525 ::std::vec![ #( #insert_columns ),* ],
6526 ::std::vec![ #( #insert_values ),* ],
6527 );
6528 #root::sql::insert_tx(tx, &_query).await
6529 }
6530 }
6531 };
6532
6533 let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6534 let pk_column_lit = pk_col.as_str();
6535 let assignments = &fields.update_assignments;
6536 let dispatch_unset = if fields.pk_is_auto {
6537 quote! {
6538 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6539 return self.insert_tx(tx).await.map(|()| 1u64);
6540 }
6541 }
6542 } else {
6543 quote!()
6544 };
6545 let write = restore_auto_now_unless(
6546 &fields.auto_now_idents,
6547 &save_ret,
6548 &save_keep,
6549 "e! {
6550 let _query = #root::core::UpdateQuery::new(
6551 <Self as #root::core::Model>::SCHEMA,
6552 ::std::vec![ #( #assignments ),* ],
6553 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6554 #pk_column_lit,
6555 #root::core::Op::Eq,
6556 ::core::convert::Into::<#root::core::SqlValue>::into(
6557 ::core::clone::Clone::clone(&self.#pk_ident)
6558 ),
6559 )),
6560 );
6561 let _affected = #root::sql::update_tx(tx, &_query).await?;
6562 ::core::result::Result::Ok(_affected)
6563 },
6564 );
6565 quote! {
6566 /// Save this row inside an open transaction. `INSERT` when
6567 /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6568 /// PK. Works against any backend that `tx` wraps.
6569 ///
6570 /// # Errors
6571 /// As [`Self::save_pool`].
6572 pub async fn save_tx(
6573 &mut self,
6574 tx: &mut #root::sql::PoolTx<'_>,
6575 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6576 #dispatch_unset
6577 #write
6578 }
6579 }
6580 } else {
6581 quote!()
6582 };
6583
6584 let tx_delete_method = {
6585 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6586 let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6587 if let Some(pk_ident) = pk_ident_for_tx {
6588 quote! {
6589 /// Delete the row identified by this instance's PK
6590 /// inside an open transaction. Works against any backend
6591 /// that `tx` wraps.
6592 ///
6593 /// # Errors
6594 /// As [`Self::delete_pool`].
6595 pub async fn delete_tx(
6596 &self,
6597 tx: &mut #root::sql::PoolTx<'_>,
6598 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6599 let _query = #root::core::DeleteQuery::by_pk(
6600 <Self as #root::core::Model>::SCHEMA,
6601 #pk_column_lit,
6602 ::core::convert::Into::<#root::core::SqlValue>::into(
6603 ::core::clone::Clone::clone(&self.#pk_ident)
6604 ),
6605 );
6606 #root::sql::delete_tx(tx, &_query).await
6607 }
6608 }
6609 } else {
6610 quote!()
6611 }
6612 };
6613
6614 // Update emission captures both BEFORE and AFTER state — runs an
6615 // extra SELECT against `_executor` BEFORE the UPDATE, captures
6616 // each tracked field's prior value, then after the UPDATE diffs
6617 // against the in-memory `&self`. `diff_changes` drops unchanged
6618 // columns so the JSON only contains the actual delta.
6619 //
6620 // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6621 // and binds `_audit_before_pairs`; `audit_update_post` runs
6622 // after the UPDATE and emits the PendingEntry.
6623 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6624 audited_fields
6625 {
6626 if tracked.is_empty() {
6627 (quote!(), quote!())
6628 } else {
6629 let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
6630 let pk_column_lit_for_select =
6631 primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6632 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6633 if fields.pk_is_auto {
6634 quote!(self.#pk_ident.get().copied().unwrap_or_default())
6635 } else {
6636 quote!(::core::clone::Clone::clone(&self.#pk_ident))
6637 }
6638 } else {
6639 quote!(0_i64)
6640 };
6641 let before_pairs = tracked.iter().map(|c| {
6642 let column_lit = c.column.as_str();
6643 let value_ty = &c.value_ty;
6644 quote! {
6645 (
6646 #column_lit,
6647 match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6648 &_audit_before_row, #column_lit,
6649 ) {
6650 ::core::result::Result::Ok(v) => #root::core::__json_value(&v),
6651 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6652 },
6653 )
6654 }
6655 });
6656 let pre = quote! {
6657 let _audit_before_query = #root::audit::before_image_query(
6658 <Self as #root::core::Model>::SCHEMA,
6659 #pk_column_lit_for_select,
6660 ::core::convert::Into::<#root::core::SqlValue>::into(#pk_value_for_bind),
6661 &[ #( #tracked_cols ),* ],
6662 );
6663 // A failed pre-read must not let the UPDATE run unaudited (#1907).
6664 let _audit_before_pairs:
6665 ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6666 #root::sql::select_rows_on(&mut *_executor, &_audit_before_query)
6667 .await?
6668 .into_iter()
6669 .next()
6670 .map(|_audit_before_row| ::std::vec![ #( #before_pairs ),* ]);
6671 };
6672 let post = quote! {
6673 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6674 let _audit_after = self.__rustango_audit_pairs();
6675 if let ::core::option::Option::Some(_audit_entry) =
6676 #root::audit::PendingEntry::update_diff(
6677 <Self as #root::core::Model>::SCHEMA.table,
6678 self.__rustango_audit_pk(),
6679 &_audit_before,
6680 &_audit_after,
6681 )
6682 {
6683 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6684 }
6685 }
6686 };
6687 (pre, post)
6688 }
6689 } else {
6690 (quote!(), quote!())
6691 };
6692
6693 // Bulk-insert audit: one `Create` entry per row once RETURNING has
6694 // set its PK, written in one batched INSERT.
6695 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6696 quote! {
6697 let _audit_entries: ::std::vec::Vec<#root::audit::PendingEntry> = rows
6698 .iter()
6699 .map(|_row| _row.__rustango_audit_entry(#root::audit::AuditOp::Create))
6700 .collect();
6701 #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6702 }
6703 } else {
6704 quote!()
6705 };
6706
6707 let save_method = if fields.pk_is_auto {
6708 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6709 let pk_column_lit = pk_column.as_str();
6710 let assignments = &fields.update_assignments;
6711 let upsert_cols = &fields.upsert_update_columns;
6712 let upsert_pushes = &fields.insert_pushes;
6713 // Read before the pushes, which fill each unset id.
6714 let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
6715 fields.uuid_v7_fields.iter().cloned().unzip();
6716 let upsert_returning = &fields.returning_cols;
6717 let upsert_auto_assigns = &fields.auto_assigns;
6718 let auto_now_idents = &fields.auto_now_idents;
6719 // Conflict target: prefer the first declared `unique_together`
6720 // when it exists. Plain `Auto<T>` PKs are server-assigned via
6721 // `BIGSERIAL` and never collide on insert, so a PK-only target
6722 // would silently turn `upsert()` into "always-insert" for
6723 // surrogate-PK models with composite UNIQUE constraints — see
6724 // `RolePermission` / `UserRole` / `UserPermission` in the
6725 // tenancy permission engine. When no `unique_together` is
6726 // declared we keep the PK target (the original behaviour).
6727 let upsert_target_columns: Vec<String> =
6728 upsert_unique.map_or_else(|| vec![pk_column.clone()], <[String]>::to_vec);
6729 let upsert_target_lits = upsert_target_columns
6730 .iter()
6731 .map(String::as_str)
6732 .collect::<Vec<_>>();
6733 let conflict_clause = if fields.upsert_update_columns.is_empty() {
6734 quote!(#root::core::ConflictClause::DoNothing)
6735 } else {
6736 quote!(#root::core::ConflictClause::DoUpdate {
6737 target: ::std::vec![ #( #upsert_target_lits ),* ],
6738 update_columns: ::std::vec![ #( #upsert_cols ),* ],
6739 })
6740 };
6741 // Audited: the upsert reports whether it inserted or updated (#1795).
6742 let (upsert_run, audit_upsert_emit) = if audited_fields.is_some() {
6743 (
6744 quote! {
6745 let (_returning_row_v, _audit_op) =
6746 #root::audit::upsert_returning_on(&mut *_executor, &query).await?;
6747 },
6748 quote! {
6749 let _audit_entry = self.__rustango_audit_entry(_audit_op);
6750 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6751 },
6752 )
6753 } else {
6754 (
6755 quote! {
6756 let _returning_row_v =
6757 #root::sql::__macro_internals::insert_returning_on(_executor, &query)
6758 .await?;
6759 },
6760 quote!(),
6761 )
6762 };
6763 let save_on_write = restore_auto_now_unless(
6764 auto_now_idents,
6765 &save_ret,
6766 &save_keep,
6767 "e! {
6768 #audit_update_pre
6769 let _query = #root::core::UpdateQuery::new(
6770 <Self as #root::core::Model>::SCHEMA,
6771 ::std::vec![ #( #assignments ),* ],
6772 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6773 #pk_column_lit,
6774 #root::core::Op::Eq,
6775 ::core::convert::Into::<#root::core::SqlValue>::into(
6776 ::core::clone::Clone::clone(&self.#pk_ident)
6777 ),
6778 )),
6779 );
6780 let _affected = #root::sql::__macro_internals::update_on(
6781 #executor_passes_to_data_write,
6782 &_query,
6783 ).await?;
6784 #audit_update_post
6785 ::core::result::Result::Ok(_affected)
6786 },
6787 );
6788 let upsert_write = restore_auto_now_unless(
6789 auto_now_idents,
6790 "e!(::core::result::Result<(), #root::sql::ExecError>),
6791 "e!(_saved.is_ok()),
6792 "e! {
6793 // The UPDATE branch writes `auto_now` too, so stamp it like `save`.
6794 #( self.#auto_now_idents = #root::sql::Auto::Set(#root::__chrono::Utc::now()); )*
6795 #( #upsert_pushes )*
6796 let query = #root::core::InsertQuery::new(
6797 <Self as #root::core::Model>::SCHEMA,
6798 _columns,
6799 _values,
6800 )
6801 .returning(::std::vec![ #( #upsert_returning ),* ])
6802 .on_conflict(#conflict_clause)
6803 .generated(_generated);
6804 #upsert_run
6805 let _returning_row = &_returning_row_v;
6806 #( #upsert_auto_assigns )*
6807 #audit_upsert_emit
6808 ::core::result::Result::Ok(())
6809 },
6810 );
6811 Some(quote! {
6812 /// Insert this row if its `Auto<T>` primary key is
6813 /// `Unset`, otherwise update the existing row matching the
6814 /// PK. The caller doesn't need to pick `insert` vs the
6815 /// bulk-update path manually.
6816 ///
6817 /// On the insert branch, populates the PK from `RETURNING`
6818 /// (same behavior as `insert`). On the update branch,
6819 /// writes every non-PK column back; if no row matches the
6820 /// PK, returns `Ok(())` silently.
6821 ///
6822 /// Only generated when the primary key is declared as
6823 /// `Auto<T>`. Models with a manually-managed PK must use
6824 /// `insert` or the QuerySet update builder.
6825 ///
6826 /// **Postgres only — prefer [`Self::save_pool`].** This takes a
6827 /// driver-specific `PgPool`, so it does not exist at all on a
6828 /// build without the `postgres` feature: selecting `sqlite`
6829 /// makes the method vanish rather than fail with anything that
6830 /// names the cause.
6831 ///
6832 /// [`Self::save_pool`] takes `rustango::sql::Pool` and works on
6833 /// all three backends. The naming is inverted — the short name
6834 /// is the narrow one — and that is tracked in #1293.
6835 ///
6836 /// # Errors
6837 /// Returns [`#root::sql::ExecError`] for SQL-writing
6838 /// or driver failures.
6839 #[cfg(feature = "postgres")]
6840 pub async fn save(
6841 &mut self,
6842 pool: &#root::sql::sqlx::PgPool,
6843 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6844 #pool_to_save_on
6845 }
6846
6847 /// Like [`Self::save`] but accepts any sqlx executor —
6848 /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6849 /// escape hatch for tenant-scoped writes: schema-mode
6850 /// tenants share the registry pool but rely on a per-
6851 /// checkout `SET search_path`, so passing `&PgPool` would
6852 /// silently hit the wrong schema. Acquire a connection
6853 /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6854 ///
6855 /// # Errors
6856 /// As [`Self::save`].
6857 #[cfg(feature = "postgres")]
6858 pub async fn save_on #executor_generics (
6859 &mut self,
6860 #executor_param,
6861 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6862 #executor_where
6863 {
6864 // #1029 — INSERT writes exactly one row → 1; UPDATE returns
6865 // the rows-affected count (0 when the PK no longer exists).
6866 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6867 return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
6868 }
6869 #save_on_write
6870 }
6871
6872 /// Per-call override for the audit source. Runs
6873 /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6874 /// scope so the resulting audit entry records `source`
6875 /// instead of the task-local default. Useful for seed
6876 /// scripts and one-off CLI tools that don't sit inside an
6877 /// admin handler. The override applies only to this call;
6878 /// no global state changes.
6879 ///
6880 /// # Errors
6881 /// As [`Self::save_on`].
6882 #[cfg(feature = "postgres")]
6883 pub async fn save_on_with #executor_generics (
6884 &mut self,
6885 #executor_param,
6886 source: #root::audit::AuditSource,
6887 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6888 #executor_where
6889 {
6890 #root::audit::with_source(source, self.save_on(_executor)).await
6891 }
6892
6893 /// Insert this row or update it in-place if the primary key already
6894 /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6895 ///
6896 /// With `Auto::Unset` PK the server assigns a new key and no conflict
6897 /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6898 /// inserted if absent or all non-PK columns are overwritten if present.
6899 ///
6900 /// # Errors
6901 /// As [`Self::insert_on`].
6902 #[cfg(feature = "postgres")]
6903 pub async fn upsert(
6904 &mut self,
6905 pool: &#root::sql::sqlx::PgPool,
6906 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6907 #pool_to_upsert_on
6908 }
6909
6910 /// Like [`Self::upsert`] but accepts any sqlx executor.
6911 /// See [`Self::save_on`] for tenancy-scoped rationale.
6912 ///
6913 /// # Errors
6914 /// As [`Self::upsert`].
6915 #[cfg(feature = "postgres")]
6916 pub async fn upsert_on #executor_generics (
6917 &mut self,
6918 #executor_param,
6919 ) -> ::core::result::Result<(), #root::sql::ExecError>
6920 #executor_where
6921 {
6922 let mut _columns: ::std::vec::Vec<&'static str> =
6923 ::std::vec::Vec::new();
6924 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6925 ::std::vec::Vec::new();
6926 #[allow(unused_mut)]
6927 let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
6928 #(
6929 if matches!(&self.#gen_idents, #root::sql::Auto::Unset) {
6930 _generated.push(#gen_cols);
6931 }
6932 )*
6933 #upsert_write
6934 }
6935 })
6936 } else {
6937 None
6938 };
6939
6940 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6941 let pk_column_lit = pk_column.as_str();
6942 // Optional `soft_delete_on` / `restore_on` companions when the
6943 // model has a `#[rustango(soft_delete)]` column. They land
6944 // alongside the regular `delete_on` so callers have both
6945 // options — a hard delete (audit-tracked as a real DELETE) and
6946 // a logical delete (audit-tracked as an UPDATE setting the
6947 // deleted_at column to NOW()).
6948 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6949 let col_lit = col;
6950 let sd_field_ident = fields
6951 .soft_delete_field_ident
6952 .clone()
6953 .expect("soft_delete_column without ident");
6954 quote! {
6955 /// Soft-delete this row by setting its
6956 /// `#[rustango(soft_delete)]` column to `NOW()`.
6957 /// The row stays in the table; query helpers can
6958 /// filter it out by checking the column for `IS NOT
6959 /// NULL`.
6960 ///
6961 /// # Errors
6962 /// As [`Self::delete`].
6963 #[cfg(feature = "postgres")]
6964 pub async fn soft_delete_on #executor_generics (
6965 &self,
6966 #executor_param,
6967 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6968 #executor_where
6969 {
6970 let _now = #root::__chrono::Utc::now();
6971 let _query = #root::soft_delete::__mark_query(
6972 <Self as #root::core::Model>::SCHEMA,
6973 #col_lit,
6974 #pk_column_lit,
6975 ::core::convert::Into::<#root::core::SqlValue>::into(
6976 ::core::clone::Clone::clone(&self.#pk_ident)
6977 ),
6978 ::core::option::Option::Some(_now),
6979 );
6980 let _affected = #root::sql::__macro_internals::update_on(
6981 #executor_passes_to_data_write,
6982 &_query,
6983 ).await?;
6984 #audit_softdelete_emit
6985 ::core::result::Result::Ok(_affected)
6986 }
6987
6988 /// Inverse of [`Self::soft_delete_on`] — clears the
6989 /// soft-delete column back to NULL so the row is
6990 /// considered live again.
6991 ///
6992 /// # Errors
6993 /// As [`Self::delete`].
6994 #[cfg(feature = "postgres")]
6995 pub async fn restore_on #executor_generics (
6996 &self,
6997 #executor_param,
6998 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6999 #executor_where
7000 {
7001 let _query = #root::soft_delete::__mark_query(
7002 <Self as #root::core::Model>::SCHEMA,
7003 #col_lit,
7004 #pk_column_lit,
7005 ::core::convert::Into::<#root::core::SqlValue>::into(
7006 ::core::clone::Clone::clone(&self.#pk_ident)
7007 ),
7008 ::core::option::Option::None,
7009 );
7010 let _affected = #root::sql::__macro_internals::update_on(
7011 #executor_passes_to_data_write,
7012 &_query,
7013 ).await?;
7014 #audit_restore_emit
7015 ::core::result::Result::Ok(_affected)
7016 }
7017
7018 /// Tri-dialect counterpart of [`Self::soft_delete_on`]
7019 /// — takes [`#root::sql::Pool`] and dispatches per
7020 /// backend. Eloquent `Model::delete()` semantics on
7021 /// soft-delete-enabled models (closes #821 partial).
7022 ///
7023 /// Sets the `#[rustango(soft_delete)]` column to
7024 /// `NOW()` on every backend. Query helpers
7025 /// (`QuerySet::active()` / `only_trashed()`,
7026 /// `soft_delete::active_filter` /
7027 /// `compose_with_active`) filter trashed rows out by
7028 /// reading `IS NULL` on the same column. Keyed on the PK;
7029 /// global scopes do not apply.
7030 ///
7031 /// # Errors
7032 /// As [`#root::sql::update_pool`].
7033 pub async fn soft_delete(
7034 &self,
7035 pool: &#root::sql::Pool,
7036 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7037 let _now = #root::__chrono::Utc::now();
7038 let _query = #root::soft_delete::__mark_query(
7039 <Self as #root::core::Model>::SCHEMA,
7040 #col_lit,
7041 #pk_column_lit,
7042 ::core::convert::Into::<#root::core::SqlValue>::into(
7043 ::core::clone::Clone::clone(&self.#pk_ident)
7044 ),
7045 ::core::option::Option::Some(_now),
7046 );
7047 #pool_softdelete_run
7048 }
7049
7050 /// Tri-dialect counterpart of [`Self::restore_on`].
7051 /// Clears the `#[rustango(soft_delete)]` column back
7052 /// to `NULL`, marking the row live again. Eloquent
7053 /// `Model::restore()` parity. Keyed on the PK; global
7054 /// scopes do not apply.
7055 ///
7056 /// # Errors
7057 /// As [`#root::sql::update_pool`].
7058 pub async fn restore(
7059 &self,
7060 pool: &#root::sql::Pool,
7061 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7062 let _query = #root::soft_delete::__mark_query(
7063 <Self as #root::core::Model>::SCHEMA,
7064 #col_lit,
7065 #pk_column_lit,
7066 ::core::convert::Into::<#root::core::SqlValue>::into(
7067 ::core::clone::Clone::clone(&self.#pk_ident)
7068 ),
7069 ::core::option::Option::None,
7070 );
7071 #pool_restore_run
7072 }
7073
7074 /// Hard-delete this row, ignoring the soft-delete
7075 /// column. Eloquent `Model::forceDelete()` parity —
7076 /// the escape hatch when you need to actually purge
7077 /// data (GDPR, fixture cleanup, etc.).
7078 ///
7079 /// Equivalent to [`Self::delete_pool`] (the framework's
7080 /// non-soft delete) — exposed under the Eloquent name
7081 /// for muscle-memory + so soft-delete-enabled models
7082 /// have all three operations (soft / restore / force)
7083 /// in one place.
7084 ///
7085 /// # Errors
7086 /// As [`Self::delete_pool`].
7087 pub async fn force_delete(
7088 &self,
7089 pool: &#root::sql::Pool,
7090 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7091 Self::delete_pool(self, pool).await
7092 }
7093
7094 /// Returns `true` when this row is soft-deleted (its
7095 /// `#[rustango(soft_delete)]` column is currently
7096 /// set — Eloquent `$model->trashed()` parity).
7097 ///
7098 /// Pure in-memory predicate over `&self`; does not
7099 /// hit the database. Useful in admin/template code
7100 /// like `{% if post.trashed() %}…{% endif %}` and in
7101 /// guard clauses on restore/force-delete flows.
7102 pub fn trashed(&self) -> bool {
7103 ::core::option::Option::is_some(&self.#sd_field_ident)
7104 }
7105
7106 /// Fetch every row whose `#[rustango(soft_delete)]`
7107 /// column is `NULL` (a.k.a. the live, non-trashed
7108 /// rows). Eloquent's default `Model::all()` behavior on
7109 /// a soft-delete model (Eloquent auto-scopes trashed
7110 /// rows out; rustango doesn't have auto-scoping yet —
7111 /// see issue #820 — so this is the explicit shortcut).
7112 ///
7113 /// One-liner over `QuerySet::<Self>::default()
7114 /// .active().fetch(pool)`. Closes #821 partial.
7115 ///
7116 /// # Errors
7117 /// As [`#root::sql::FetcherPool::fetch`].
7118 pub async fn active(
7119 pool: &#root::sql::Pool,
7120 ) -> ::core::result::Result<
7121 ::std::vec::Vec<Self>,
7122 #root::sql::ExecError,
7123 > {
7124 use #root::sql::FetcherPool as _;
7125 #root::query::QuerySet::<Self>::default()
7126 .active()
7127 .fetch(pool)
7128 .await
7129 }
7130
7131 /// Fetch ONLY soft-deleted rows. Eloquent
7132 /// `Model::onlyTrashed()->get()` parity — drives the
7133 /// admin "Trash" page, restore flows, GDPR purge
7134 /// scans, etc.
7135 ///
7136 /// One-liner over `QuerySet::<Self>::default()
7137 /// .only_trashed().fetch(pool)`. Closes #821
7138 /// partial.
7139 ///
7140 /// # Errors
7141 /// As [`#root::sql::FetcherPool::fetch`].
7142 pub async fn only_trashed(
7143 pool: &#root::sql::Pool,
7144 ) -> ::core::result::Result<
7145 ::std::vec::Vec<Self>,
7146 #root::sql::ExecError,
7147 > {
7148 use #root::sql::FetcherPool as _;
7149 #root::query::QuerySet::<Self>::default()
7150 .only_trashed()
7151 .fetch(pool)
7152 .await
7153 }
7154
7155 /// Fetch every row, both live and soft-deleted.
7156 /// Eloquent `Model::withTrashed()->get()` parity.
7157 ///
7158 /// Today every queryset already includes trashed rows
7159 /// (rustango has no global-scope tracking yet — issue
7160 /// #820), so this is functionally equivalent to
7161 /// [`Self::all`]. Exposed as a named shortcut so
7162 /// soft-delete-aware code reads `Model::with_trashed_pool`
7163 /// rather than `Model::all_pool` — keeps intent visible
7164 /// in callers and stays correct when auto-scoping lands.
7165 ///
7166 /// Closes #821 partial.
7167 ///
7168 /// # Errors
7169 /// As [`#root::sql::FetcherPool::fetch`].
7170 pub async fn with_trashed(
7171 pool: &#root::sql::Pool,
7172 ) -> ::core::result::Result<
7173 ::std::vec::Vec<Self>,
7174 #root::sql::ExecError,
7175 > {
7176 use #root::sql::FetcherPool as _;
7177 #root::query::QuerySet::<Self>::default()
7178 .with_trashed()
7179 .fetch(pool)
7180 .await
7181 }
7182
7183 }
7184 } else {
7185 quote!()
7186 };
7187 quote! {
7188 /// Delete the row identified by this instance's primary key.
7189 ///
7190 /// Returns the number of rows affected (0 or 1).
7191 ///
7192 /// **Postgres only — prefer [`Self::delete_pool`].** This takes
7193 /// a driver-specific `PgPool`, so it does not exist on a build
7194 /// without the `postgres` feature. [`Self::delete_pool`] takes
7195 /// `rustango::sql::Pool` and works on all three. The naming is
7196 /// inverted; tracked in #1293.
7197 ///
7198 /// # Errors
7199 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7200 /// driver failures.
7201 #[cfg(feature = "postgres")]
7202 pub async fn delete(
7203 &self,
7204 pool: &#root::sql::sqlx::PgPool,
7205 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7206 #pool_to_delete_on
7207 }
7208
7209 /// Like [`Self::delete`] but accepts any sqlx executor —
7210 /// for tenant-scoped deletes against an explicitly-acquired
7211 /// connection. See [`Self::save_on`] for the rationale.
7212 ///
7213 /// # Errors
7214 /// As [`Self::delete`].
7215 #[cfg(feature = "postgres")]
7216 pub async fn delete_on #executor_generics (
7217 &self,
7218 #executor_param,
7219 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7220 #executor_where
7221 {
7222 let query = #root::core::DeleteQuery::by_pk(
7223 <Self as #root::core::Model>::SCHEMA,
7224 #pk_column_lit,
7225 ::core::convert::Into::<#root::core::SqlValue>::into(
7226 ::core::clone::Clone::clone(&self.#pk_ident)
7227 ),
7228 );
7229 let _affected = #root::sql::__macro_internals::delete_on(
7230 #executor_passes_to_data_write,
7231 &query,
7232 ).await?;
7233 #audit_delete_emit
7234 ::core::result::Result::Ok(_affected)
7235 }
7236
7237 /// Per-call audit-source override for [`Self::delete_on`].
7238 /// See [`Self::save_on_with`] for shape rationale.
7239 ///
7240 /// # Errors
7241 /// As [`Self::delete_on`].
7242 #[cfg(feature = "postgres")]
7243 pub async fn delete_on_with #executor_generics (
7244 &self,
7245 #executor_param,
7246 source: #root::audit::AuditSource,
7247 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7248 #executor_where
7249 {
7250 #root::audit::with_source(source, self.delete_on(_executor)).await
7251 }
7252 #pool_delete_method
7253 #pool_insert_method
7254 #audit_entry_method
7255 #pool_save_method
7256 #refresh_replicate_methods
7257 #tx_delete_method
7258 #tx_insert_method
7259 #tx_save_method
7260 #soft_delete_methods
7261
7262 /// Returns `true` when `other` represents the same DB
7263 /// row as `self` — i.e. their primary keys compare
7264 /// equal. Eloquent `$model->is($other)` parity.
7265 ///
7266 /// Because both arguments are typed `&Self`, the
7267 /// model/table check is automatic — `Post::is` cannot
7268 /// be invoked against a `Comment` at compile time. Only
7269 /// the PK has to be compared at runtime.
7270 pub fn is(&self, other: &Self) -> bool {
7271 self.#pk_ident == other.#pk_ident
7272 }
7273
7274 /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7275 /// parity.
7276 pub fn is_not(&self, other: &Self) -> bool {
7277 self.#pk_ident != other.#pk_ident
7278 }
7279
7280 /// Returns this row's primary-key value as an
7281 /// [`#root::core::SqlValue`]. Eloquent
7282 /// `$model->getKey()` parity.
7283 ///
7284 /// Useful when you need to thread the PK through a
7285 /// generic `Into<SqlValue>`-bound API without knowing the
7286 /// concrete PK type (`i64` vs `Uuid` vs `String`).
7287 #[must_use]
7288 pub fn get_key(&self) -> #root::core::SqlValue {
7289 ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7290 }
7291
7292 }
7293 });
7294
7295 let insert_method = if fields.has_auto {
7296 let pushes = &fields.insert_pushes;
7297 let returning_cols = &fields.returning_cols;
7298 let auto_assigns = &fields.auto_assigns;
7299 quote! {
7300 /// Insert this row into its table. Skips columns whose
7301 /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7302 /// sequence fills them in, then reads each `Auto` column
7303 /// back via `RETURNING` and stores it on `self`.
7304 ///
7305 /// **Postgres only — prefer [`Self::insert_pool`].** This takes
7306 /// a driver-specific `PgPool`, so it does not exist on a build
7307 /// without the `postgres` feature. [`Self::insert_pool`] takes
7308 /// `rustango::sql::Pool` and works on all three. The naming is
7309 /// inverted; tracked in #1293.
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 &mut self,
7317 pool: &#root::sql::sqlx::PgPool,
7318 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7319 #pool_to_insert_on
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 #executor_generics (
7329 &mut self,
7330 #executor_param,
7331 ) -> ::core::result::Result<(), #root::sql::ExecError>
7332 #executor_where
7333 {
7334 let mut _columns: ::std::vec::Vec<&'static str> =
7335 ::std::vec::Vec::new();
7336 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7337 ::std::vec::Vec::new();
7338 #( #pushes )*
7339 let query = #root::core::InsertQuery::new(
7340 <Self as #root::core::Model>::SCHEMA,
7341 _columns,
7342 _values,
7343 )
7344 .returning(::std::vec![ #( #returning_cols ),* ]);
7345 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7346 #executor_passes_to_data_write,
7347 &query,
7348 ).await?;
7349 let _returning_row = &_returning_row_v;
7350 #( #auto_assigns )*
7351 #audit_insert_emit
7352 ::core::result::Result::Ok(())
7353 }
7354
7355 /// Per-call audit-source override for [`Self::insert_on`].
7356 /// See [`Self::save_on_with`] for shape rationale.
7357 ///
7358 /// # Errors
7359 /// As [`Self::insert_on`].
7360 #[cfg(feature = "postgres")]
7361 pub async fn insert_on_with #executor_generics (
7362 &mut self,
7363 #executor_param,
7364 source: #root::audit::AuditSource,
7365 ) -> ::core::result::Result<(), #root::sql::ExecError>
7366 #executor_where
7367 {
7368 #root::audit::with_source(source, self.insert_on(_executor)).await
7369 }
7370 }
7371 } else {
7372 let insert_columns = &fields.insert_columns;
7373 let insert_values = &fields.insert_values;
7374 quote! {
7375 /// Insert this row into its table.
7376 ///
7377 /// # Errors
7378 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7379 /// driver failures.
7380 #[cfg(feature = "postgres")]
7381 pub async fn insert(
7382 &self,
7383 pool: &#root::sql::sqlx::PgPool,
7384 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7385 self.insert_on(pool).await
7386 }
7387
7388 /// Like [`Self::insert`] but accepts any sqlx executor.
7389 /// See [`Self::save_on`] for tenancy-scoped rationale.
7390 ///
7391 /// # Errors
7392 /// As [`Self::insert`].
7393 #[cfg(feature = "postgres")]
7394 pub async fn insert_on<'_c, _E>(
7395 &self,
7396 _executor: _E,
7397 ) -> ::core::result::Result<(), #root::sql::ExecError>
7398 where
7399 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7400 {
7401 let query = #root::core::InsertQuery::new(
7402 <Self as #root::core::Model>::SCHEMA,
7403 ::std::vec![ #( #insert_columns ),* ],
7404 ::std::vec![ #( #insert_values ),* ],
7405 );
7406 #root::sql::__macro_internals::insert_on(_executor, &query).await
7407 }
7408 }
7409 };
7410
7411 let bulk_insert_method = if fields.has_auto {
7412 let cols_no_auto = &fields.bulk_columns_no_auto;
7413 let cols_all = &fields.bulk_columns_all;
7414 let pushes_no_auto = &fields.bulk_pushes_no_auto;
7415 let pushes_all = &fields.bulk_pushes_all;
7416 let returning_cols = &fields.returning_cols;
7417 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7418 let uniformity = &fields.bulk_auto_uniformity;
7419 let fill_in_place = &fields.bulk_fill_in_place;
7420 // Nothing to read back when every Auto field is filled Rust-side (#1934).
7421 let read_back = if fields.returning_cols.is_empty() {
7422 quote!()
7423 } else {
7424 quote! {
7425 if _returned.len() != rows.len() {
7426 return ::core::result::Result::Err(
7427 #root::sql::ExecError::Sql(
7428 #root::sql::SqlError::BulkInsertReturningMismatch {
7429 expected: rows.len(),
7430 actual: _returned.len(),
7431 }
7432 )
7433 );
7434 }
7435 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7436 #auto_assigns_for_row
7437 }
7438 }
7439 };
7440 // The DB-filled `Auto` picks the path; Rust-filled ones fill per row (#1950).
7441 let first_auto_ident = fields
7442 .first_db_auto
7443 .as_ref()
7444 .map(|(ident, _)| ident)
7445 .or(fields.first_auto_ident.as_ref())
7446 .expect("has_auto implies first_auto_ident is Some");
7447 quote! {
7448 /// Bulk-insert `rows` in a single round-trip. Every row's
7449 /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7450 /// (sequence fills them in) or uniformly `Auto::Set(_)`
7451 /// (caller-supplied values). Mixed Set/Unset is rejected
7452 /// — call `insert` per row for that case.
7453 ///
7454 /// Empty slice is a no-op. Each row's `Auto` fields are
7455 /// populated from the `RETURNING` clause in input order
7456 /// before this returns.
7457 ///
7458 /// # Errors
7459 /// Returns [`#root::sql::ExecError`] for validation,
7460 /// SQL-writing, mixed-Auto rejection, or driver failures.
7461 #[cfg(feature = "postgres")]
7462 pub async fn bulk_insert(
7463 rows: &mut [Self],
7464 pool: &#root::sql::sqlx::PgPool,
7465 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7466 #pool_to_bulk_insert_on
7467 }
7468
7469 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7470 /// See [`Self::save_on`] for tenancy-scoped rationale.
7471 ///
7472 /// # Errors
7473 /// As [`Self::bulk_insert`].
7474 #[cfg(feature = "postgres")]
7475 pub async fn bulk_insert_on #executor_generics (
7476 rows: &mut [Self],
7477 #executor_param,
7478 ) -> ::core::result::Result<(), #root::sql::ExecError>
7479 #executor_where
7480 {
7481 if rows.is_empty() {
7482 return ::core::result::Result::Ok(());
7483 }
7484 let _first_unset = matches!(
7485 rows[0].#first_auto_ident,
7486 #root::sql::Auto::Unset
7487 );
7488 #( #uniformity )*
7489 for _row in rows.iter_mut() {
7490 #( #fill_in_place )*
7491 }
7492
7493 let mut _all_rows: ::std::vec::Vec<
7494 ::std::vec::Vec<#root::core::SqlValue>,
7495 > = ::std::vec::Vec::with_capacity(rows.len());
7496 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7497 for _row in rows.iter() {
7498 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7499 ::std::vec::Vec::new();
7500 #( #pushes_no_auto )*
7501 _all_rows.push(_row_vals);
7502 }
7503 ::std::vec![ #( #cols_no_auto ),* ]
7504 } else {
7505 for _row in rows.iter() {
7506 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7507 ::std::vec::Vec::new();
7508 #( #pushes_all )*
7509 _all_rows.push(_row_vals);
7510 }
7511 ::std::vec![ #( #cols_all ),* ]
7512 };
7513
7514 let _query = #root::core::BulkInsertQuery::new(
7515 <Self as #root::core::Model>::SCHEMA,
7516 _columns,
7517 _all_rows,
7518 )
7519 .returning(::std::vec![ #( #returning_cols ),* ]);
7520 let _returned = #root::sql::__macro_internals::bulk_insert_on(
7521 #executor_passes_to_data_write,
7522 &_query,
7523 ).await?;
7524 #read_back
7525 #audit_bulk_insert_emit
7526 ::core::result::Result::Ok(())
7527 }
7528 }
7529 } else {
7530 let cols_all = &fields.bulk_columns_all;
7531 let pushes_all = &fields.bulk_pushes_all;
7532 quote! {
7533 /// Bulk-insert `rows` in a single round-trip. Every row's
7534 /// fields are written verbatim — there are no `Auto<T>`
7535 /// fields on this model.
7536 ///
7537 /// Empty slice is a no-op.
7538 ///
7539 /// # Errors
7540 /// Returns [`#root::sql::ExecError`] for validation,
7541 /// SQL-writing, or driver failures.
7542 #[cfg(feature = "postgres")]
7543 pub async fn bulk_insert(
7544 rows: &[Self],
7545 pool: &#root::sql::sqlx::PgPool,
7546 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7547 #pool_to_bulk_insert_on
7548 }
7549
7550 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7551 /// See [`Self::save_on`] for tenancy-scoped rationale.
7552 ///
7553 /// # Errors
7554 /// As [`Self::bulk_insert`].
7555 #[cfg(feature = "postgres")]
7556 pub async fn bulk_insert_on #executor_generics (
7557 rows: &[Self],
7558 #executor_param,
7559 ) -> ::core::result::Result<(), #root::sql::ExecError>
7560 #executor_where
7561 {
7562 if rows.is_empty() {
7563 return ::core::result::Result::Ok(());
7564 }
7565 let mut _all_rows: ::std::vec::Vec<
7566 ::std::vec::Vec<#root::core::SqlValue>,
7567 > = ::std::vec::Vec::with_capacity(rows.len());
7568 for _row in rows.iter() {
7569 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7570 ::std::vec::Vec::new();
7571 #( #pushes_all )*
7572 _all_rows.push(_row_vals);
7573 }
7574 let _query = #root::core::BulkInsertQuery::new(
7575 <Self as #root::core::Model>::SCHEMA,
7576 ::std::vec![ #( #cols_all ),* ],
7577 _all_rows,
7578 );
7579 let _ = #root::sql::__macro_internals::bulk_insert_on(
7580 #executor_passes_to_data_write,
7581 &_query,
7582 ).await?;
7583 #audit_bulk_insert_emit
7584 ::core::result::Result::Ok(())
7585 }
7586 }
7587 };
7588
7589 // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7590 // (no postgres-feature gate); routes through the existing
7591 // `bulk_insert_pool` + per-dialect conflict writer.
7592 //
7593 // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7594 // sequence picks the PK for fresh inserts; the UPDATE branch never
7595 // touches the Auto column.
7596 // Audited models audit each written row; MySQL, with no RETURNING,
7597 // is refused there (#1747, #1795).
7598 let run_conflict_bulk_insert = match (audited_fields.is_some(), primary_key.is_some()) {
7599 (false, _) => quote!(#root::sql::bulk_insert_pool(pool, &_query).await),
7600 (true, true) => quote! {
7601 #root::audit::bulk_insert_with_audit::<Self>(pool, &_query, |_r: &Self, _op| {
7602 _r.__rustango_audit_entry(_op)
7603 })
7604 .await
7605 },
7606 (true, false) => quote! {
7607 ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
7608 table: <Self as #root::core::Model>::SCHEMA.table,
7609 })
7610 },
7611 };
7612 let bulk_upsert_pool_method = {
7613 let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
7614 fields.uuid_v7_fields.iter().cloned().unzip();
7615 // Pick the "no Auto" columns when the model has Auto fields,
7616 // else every column.
7617 let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7618 (
7619 fields.bulk_columns_no_auto.clone(),
7620 fields.bulk_pushes_no_auto.clone(),
7621 )
7622 } else {
7623 (
7624 fields.bulk_columns_all.clone(),
7625 fields.bulk_pushes_all.clone(),
7626 )
7627 };
7628 quote! {
7629 /// Tri-dialect bulk insert that updates on conflict — the
7630 /// "import a batch idempotently" shape. Issue #267 / T1.5.
7631 ///
7632 /// Per-row values are extracted and lowered into a
7633 /// [`#root::core::BulkInsertQuery`] with
7634 /// `on_conflict = DoUpdate { target, update_columns }`. The
7635 /// writer dispatches per-dialect:
7636 /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7637 /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)`; refused when
7638 /// `target` is not the only unique key, since MySQL matches every UNIQUE index
7639 ///
7640 /// `target` names the column(s) whose unique constraint
7641 /// defines the conflict (typically a `unique` or
7642 /// `unique_together` natural-key column, NOT the `Auto<T>`
7643 /// PK). `update_cols` names the columns to overwrite on
7644 /// conflict — every other column is left untouched on the
7645 /// existing row.
7646 ///
7647 /// Auto-PK rows must all have `Auto::Unset` (the sequence
7648 /// picks the PK on insert; the update path never touches
7649 /// the Auto column). Auto-set rows trigger a hard error.
7650 /// Empty slice is a no-op. On an audited model each written
7651 /// row gets a `create` or `update` audit row; MySQL refuses it.
7652 ///
7653 /// # Errors
7654 /// Returns [`#root::sql::ExecError`] for validation,
7655 /// SQL-writing, or driver failures.
7656 pub async fn bulk_upsert_pool(
7657 rows: &[Self],
7658 target: &[&'static str],
7659 update_cols: &[&'static str],
7660 pool: &#root::sql::Pool,
7661 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7662 if rows.is_empty() {
7663 return ::core::result::Result::Ok(());
7664 }
7665 let mut _all_rows: ::std::vec::Vec<
7666 ::std::vec::Vec<#root::core::SqlValue>,
7667 > = ::std::vec::Vec::with_capacity(rows.len());
7668 for _row in rows.iter() {
7669 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7670 ::std::vec::Vec::new();
7671 #( #upsert_pushes )*
7672 _all_rows.push(_row_vals);
7673 }
7674 #[allow(unused_mut)]
7675 let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7676 #(
7677 if rows.iter().all(|_r| matches!(&_r.#gen_idents, #root::sql::Auto::Unset)) {
7678 _generated.push(#gen_cols);
7679 }
7680 )*
7681 let _query = #root::core::BulkInsertQuery::new(
7682 <Self as #root::core::Model>::SCHEMA,
7683 ::std::vec![ #( #upsert_cols ),* ],
7684 _all_rows,
7685 )
7686 .on_conflict_do_update(target, update_cols)
7687 .generated(_generated);
7688 #run_conflict_bulk_insert
7689 }
7690
7691 /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7692 /// skip rows that would violate a unique constraint. Issue
7693 /// #267 / T1.5. Same per-dialect dispatch as
7694 /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7695 /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7696 /// <pivot> = <pivot>` (MySQL no-op write).
7697 ///
7698 /// # Errors
7699 /// As [`Self::bulk_upsert_pool`].
7700 pub async fn bulk_insert_or_ignore_pool(
7701 rows: &[Self],
7702 pool: &#root::sql::Pool,
7703 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7704 if rows.is_empty() {
7705 return ::core::result::Result::Ok(());
7706 }
7707 let mut _all_rows: ::std::vec::Vec<
7708 ::std::vec::Vec<#root::core::SqlValue>,
7709 > = ::std::vec::Vec::with_capacity(rows.len());
7710 for _row in rows.iter() {
7711 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7712 ::std::vec::Vec::new();
7713 #( #upsert_pushes )*
7714 _all_rows.push(_row_vals);
7715 }
7716 let _query = #root::core::BulkInsertQuery::new(
7717 <Self as #root::core::Model>::SCHEMA,
7718 ::std::vec![ #( #upsert_cols ),* ],
7719 _all_rows,
7720 )
7721 .on_conflict_do_nothing();
7722 #run_conflict_bulk_insert
7723 }
7724 }
7725 };
7726
7727 // Ergonomic `Model::bulk_update(objs, fields)`.
7728 // The SQL/IR/executor stack
7729 // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7730 // `write_bulk_update_*` writers) already existed; what was missing
7731 // was the per-model constructor that maps `&[Self]` + a runtime
7732 // column list into rows of `[pk, col_vals…]` so callers don't
7733 // hand-build the IR. Emitted only when the model has a primary key
7734 // (the PK is the join key and can't itself be updated).
7735 let bulk_update_rows_run = if audited_fields.is_some() {
7736 quote!(#root::audit::bulk_update_with_audit::<Self>(pool, &_query, |_r: &Self| {
7737 _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
7738 })
7739 .await)
7740 } else {
7741 quote!(#root::sql::bulk_update_pool(pool, &_query).await)
7742 };
7743 let bulk_update_method = match &fields.primary_key {
7744 None => quote! {},
7745 Some((pk_ident, pk_col)) => {
7746 // One pair of match arms per non-PK column: a validation arm
7747 // resolving the runtime name to its `&'static str` column,
7748 // and a value arm pushing that field off the row.
7749 let mut col_arms: Vec<TokenStream2> = Vec::new();
7750 let mut val_arms: Vec<TokenStream2> = Vec::new();
7751 for entry in &fields.column_entries {
7752 if &entry.column == pk_col {
7753 continue;
7754 }
7755 let col = &entry.column;
7756 let ident = &entry.ident;
7757 col_arms.push(quote! { #col => #col, });
7758 val_arms.push(quote! {
7759 #col => _row_vals.push(
7760 ::core::convert::Into::<#root::core::SqlValue>::into(
7761 ::core::clone::Clone::clone(&_o.#ident)
7762 )
7763 ),
7764 });
7765 }
7766 quote! {
7767 /// Write per-row-different values for the named `fields`
7768 /// across every object in `objs`, matched by primary key.
7769 /// Past the backend's bind cap the rows go in batches inside
7770 /// one transaction, so the update is all or none.
7771 ///
7772 /// `fields` names the **columns** to update. The primary
7773 /// key identifies each row and cannot itself be updated
7774 /// (pass it and you get
7775 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7776 /// Empty `objs` or `fields` is a no-op returning `0`.
7777 /// Objects whose PK matches no row are simply not updated.
7778 /// Returns the number of rows affected.
7779 ///
7780 /// Tri-dialect: lowers to a
7781 /// [`#root::core::BulkUpdateQuery`] and dispatches
7782 /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7783 /// a CTE + correlated subquery on SQLite, an inner
7784 /// `JOIN (VALUES …)` on MySQL.
7785 ///
7786 /// # Errors
7787 /// [`#root::core::QueryError::UnknownField`] for a name
7788 /// that isn't a column on this model,
7789 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7790 /// `fields` names the PK, or [`#root::sql::ExecError`] for
7791 /// SQL-writing / driver failures.
7792 pub async fn bulk_update(
7793 objs: &[Self],
7794 fields: &[&str],
7795 pool: &#root::sql::Pool,
7796 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7797 if objs.is_empty() || fields.is_empty() {
7798 return ::core::result::Result::Ok(0);
7799 }
7800 let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7801 let mut _update_columns: ::std::vec::Vec<&'static str> =
7802 ::std::vec::Vec::with_capacity(fields.len());
7803 for &_f in fields {
7804 let _col: &'static str = match _f {
7805 #pk_col => {
7806 return ::core::result::Result::Err(
7807 ::core::convert::Into::into(
7808 #root::core::QueryError::BulkUpdatePrimaryKey {
7809 model: _model_name,
7810 field: ::std::string::ToString::to_string(_f),
7811 }
7812 )
7813 );
7814 }
7815 #( #col_arms )*
7816 _ => {
7817 return ::core::result::Result::Err(
7818 ::core::convert::Into::into(
7819 #root::core::QueryError::UnknownField {
7820 model: _model_name,
7821 field: ::std::string::ToString::to_string(_f),
7822 }
7823 )
7824 );
7825 }
7826 };
7827 // A model whose only field is the primary key emits no
7828 // non-PK arms, so every arm above diverges and this push
7829 // is genuinely unreachable — correct, since such a model
7830 // has nothing to bulk-update. Scoped to the statement
7831 // rather than the fn so a real unreachability elsewhere
7832 // in this body still warns. Without it every PK-only
7833 // model warns in the *user's* crate, which they cannot
7834 // act on (#1290).
7835 #[allow(unreachable_code)]
7836 _update_columns.push(_col);
7837 }
7838 let mut _rows: ::std::vec::Vec<
7839 ::std::vec::Vec<#root::core::SqlValue>,
7840 > = ::std::vec::Vec::with_capacity(objs.len());
7841 for _o in objs.iter() {
7842 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7843 ::std::vec::Vec::with_capacity(fields.len() + 1);
7844 // PK first — the writers expect `[pk, …update cols]`.
7845 _row_vals.push(
7846 ::core::convert::Into::<#root::core::SqlValue>::into(
7847 ::core::clone::Clone::clone(&_o.#pk_ident)
7848 )
7849 );
7850 for &_f in fields {
7851 match _f {
7852 #( #val_arms )*
7853 // Unreachable: every name was validated
7854 // against the same arm set above.
7855 _ => {}
7856 }
7857 }
7858 _rows.push(_row_vals);
7859 }
7860 let _query = #root::core::BulkUpdateQuery::new(
7861 <Self as #root::core::Model>::SCHEMA,
7862 _update_columns,
7863 _rows,
7864 );
7865 #bulk_update_rows_run
7866 }
7867 }
7868 }
7869 };
7870
7871 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7872 quote! {
7873 /// Hidden runtime accessor for the primary-key value as a
7874 /// [`SqlValue`]. Used by reverse-relation helpers
7875 /// (`<parent>::<child>_set`) emitted from sibling models'
7876 /// FK fields. Not part of the public API.
7877 #[doc(hidden)]
7878 pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7879 ::core::convert::Into::<#root::core::SqlValue>::into(
7880 ::core::clone::Clone::clone(&self.#pk_ident)
7881 )
7882 }
7883 }
7884 });
7885
7886 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7887 quote! {
7888 impl #root::sql::HasPkValue for #struct_name {
7889 fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7890 ::core::convert::Into::<#root::core::SqlValue>::into(
7891 ::core::clone::Clone::clone(&self.#pk_ident)
7892 )
7893 }
7894 }
7895 }
7896 });
7897
7898 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7899
7900 // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7901 // dispatch to the right per-backend body without the macro emitting
7902 // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7903 // so audited models with non-Auto PKs (which still go through
7904 // `insert_one_with_audit` → `apply_auto_pk`) link.
7905 let assign_auto_pk_pool_impl = {
7906 let auto_assigns = &fields.auto_assigns;
7907 // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7908 // so the body is structurally identical to `auto_assigns` —
7909 // only the helper name swaps from `try_get_returning` to
7910 // `try_get_returning_sqlite` so the closure typechecks against
7911 // a `SqliteRow` instead of a `PgRow`.
7912 let auto_assigns_sqlite: Vec<TokenStream2> = fields
7913 .auto_field_idents
7914 .iter()
7915 .map(|(ident, column, uuid)| {
7916 // A UUID a column default wrote is TEXT, not the 16 bytes sqlx binds.
7917 if *uuid {
7918 return quote! {
7919 self.#ident = #root::sql::Auto::Set(#root::sql::try_get_flat_sqlite(
7920 _returning_row, #column
7921 )?);
7922 };
7923 }
7924 quote! {
7925 self.#ident = #root::sql::try_get_returning_sqlite(
7926 _returning_row, #column
7927 )?;
7928 }
7929 })
7930 .collect();
7931 // #1028 — decode each `generated_as` column from the same
7932 // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7933 // own type drives the decode (no `Auto<T>` wrapper).
7934 let generated_assigns: Vec<TokenStream2> = fields
7935 .generated_field_idents
7936 .iter()
7937 .map(|(ident, column)| {
7938 quote! {
7939 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7940 }
7941 })
7942 .collect();
7943 let generated_assigns_sqlite: Vec<TokenStream2> = fields
7944 .generated_field_idents
7945 .iter()
7946 .map(|(ident, column)| {
7947 quote! {
7948 self.#ident = #root::sql::try_get_returning_sqlite(
7949 _returning_row, #column
7950 )?;
7951 }
7952 })
7953 .collect();
7954 let mysql_body = if let Some((first, value_ty)) = fields.first_db_auto.as_ref() {
7955 // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7956 // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7957 // Auto<Uuid>/etc. fail to link against MySQL (intended —
7958 // those models can't use AUTO_INCREMENT). The trait is only
7959 // touched on the MySQL arm at runtime, so PG-only consumers
7960 // never see the bound failure.
7961 //
7962 // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7963 // Auto<i64> PK + auto_now_add timestamp) errored hard at
7964 // runtime with "multi-column RETURNING". MySQL has no
7965 // multi-column RETURNING semantic and a follow-up SELECT
7966 // would need cross-trait plumbing. Pragmatic shape: succeed
7967 // with the FIRST Auto field populated from LAST_INSERT_ID();
7968 // any other Auto fields stay `Auto::Unset`. Callers that
7969 // need the DB-defaulted timestamp / UUID can re-fetch the
7970 // row by PK after `save_pool`. Fixes the cookbook chapter
7971 // 12 dialect divergence.
7972 quote! {
7973 let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7974 ::rustango_from_mysql_auto_id(_id)?;
7975 self.#first = #root::sql::Auto::Set(_converted);
7976 ::core::result::Result::Ok(())
7977 }
7978 } else {
7979 quote! {
7980 let _ = _id;
7981 ::core::result::Result::Ok(())
7982 }
7983 };
7984 quote! {
7985 impl #root::sql::AssignAutoPkPool for #struct_name {
7986 fn __rustango_assign_from_pg_row(
7987 &mut self,
7988 _returning_row: &#root::sql::PgReturningRow,
7989 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7990 #( #auto_assigns )*
7991 #( #generated_assigns )*
7992 ::core::result::Result::Ok(())
7993 }
7994 fn __rustango_assign_from_mysql_id(
7995 &mut self,
7996 _id: i64,
7997 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7998 #mysql_body
7999 }
8000 fn __rustango_assign_from_sqlite_row(
8001 &mut self,
8002 _returning_row: &#root::sql::SqliteReturningRow,
8003 ) -> ::core::result::Result<(), #root::sql::ExecError> {
8004 #( #auto_assigns_sqlite )*
8005 #( #generated_assigns_sqlite )*
8006 ::core::result::Result::Ok(())
8007 }
8008 }
8009 }
8010 };
8011
8012 let from_aliased_row_inits = &fields.from_aliased_row_inits;
8013 let aliased_row_helper = quote! {
8014 /// Decode a row's aliased target columns (produced by
8015 /// `select_related`'s LEFT JOIN) into a fresh instance of
8016 /// this model. Reads each column via
8017 /// `format!("{prefix}__{col}")`, matching the alias the
8018 /// SELECT writer emitted. Slice 9.0d.
8019 #[doc(hidden)]
8020 #[cfg(feature = "postgres")]
8021 pub fn __rustango_from_aliased_row(
8022 row: &#root::sql::sqlx::postgres::PgRow,
8023 prefix: &str,
8024 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8025 ::core::result::Result::Ok(Self {
8026 #( #from_aliased_row_inits ),*
8027 })
8028 }
8029 };
8030 // v0.23.0-batch8 — MySQL counterpart, gated through the
8031 // cfg-aware macro_rules so PG-only builds expand to nothing.
8032 let from_aliased_row_inits_my = &fields.from_aliased_row_inits_my;
8033 let aliased_row_helper_my = quote! {
8034 #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
8035 #( #from_aliased_row_inits_my ),*
8036 });
8037 };
8038
8039 // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
8040 // pattern + cfg gate on the `sqlite` feature.
8041 let aliased_row_helper_sqlite = quote! {
8042 #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
8043 #( #from_aliased_row_inits ),*
8044 });
8045 };
8046
8047 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
8048 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
8049 let load_related_impl_sqlite =
8050 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
8051
8052 // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
8053 // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
8054 // default `Self::objects()`. Each accessor returns a fresh
8055 // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
8056 // methods the user defined.
8057 let extra_manager_fns: Vec<TokenStream2> = manager_fns
8058 .iter()
8059 .map(|fn_ident| {
8060 let model_name_str = struct_name.to_string();
8061 let fn_name_str = fn_ident.to_string();
8062 let doc = format!(
8063 "Custom-named QuerySet accessor for [`{model_name_str}`]. \
8064 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
8065 equivalent to `Self::objects()`. Chains with any \
8066 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
8067 extension methods."
8068 );
8069 quote! {
8070 #[doc = #doc]
8071 #[must_use]
8072 pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
8073 #root::query::QuerySet::new()
8074 }
8075 }
8076 })
8077 .collect();
8078
8079 quote! {
8080 impl #struct_name {
8081 /// Start a new `QuerySet` over this model.
8082 #[must_use]
8083 pub fn objects() -> #root::query::QuerySet<#struct_name> {
8084 #root::query::QuerySet::new()
8085 }
8086
8087 /// Eloquent-shape alias of [`Self::objects`]. Returns
8088 /// a fresh `QuerySet<Self>` ready for `.filter()` /
8089 /// `.where_()` / etc. Matches Laravel muscle-memory:
8090 ///
8091 /// ```ignore
8092 /// // Eloquent: Post::query()->where('published', true)
8093 /// // rustango: Post::query().filter("published", true)
8094 /// // or: Post::objects().filter("published", true)
8095 /// ```
8096 ///
8097 /// Both names point at the same underlying constructor;
8098 /// neither is preferred.
8099 #[must_use]
8100 pub fn query() -> #root::query::QuerySet<#struct_name> {
8101 #root::query::QuerySet::new()
8102 }
8103
8104 #( #extra_manager_fns )*
8105
8106 #insert_method
8107
8108 #bulk_insert_method
8109
8110 #bulk_upsert_pool_method
8111
8112 #bulk_update_method
8113
8114 #save_method
8115
8116 #pk_methods
8117
8118 #pk_value_helper
8119
8120 #aliased_row_helper
8121
8122 #column_consts
8123 }
8124
8125 #aliased_row_helper_my
8126
8127 #aliased_row_helper_sqlite
8128
8129 #load_related_impl
8130
8131 #load_related_impl_my
8132
8133 #load_related_impl_sqlite
8134
8135 #has_pk_value_impl
8136
8137 #fk_pk_access_impl
8138
8139 #assign_auto_pk_pool_impl
8140 }
8141}
8142
8143/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
8144/// `auto_assigns` but reading from `_returning_row` and writing to
8145/// `_row_mut` instead of `self`.
8146fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
8147 let root = rustango_root();
8148 let lines = fields.auto_field_idents.iter().map(|(ident, column, _)| {
8149 let col_lit = column.as_str();
8150 quote! {
8151 _row_mut.#ident = #root::sql::sqlx::Row::try_get(
8152 _returning_row,
8153 #col_lit,
8154 )?;
8155 }
8156 });
8157 quote! { #( #lines )* }
8158}
8159
8160/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
8161fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
8162 let lines = entries.iter().map(|e| {
8163 let ident = &e.ident;
8164 let col_ty = column_type_ident(ident);
8165 quote! {
8166 #[allow(non_upper_case_globals)]
8167 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
8168 }
8169 });
8170 quote! { #(#lines)* }
8171}
8172
8173/// Emit a hidden per-model module carrying one zero-sized type per field,
8174/// each with a `Column` impl pointing back at the model.
8175fn column_module_tokens(
8176 module_ident: &syn::Ident,
8177 struct_name: &syn::Ident,
8178 entries: &[ColumnEntry],
8179) -> TokenStream2 {
8180 let root = rustango_root();
8181 let items = entries.iter().map(|e| {
8182 let col_ty = column_type_ident(&e.ident);
8183 let value_ty = &e.value_ty;
8184 let name = &e.name;
8185 let column = &e.column;
8186 let field_type_tokens = &e.field_type_tokens;
8187 quote! {
8188 #[derive(::core::clone::Clone, ::core::marker::Copy)]
8189 pub struct #col_ty;
8190
8191 impl #root::core::Column for #col_ty {
8192 type Model = super::#struct_name;
8193 type Value = #value_ty;
8194 const NAME: &'static str = #name;
8195 const COLUMN: &'static str = #column;
8196 const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
8197 }
8198 }
8199 });
8200 quote! {
8201 #[doc(hidden)]
8202 #[allow(non_camel_case_types, non_snake_case)]
8203 pub mod #module_ident {
8204 // Re-import the parent scope so field types referencing
8205 // sibling models (e.g. `ForeignKey<Author>`) resolve
8206 // inside this submodule. Without this we'd hit
8207 // `proc_macro_derive_resolution_fallback` warnings.
8208 #[allow(unused_imports)]
8209 use super::*;
8210 #(#items)*
8211 }
8212 }
8213}
8214
8215fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
8216 let name = ident_name(field_ident);
8217 syn::Ident::new(&format!("{name}_col"), field_ident.span())
8218}
8219
8220/// Run `body` (of type `ret`) and put the `auto_now` fields back unless
8221/// `keep` holds for `_saved`, so a failed or no-row write keeps the old stamp.
8222fn restore_auto_now_unless(
8223 idents: &[syn::Ident],
8224 ret: &TokenStream2,
8225 keep: &TokenStream2,
8226 body: &TokenStream2,
8227) -> TokenStream2 {
8228 if idents.is_empty() {
8229 return body.clone();
8230 }
8231 let prev: Vec<syn::Ident> = (0..idents.len())
8232 .map(|i| quote::format_ident!("__rustango_auto_now_prev_{}", i))
8233 .collect();
8234 quote! {
8235 #( let #prev = ::core::clone::Clone::clone(&self.#idents); )*
8236 let _saved: #ret = async { #body }.await;
8237 if !(#keep) {
8238 #( self.#idents = #prev; )*
8239 }
8240 _saved
8241 }
8242}
8243
8244/// A field's name without the `r#` of a raw ident: `r#type` is `type` (#1937).
8245fn ident_name(ident: &syn::Ident) -> String {
8246 syn::ext::IdentExt::unraw(ident).to_string()
8247}
8248
8249/// The ident for a field named `name`, raw when it is a keyword (`type` → `r#type`).
8250fn ident_from_name(name: &str, span: proc_macro2::Span) -> syn::Ident {
8251 match syn::parse_str::<syn::Ident>(name) {
8252 Ok(mut ident) => {
8253 ident.set_span(span);
8254 ident
8255 }
8256 Err(_) => syn::Ident::new_raw(name, span),
8257 }
8258}
8259
8260fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8261 syn::Ident::new(
8262 &format!("__rustango_cols_{struct_name}"),
8263 struct_name.span(),
8264 )
8265}
8266
8267fn from_row_impl_tokens(
8268 struct_name: &syn::Ident,
8269 from_row_inits: &[TokenStream2],
8270 from_row_inits_my: &[TokenStream2],
8271) -> TokenStream2 {
8272 let root = rustango_root();
8273 // The Postgres impl is always emitted — every rustango build pulls in
8274 // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8275 // routed through `#root::__impl_my_from_row!`, a cfg-gated
8276 // macro_rules whose body collapses to nothing when rustango is built
8277 // without the `mysql` feature. No user-facing feature shim required.
8278 //
8279 // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8280 // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8281 // back into a comma-separated list inside square brackets. Since each
8282 // entry is already in `field: expr` shape, the existing tokens slot in.
8283 quote! {
8284 #[cfg(feature = "postgres")]
8285 impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8286 for #struct_name
8287 {
8288 fn from_row(
8289 row: &'r #root::sql::sqlx::postgres::PgRow,
8290 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8291 ::core::result::Result::Ok(Self {
8292 #( #from_row_inits ),*
8293 })
8294 }
8295 }
8296
8297 #root::__impl_my_from_row!(#struct_name, |row| {
8298 #( #from_row_inits_my ),*
8299 });
8300
8301 #root::__impl_sqlite_from_row!(#struct_name, |row| {
8302 #( #from_row_inits ),*
8303 });
8304 }
8305}
8306
8307struct ContainerAttrs {
8308 table: Option<String>,
8309 display: Option<(String, proc_macro2::Span)>,
8310 /// Explicit app label from `#[rustango(app = "blog")]`.
8311 /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8312 /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8313 /// at runtime — this attribute is the override for cases where
8314 /// the inference is wrong (e.g. a model that conceptually belongs
8315 /// to one app but is physically in another module).
8316 app: Option<String>,
8317 /// Per-model admin knobs from
8318 /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8319 /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8320 /// admin code falls back to `AdminConfig::DEFAULT`.
8321 admin: Option<AdminAttrs>,
8322 /// Per-model audit configuration from `#[rustango(audit(...))]`.
8323 /// `None` when the model isn't audited — write paths emit no
8324 /// audit entries. When present, single-row writes capture
8325 /// before/after for the listed fields and bulk writes batch
8326 /// snapshots into one INSERT into `rustango_audit_log`.
8327 audit: Option<AuditAttrs>,
8328 /// `true` when `#[rustango(permissions)]` is present. Signals that
8329 /// `auto_create_permissions` should seed the four CRUD codenames for
8330 /// this model.
8331 permissions: bool,
8332 /// Many-to-many relations declared via
8333 /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8334 /// src = "post_id", dst = "tag_id"))]`.
8335 m2m: Vec<M2MAttr>,
8336 /// Polymorphic M2M relations declared via
8337 /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8338 /// pk_column = "taggable_id", ct_column = "taggable_type",
8339 /// related_column = "tag_id"))]` (issue #818).
8340 generic_m2m: Vec<GenericM2MAttr>,
8341 /// Composite indexes declared via
8342 /// `#[rustango(index("col1, col2"))]` or
8343 /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8344 /// Single-column indexes from `#[rustango(index)]` on fields are
8345 /// accumulated here during field collection.
8346 indexes: Vec<IndexAttr>,
8347 /// Table-level CHECK constraints declared via
8348 /// `#[rustango(check(name = "…", expr = "…"))]`.
8349 checks: Vec<CheckAttr>,
8350 /// Table-level PG `EXCLUDE` constraints declared via
8351 /// `#[rustango(exclude(name = "…", using = "gist", elements =
8352 /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8353 /// migration writer renders them on Postgres and skips with a
8354 /// warning on MySQL/SQLite. Issue #319.
8355 excludes: Vec<ExcludeAttr>,
8356 /// Composite (multi-column) FKs declared via
8357 /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8358 /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8359 composite_fks: Vec<CompositeFkAttr>,
8360 /// Generic ("any model") FKs declared via
8361 /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8362 /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8363 generic_fks: Vec<GenericFkAttr>,
8364 /// Where this model lives in a tenancy deployment, declared via
8365 /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8366 /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8367 /// to partition diff output between registry-scoped and
8368 /// tenant-scoped migration files.
8369 scope: Option<String>,
8370 /// Extra QuerySet accessor names from
8371 /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8372 /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8373 /// the default `Self::objects()`. Multiple attributes allowed.
8374 manager_fns: Vec<syn::Ident>,
8375 /// Default ordering declared via `#[rustango(default_order =
8376 /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8377 /// `(column_name, desc_flag, span_for_error_reporting)` — the
8378 /// `-` prefix means descending; the `+` prefix or no prefix means
8379 /// ascending.
8380 default_order: Vec<(String, bool, proc_macro2::Span)>,
8381 /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8382 /// Routes the emitted schema's `is_view = true` so the migration
8383 /// snapshot skips this model (its underlying SQL view is operator-
8384 /// managed, not rustango-managed).
8385 is_view: bool,
8386 /// `#[rustango(managed = false)]` — who owns the table.
8387 /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8388 /// the migration snapshot skips this model so `makemigrations` /
8389 /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8390 /// TABLE` against it (operator-managed schema).
8391 managed: bool,
8392 /// Name of the model's base manager, from
8393 /// `#[rustango(base_manager_name = "...")]`. Threaded into
8394 /// `ModelSchema::base_manager_name`. Declarative-only today.
8395 base_manager_name: Option<String>,
8396 /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8397 /// field this model's instances are ordered relative to.
8398 /// Declarative-only today; threaded onto
8399 /// `ModelSchema::order_with_respect_to`.
8400 order_with_respect_to: Option<String>,
8401 /// `#[rustango(proxy)]` / `#[rustango(proxy = true)]`.
8402 /// Marks the model as a proxy that
8403 /// shares its DB table with another struct. Threaded into
8404 /// `ModelSchema::proxy` so future codegen can skip table-owning
8405 /// behavior for proxies.
8406 proxy: bool,
8407 /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8408 /// Each comma-separated capability token surfaces on
8409 /// `ModelSchema::required_db_features` so `manage check --deploy`
8410 /// can warn when the active dialect lacks one.
8411 required_db_features: Vec<String>,
8412 /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8413 /// Normalized to the dialect name `manage check --deploy`
8414 /// compares against `Settings.database.backend`. Aliases
8415 /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8416 /// stored under the canonical name.
8417 required_db_vendor: Option<String>,
8418 /// `#[rustango(default_related_name = "...")]`. Threaded into
8419 /// `ModelSchema::default_related_name`. Reverse-relation accessor
8420 /// name to use when an FK / M2M field doesn't override it.
8421 /// Today rustango doesn't auto-emit reverse managers; the
8422 /// metadata is the foundation for that work.
8423 default_related_name: Option<String>,
8424 /// `#[rustango(db_table_comment = "...")]`. Threaded into
8425 /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8426 /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8427 /// inline `COMMENT='...'`, SQLite: no-op).
8428 db_table_comment: Option<String>,
8429 /// `#[rustango(get_latest_by = "created_at")]` /
8430 /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8431 /// `(column, descending)` where `descending = true` when the
8432 /// attribute value starts with `-`. Threaded into
8433 /// `ModelSchema::get_latest_by`.
8434 get_latest_by: Option<(String, bool)>,
8435 /// Extra `(codename, label)` permissions
8436 /// from `#[rustango(extra_permissions = "approve:Can approve,
8437 /// archive:Can archive")]`. Comma-separated `codename:label`
8438 /// pairs. Threaded into `ModelSchema::extra_permissions`.
8439 extra_permissions: Vec<(String, String)>,
8440 /// Which CRUD codenames (`"add"` / `"change"` / `"delete"` /
8441 /// `"view"`) the framework
8442 /// auto-creates. Empty `Vec` (default) means **all four**. Set via
8443 /// `#[rustango(default_permissions = "view,change")]` to opt out.
8444 /// Validated at parse time; unknown actions fail with a span-pointing
8445 /// error.
8446 default_permissions: Vec<String>,
8447 /// `#[rustango(verbose_name = "blog post")]` — the
8448 /// human-readable singular label for the model. Threaded into
8449 /// `ModelSchema::verbose_name` so admin section headers /
8450 /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8451 /// over the Rust struct identifier.
8452 verbose_name: Option<String>,
8453 /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8454 /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8455 /// When unset, `display_label_plural()` auto-suffixes `s`.
8456 verbose_name_plural: Option<String>,
8457 /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8458 /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8459 /// a name (used by `QuerySet::without_global_scope`) with a
8460 /// `fn() -> WhereExpr` path that the macro emits into
8461 /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8462 global_scopes: Vec<GlobalScopeAttr>,
8463 /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8464 /// `#[rustango(through(name, far, far_fk_column, intermediate,
8465 /// intermediate_fk_column, intermediate_pk_column))]` — issue
8466 /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8467 /// entry emits an inherent `<name>_through(&self)` accessor that
8468 /// returns a `QuerySet<Far>` filtered via a correlated subquery
8469 /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8470 /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8471 through_relations: Vec<ThroughAttr>,
8472 /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8473 /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8474 /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8475 /// Each entry emits two associated functions on the parent —
8476 /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8477 /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8478 /// over a correlated subquery against the child table. Users
8479 /// drop the result into `QuerySet::where_raw(...)`.
8480 reverse_has_relations: Vec<ReverseHasAttr>,
8481 /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8482 /// issue #830. Each emits a `Model::generic_reverse_relations()`
8483 /// entry so the relation-existence family resolves polymorphic
8484 /// children by name.
8485 generic_has_relations: Vec<GenericHasAttr>,
8486}
8487
8488/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8489/// declaration. Each entry becomes one `core::GlobalScope` in the
8490/// emitted schema literal; `apply` resolves at macro-expand time
8491/// against the consumer's scope so the function must be in scope at
8492/// the use site. Issue #820.
8493struct GlobalScopeAttr {
8494 name: String,
8495 apply: syn::Path,
8496}
8497
8498/// Parsed `#[rustango(through(...))]` declaration. Issue
8499/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8500/// `hasManyThrough` / `hasOneThrough` parity.
8501///
8502/// `Country hasManyThrough Post via User` declares as:
8503///
8504/// ```ignore
8505/// #[rustango(through(
8506/// name = "posts",
8507/// far = "Post",
8508/// far_fk_column = "author_id",
8509/// intermediate = "User",
8510/// intermediate_fk_column = "country_id",
8511/// ))]
8512/// struct Country { ... }
8513/// ```
8514///
8515/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8516/// which returns a queryset filtered via a correlated subquery —
8517/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8518/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8519/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8520/// because the subquery lives inside a `WhereExpr::InSubquery` node
8521/// and the outer queryset's pending list stays empty.
8522///
8523/// All four required arguments use **SQL column / table names**
8524/// (not Rust field names) to sidestep the multi-hop-filter substrate
8525/// gap. Once that substrate lands, a higher-level Rust-field-name
8526/// shorthand can be added without breaking this surface.
8527struct ThroughAttr {
8528 /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8529 name: String,
8530 /// Far model type identifier. `far = "Post"` → returns
8531 /// `QuerySet<Post>`. Resolved verbatim against the scope where
8532 /// the derive expands.
8533 far: syn::Ident,
8534 /// SQL column on the far model's table that references the
8535 /// intermediate model's primary key. For `Post`'s
8536 /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8537 /// default `<field>_id` convention) or whatever the user
8538 /// declared via `#[rustango(db_column = "...")]`.
8539 far_fk_column: String,
8540 /// Intermediate model type identifier. Needed to look up its
8541 /// `SCHEMA` so the subquery's `FROM` clause points at the
8542 /// intermediate table. `intermediate = "User"`.
8543 intermediate: syn::Ident,
8544 /// SQL column on the intermediate's table that references the
8545 /// source (this) model's primary key. For `User`'s
8546 /// `country: ForeignKey<Country>` the column is `"country_id"`.
8547 intermediate_fk_column: String,
8548 /// SQL primary-key column on the intermediate's table — the
8549 /// column the subquery projects. Optional; defaults to `"id"`
8550 /// (rustango's default PK column name). Override when the
8551 /// intermediate declares a custom PK column.
8552 intermediate_pk_column: String,
8553}
8554
8555/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8556/// child_fk_column = "..."))]` declaration. Issue
8557/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8558/// `whereHas` / `whereDoesntHave` parity.
8559///
8560/// `Post hasMany Comment` declares as:
8561///
8562/// ```ignore
8563/// #[rustango(reverse_has(
8564/// name = "comments",
8565/// child = "Comment",
8566/// child_fk_column = "post_id",
8567/// ))]
8568/// struct Post { ... }
8569/// ```
8570///
8571/// The macro emits two associated functions on `Post`:
8572///
8573/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8574/// `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8575/// <outer>.<self_pk_column>)`.
8576/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8577/// but `NOT EXISTS`, the `whereDoesntHave` analog.
8578///
8579/// User code:
8580///
8581/// ```ignore
8582/// // Posts with at least one comment:
8583/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8584/// // Posts with no comments:
8585/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8586/// ```
8587///
8588/// As with #817, all column / table identifiers are **SQL names**
8589/// (not Rust field names) so the substrate is independent of the
8590/// outstanding multi-hop filter resolver gap. The emitted
8591/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8592/// SQL-emit time via the writer's scope stack.
8593struct ReverseHasAttr {
8594 /// Accessor name. `name = "comments"` → emits
8595 /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8596 name: String,
8597 /// Child model type identifier. `child = "Comment"` — needed to
8598 /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8599 /// points at the child table.
8600 child: syn::Ident,
8601 /// SQL column on the child's table that references this model's
8602 /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8603 /// column is `"post_id"`.
8604 child_fk_column: String,
8605 /// SQL primary-key column on **this** model's table — the column
8606 /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8607 /// (rustango's default PK column name).
8608 self_pk_column: String,
8609}
8610
8611/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8612/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8613/// relation-existence family (issue #830). The child is a polymorphic,
8614/// content-type-discriminated model; emits a `Model::
8615/// generic_reverse_relations()` entry the queryset resolves by name.
8616struct GenericHasAttr {
8617 /// Accessor name, e.g. `name = "tags"`.
8618 name: String,
8619 /// Child model type identifier (`child = "Tag"`) — looked up for its
8620 /// `SCHEMA` so the subquery's `FROM` points at the child table.
8621 child: syn::Ident,
8622 /// Column on the child table holding the parent's content-type id.
8623 /// Optional; defaults to `"content_type_id"`.
8624 ct_column: String,
8625 /// Column on the child table holding the parent's PK value.
8626 /// Optional; defaults to `"object_pk"`.
8627 pk_column: String,
8628 /// SQL primary-key column on **this** (parent) model's table.
8629 /// Optional; defaults to `"id"`.
8630 self_pk_column: String,
8631}
8632
8633/// Parsed form of one index declaration (field-level or container-level).
8634struct IndexAttr {
8635 /// Index name; auto-derived when `None` at parse time.
8636 name: Option<String>,
8637 /// Column names in the index.
8638 columns: Vec<String>,
8639 /// `true` for `CREATE UNIQUE INDEX`.
8640 unique: bool,
8641 /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8642 /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8643 /// `"btree"` when the attribute is absent — the DDL writer omits
8644 /// the `USING` clause and the backend uses its own default
8645 /// (btree on every supported dialect).
8646 method: String,
8647 /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8648 /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8649 /// condition = "...", name = "..."))]`. `None` for plain indexes.
8650 where_clause: Option<String>,
8651 /// Covering-index columns (PG 11+ `INCLUDE (...)` clause).
8652 /// Empty `Vec` (the default) means "no covering columns".
8653 include: Vec<String>,
8654}
8655
8656/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8657struct CheckAttr {
8658 name: String,
8659 expr: String,
8660}
8661
8662/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8663/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8664/// PG-only — surfaced on every backend in the macro emit; the migration
8665/// writer skips the op on MySQL/SQLite. Issue #319.
8666struct ExcludeAttr {
8667 /// Constraint name (free-form Rust identifier).
8668 name: String,
8669 /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8670 using: String,
8671 /// Comma-separated `(column, operator)` pairs, in declaration
8672 /// order. Parsed from `"col WITH op, col WITH op"`.
8673 elements: Vec<(String, String)>,
8674 /// Optional `WHERE` predicate (raw SQL).
8675 where_clause: Option<String>,
8676}
8677
8678/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8679/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8680/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8681/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8682/// the model, not the field.
8683struct CompositeFkAttr {
8684 /// Logical relation name (free-form Rust identifier).
8685 name: String,
8686 /// SQL table name of the target.
8687 to: String,
8688 /// Source-side column names, in declaration order.
8689 from: Vec<String>,
8690 /// Target-side column names, same length / order as `from`.
8691 on: Vec<String>,
8692}
8693
8694/// Parsed form of one `#[rustango(generic_fk(name = "target",
8695/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8696/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8697/// generic ("any model") FKs live on the model, not the field.
8698struct GenericFkAttr {
8699 /// Logical relation name (free-form Rust identifier).
8700 name: String,
8701 /// Source-side column carrying the `content_type_id` value.
8702 ct_column: String,
8703 /// Source-side column carrying the target row's primary key.
8704 pk_column: String,
8705}
8706
8707/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8708struct M2MAttr {
8709 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8710 name: String,
8711 /// Target table (e.g. `"app_tags"`).
8712 to: String,
8713 /// Junction table (e.g. `"post_tags"`).
8714 through: String,
8715 /// Source FK column in the junction table (e.g. `"post_id"`).
8716 src: String,
8717 /// Destination FK column in the junction table (e.g. `"tag_id"`).
8718 dst: String,
8719 /// Whether the migration writer should auto-create the junction
8720 /// table. Default `true`. Set `auto_create = false` (#324) when
8721 /// the operator declares the through table as its own
8722 /// `#[derive(Model)]` struct with extra columns.
8723 auto_create: bool,
8724}
8725
8726/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8727/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8728/// junction carries a ContentType discriminator so unrelated models
8729/// share one pivot + related set.
8730struct GenericM2MAttr {
8731 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8732 name: String,
8733 /// Polymorphic junction table (e.g. `"taggables"`).
8734 through: String,
8735 /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8736 pk_column: String,
8737 /// Junction column holding the owning model's `content_type_id`
8738 /// discriminator (e.g. `"taggable_type"`).
8739 ct_column: String,
8740 /// Junction column holding the related model PK (e.g. `"tag_id"`).
8741 related_column: String,
8742}
8743
8744/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8745/// "user"))]`. `track` is a comma-separated list of field names whose
8746/// before/after values land in the JSONB `changes` column. `source`
8747/// is informational only — it pins a default source when the model
8748/// is written outside any `audit::with_source(...)` scope (rare).
8749#[derive(Default)]
8750struct AuditAttrs {
8751 /// Field names to capture in the `changes` JSONB. Validated
8752 /// against declared scalar fields at compile time. Empty means
8753 /// "track every scalar field".
8754 track: Option<(Vec<String>, proc_macro2::Span)>,
8755}
8756
8757/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8758/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8759/// comma-separated strings; we validate each ident against the model's
8760/// declared fields at compile time.
8761#[derive(Default)]
8762struct AdminAttrs {
8763 list_display: Option<(Vec<String>, proc_macro2::Span)>,
8764 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8765 list_per_page: Option<usize>,
8766 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8767 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8768 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8769 /// Bulk action names. No field-validation against model fields —
8770 /// these are action handlers, not column references.
8771 actions: Option<(Vec<String>, proc_macro2::Span)>,
8772 /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8773 /// sections, comma-separated fields per section, optional
8774 /// `Title:` prefix. Empty title omits the `<legend>`.
8775 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8776 /// `admin(list_display_links = "title")`. Names
8777 /// from `list_display` whose cells should link to detail/edit.
8778 /// Issue #350.
8779 list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8780 /// `admin(search_help_text = "...")`. Short
8781 /// caption rendered beside the admin list view's search box.
8782 /// Issue #353.
8783 search_help_text: Option<String>,
8784 /// `admin(actions_on_top = false)`. Hides the
8785 /// action-bar above the table. Default `true`. Issue #354.
8786 actions_on_top: Option<bool>,
8787 /// `admin(actions_on_bottom = true)`. Renders an
8788 /// additional action-bar below the table. Default `false`.
8789 /// Issue #354.
8790 actions_on_bottom: Option<bool>,
8791 /// `admin(date_hierarchy = "created_at")`. Name of
8792 /// a date / datetime field whose values render as a clickable
8793 /// year / month / day drill-down strip above the list table.
8794 /// Empty / unset disables the strip. Issue #355.
8795 date_hierarchy: Option<String>,
8796 /// `admin(prepopulated_fields = "slug:title")`.
8797 /// Each entry is `target:source[+source2]`; multiple entries are
8798 /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8799 /// The admin change-form emits JS that slugifies the source values
8800 /// into the target field on every keystroke. Issue #356.
8801 prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8802 /// `admin(raw_id_fields = "parent, owner")`. Names
8803 /// of FK fields whose change-form widget renders a lookup link
8804 /// next to the input. Issue #357.
8805 raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8806 /// `admin(autocomplete_fields = "author_id")`.
8807 /// Names of FK fields whose change-form widget renders an
8808 /// Ajax-driven typeahead populated from a `__autocomplete`
8809 /// endpoint on the target model. Issue #358.
8810 autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8811 /// `admin(list_select_related = "all" | "none" | "author, …")`
8812 /// — tunes the admin list view's FK auto-JOIN
8813 /// policy. Default `"all"` matches rustango's join-everything
8814 /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8815 list_select_related: Option<String>,
8816 /// `admin(formfield_overrides = "field:widget, field2:widget2")`.
8817 /// Each entry is `field_name:widget_name`; multiple
8818 /// entries comma-separated. Empty / unset → no overrides. The
8819 /// list of widget names supported is documented on
8820 /// `AdminConfig::formfield_overrides`. Issue #359.
8821 formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8822}
8823
8824fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8825 let mut out = ContainerAttrs {
8826 table: None,
8827 display: None,
8828 app: None,
8829 admin: None,
8830 audit: None,
8831 // Default `permissions = true` so every `#[derive(Model)]`
8832 // gets the four CRUD codenames seeded by `auto_create_permissions`
8833 // and is visible to non-superusers in the tenant admin without
8834 // manual per-model annotation. Models that intentionally don't
8835 // want permission rows (registry-internal types, framework
8836 // tables operators shouldn't manage directly) opt out via
8837 // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8838 // out-of-the-box admin invisibility regression (#62).
8839 permissions: true,
8840 m2m: Vec::new(),
8841 generic_m2m: Vec::new(),
8842 indexes: Vec::new(),
8843 checks: Vec::new(),
8844 excludes: Vec::new(),
8845 composite_fks: Vec::new(),
8846 generic_fks: Vec::new(),
8847 scope: None,
8848 manager_fns: Vec::new(),
8849 default_order: Vec::new(),
8850 is_view: false,
8851 managed: true,
8852 verbose_name: None,
8853 verbose_name_plural: None,
8854 base_manager_name: None,
8855 order_with_respect_to: None,
8856 proxy: false,
8857 required_db_features: Vec::new(),
8858 required_db_vendor: None,
8859 default_related_name: None,
8860 db_table_comment: None,
8861 get_latest_by: None,
8862 extra_permissions: Vec::new(),
8863 default_permissions: Vec::new(),
8864 global_scopes: Vec::new(),
8865 through_relations: Vec::new(),
8866 reverse_has_relations: Vec::new(),
8867 generic_has_relations: Vec::new(),
8868 };
8869 for attr in &input.attrs {
8870 if !attr.path().is_ident("rustango") {
8871 continue;
8872 }
8873 attr.parse_nested_meta(|meta| {
8874 if meta.path.is_ident("table") {
8875 let s: LitStr = meta.value()?.parse()?;
8876 let name = s.value();
8877 // v0.27.3 (#65) — macro-time guard against table names
8878 // that compile but break SQL downstream. Hyphens are
8879 // the common footgun: PostgreSQL accepts a quoted
8880 // `"intermediate-region"` in CREATE TABLE, but the
8881 // FK / index name derivation in `migrate::ddl`
8882 // emits `intermediate-region_field_fkey` unquoted,
8883 // which then fails the SQL parser. Same shape rule
8884 // as Postgres regular identifiers so the safe path
8885 // is the only path.
8886 validate_table_name(&name, s.span())?;
8887 out.table = Some(name);
8888 return Ok(());
8889 }
8890 if meta.path.is_ident("display") {
8891 let s: LitStr = meta.value()?.parse()?;
8892 out.display = Some((s.value(), s.span()));
8893 return Ok(());
8894 }
8895 if meta.path.is_ident("app") {
8896 let s: LitStr = meta.value()?.parse()?;
8897 out.app = Some(s.value());
8898 return Ok(());
8899 }
8900 if meta.path.is_ident("scope") {
8901 let s: LitStr = meta.value()?.parse()?;
8902 let val = s.value();
8903 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8904 return Err(meta.error(format!(
8905 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8906 )));
8907 }
8908 out.scope = Some(val);
8909 return Ok(());
8910 }
8911 if meta.path.is_ident("admin") {
8912 let mut admin = AdminAttrs::default();
8913 meta.parse_nested_meta(|inner| {
8914 if inner.path.is_ident("list_display") {
8915 let s: LitStr = inner.value()?.parse()?;
8916 admin.list_display =
8917 Some((split_field_list(&s.value()), s.span()));
8918 return Ok(());
8919 }
8920 if inner.path.is_ident("search_fields") {
8921 let s: LitStr = inner.value()?.parse()?;
8922 admin.search_fields =
8923 Some((split_field_list(&s.value()), s.span()));
8924 return Ok(());
8925 }
8926 if inner.path.is_ident("readonly_fields") {
8927 let s: LitStr = inner.value()?.parse()?;
8928 admin.readonly_fields =
8929 Some((split_field_list(&s.value()), s.span()));
8930 return Ok(());
8931 }
8932 if inner.path.is_ident("list_per_page") {
8933 let lit: syn::LitInt = inner.value()?.parse()?;
8934 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8935 return Ok(());
8936 }
8937 if inner.path.is_ident("ordering") {
8938 let s: LitStr = inner.value()?.parse()?;
8939 admin.ordering = Some((
8940 parse_ordering_list(&s.value()),
8941 s.span(),
8942 ));
8943 return Ok(());
8944 }
8945 if inner.path.is_ident("list_filter") {
8946 let s: LitStr = inner.value()?.parse()?;
8947 admin.list_filter =
8948 Some((split_field_list(&s.value()), s.span()));
8949 return Ok(());
8950 }
8951 if inner.path.is_ident("actions") {
8952 let s: LitStr = inner.value()?.parse()?;
8953 admin.actions =
8954 Some((split_field_list(&s.value()), s.span()));
8955 return Ok(());
8956 }
8957 if inner.path.is_ident("fieldsets") {
8958 let s: LitStr = inner.value()?.parse()?;
8959 admin.fieldsets =
8960 Some((parse_fieldset_list(&s.value()), s.span()));
8961 return Ok(());
8962 }
8963 if inner.path.is_ident("list_display_links") {
8964 let s: LitStr = inner.value()?.parse()?;
8965 admin.list_display_links =
8966 Some((split_field_list(&s.value()), s.span()));
8967 return Ok(());
8968 }
8969 if inner.path.is_ident("search_help_text") {
8970 let s: LitStr = inner.value()?.parse()?;
8971 admin.search_help_text = Some(s.value());
8972 return Ok(());
8973 }
8974 if inner.path.is_ident("actions_on_top") {
8975 let lit: syn::LitBool = inner.value()?.parse()?;
8976 admin.actions_on_top = Some(lit.value);
8977 return Ok(());
8978 }
8979 if inner.path.is_ident("actions_on_bottom") {
8980 let lit: syn::LitBool = inner.value()?.parse()?;
8981 admin.actions_on_bottom = Some(lit.value);
8982 return Ok(());
8983 }
8984 if inner.path.is_ident("date_hierarchy") {
8985 let s: LitStr = inner.value()?.parse()?;
8986 admin.date_hierarchy = Some(s.value());
8987 return Ok(());
8988 }
8989 if inner.path.is_ident("prepopulated_fields") {
8990 let s: LitStr = inner.value()?.parse()?;
8991 admin.prepopulated_fields =
8992 Some((parse_prepopulated_list(&s.value()), s.span()));
8993 return Ok(());
8994 }
8995 if inner.path.is_ident("raw_id_fields") {
8996 let s: LitStr = inner.value()?.parse()?;
8997 admin.raw_id_fields =
8998 Some((split_field_list(&s.value()), s.span()));
8999 return Ok(());
9000 }
9001 if inner.path.is_ident("autocomplete_fields") {
9002 let s: LitStr = inner.value()?.parse()?;
9003 admin.autocomplete_fields =
9004 Some((split_field_list(&s.value()), s.span()));
9005 return Ok(());
9006 }
9007 if inner.path.is_ident("list_select_related") {
9008 let s: LitStr = inner.value()?.parse()?;
9009 admin.list_select_related = Some(s.value());
9010 return Ok(());
9011 }
9012 if inner.path.is_ident("formfield_overrides") {
9013 let s: LitStr = inner.value()?.parse()?;
9014 admin.formfield_overrides =
9015 Some((parse_formfield_overrides(&s.value()), s.span()));
9016 return Ok(());
9017 }
9018 Err(inner.error(
9019 "unknown admin attribute (supported: \
9020 `list_display`, `list_display_links`, \
9021 `search_fields`, `search_help_text`, \
9022 `readonly_fields`, \
9023 `list_filter`, `list_per_page`, `ordering`, `actions`, \
9024 `actions_on_top`, `actions_on_bottom`, \
9025 `date_hierarchy`, \
9026 `prepopulated_fields`, \
9027 `raw_id_fields`, \
9028 `autocomplete_fields`, \
9029 `list_select_related`, \
9030 `formfield_overrides`, \
9031 `fieldsets`)",
9032 ))
9033 })?;
9034 out.admin = Some(admin);
9035 return Ok(());
9036 }
9037 if meta.path.is_ident("manager") {
9038 // Its empty trait could not take methods (#2132).
9039 return Err(meta.error(
9040 "`manager(ext = ...)` was removed (#2132): declare your own \
9041 `trait FooManagerExt: Sized { … }` and implement it for `QuerySet<Foo>`",
9042 ));
9043 }
9044 if meta.path.is_ident("manager_fn") {
9045 // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
9046 // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
9047 // next to the default `Self::objects()`. Multiple
9048 // attributes accumulate.
9049 let s: LitStr = meta.value()?.parse()?;
9050 let name = s.value();
9051 if name.is_empty() {
9052 return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
9053 }
9054 if name == "objects" {
9055 return Err(meta.error(
9056 "`manager_fn = \"objects\"` collides with the default \
9057 accessor — pick a different name",
9058 ));
9059 }
9060 let ident = syn::Ident::new(&name, s.span());
9061 if out.manager_fns.iter().any(|prev| *prev == ident) {
9062 return Err(meta.error(format!(
9063 "duplicate `manager_fn = \"{name}\"`"
9064 )));
9065 }
9066 out.manager_fns.push(ident);
9067 return Ok(());
9068 }
9069 if meta.path.is_ident("default_order") {
9070 // `#[rustango(default_order = "-created_at, status")]`
9071 // — issue #291 / T2.5. Comma-separated list; `-prefix`
9072 // means descending, `+prefix` or bare name means ascending.
9073 // Per-query opt-in via `QuerySet::with_default_order()`.
9074 let s: LitStr = meta.value()?.parse()?;
9075 let raw = s.value();
9076 let span = s.span();
9077 let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
9078 Vec::new();
9079 for entry in raw.split(',') {
9080 let trimmed = entry.trim();
9081 if trimmed.is_empty() {
9082 return Err(syn::Error::new(
9083 span,
9084 "`default_order = \"...\"` has an empty entry — \
9085 check for a stray comma",
9086 ));
9087 }
9088 let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
9089 (true, rest.trim().to_owned())
9090 } else if let Some(rest) = trimmed.strip_prefix('+') {
9091 (false, rest.trim().to_owned())
9092 } else {
9093 (false, trimmed.to_owned())
9094 };
9095 if name.is_empty() {
9096 return Err(syn::Error::new(
9097 span,
9098 "`default_order` entry has no column name after the prefix",
9099 ));
9100 }
9101 if parsed.iter().any(|(n, _, _)| *n == name) {
9102 return Err(syn::Error::new(
9103 span,
9104 format!("duplicate column `{name}` in `default_order`"),
9105 ));
9106 }
9107 parsed.push((name, desc, span));
9108 }
9109 if parsed.is_empty() {
9110 return Err(syn::Error::new(
9111 span,
9112 "`default_order = \"...\"` cannot be empty",
9113 ));
9114 }
9115 out.default_order = parsed;
9116 return Ok(());
9117 }
9118 if meta.path.is_ident("global_scope") {
9119 // `#[rustango(global_scope(name = "active", apply =
9120 // path::to::fn))]` — issue #820. The apply function
9121 // path resolves at macro-expand time in the consumer's
9122 // scope; the macro re-emits it verbatim into the
9123 // `ModelSchema::global_scopes` slice literal.
9124 let span = meta.path.span();
9125 let mut scope_name: Option<String> = None;
9126 let mut apply_path: Option<syn::Path> = None;
9127 meta.parse_nested_meta(|inner| {
9128 if inner.path.is_ident("name") {
9129 let s: LitStr = inner.value()?.parse()?;
9130 let raw = s.value();
9131 if raw.trim().is_empty() {
9132 return Err(syn::Error::new(
9133 s.span(),
9134 "`global_scope(name = \"...\")` must not be empty",
9135 ));
9136 }
9137 scope_name = Some(raw);
9138 return Ok(());
9139 }
9140 if inner.path.is_ident("apply") {
9141 let p: syn::Path = inner.value()?.parse()?;
9142 apply_path = Some(p);
9143 return Ok(());
9144 }
9145 Err(inner.error(
9146 "unknown `global_scope` attribute (supported: \
9147 `name`, `apply`)",
9148 ))
9149 })?;
9150 let Some(name) = scope_name else {
9151 return Err(syn::Error::new(
9152 span,
9153 "`global_scope` requires `name = \"...\"`",
9154 ));
9155 };
9156 let Some(apply) = apply_path else {
9157 return Err(syn::Error::new(
9158 span,
9159 "`global_scope` requires `apply = fn_path`",
9160 ));
9161 };
9162 if out.global_scopes.iter().any(|s| s.name == name) {
9163 return Err(syn::Error::new(
9164 span,
9165 format!(
9166 "duplicate global scope name `{name}` — \
9167 pick a unique identifier so \
9168 `QuerySet::without_global_scope(\"{name}\")` \
9169 is unambiguous"
9170 ),
9171 ));
9172 }
9173 out.global_scopes.push(GlobalScopeAttr { name, apply });
9174 return Ok(());
9175 }
9176 if meta.path.is_ident("through") {
9177 // `#[rustango(through(name = "posts", far = "Post",
9178 // far_fk_column = "author_id", intermediate = "User",
9179 // intermediate_fk_column = "country_id"
9180 // [, intermediate_pk_column = "id"]))]` — issue #817.
9181 let span = meta.path.span();
9182 let mut accessor_name: Option<String> = None;
9183 let mut far_ident: Option<syn::Ident> = None;
9184 let mut far_fk_column: Option<String> = None;
9185 let mut intermediate_ident: Option<syn::Ident> = None;
9186 let mut intermediate_fk_column: Option<String> = None;
9187 let mut intermediate_pk_column: Option<String> = None;
9188 fn parse_nonempty_string(
9189 inner: &syn::meta::ParseNestedMeta<'_>,
9190 field: &str,
9191 ) -> syn::Result<String> {
9192 let s: LitStr = inner.value()?.parse()?;
9193 let raw = s.value();
9194 let trimmed = raw.trim();
9195 if trimmed.is_empty() {
9196 return Err(syn::Error::new(
9197 s.span(),
9198 format!("`through({field} = \"...\")` must not be empty"),
9199 ));
9200 }
9201 Ok(trimmed.to_owned())
9202 }
9203 meta.parse_nested_meta(|inner| {
9204 if inner.path.is_ident("name") {
9205 accessor_name = Some(parse_nonempty_string(&inner, "name")?);
9206 return Ok(());
9207 }
9208 if inner.path.is_ident("far") {
9209 // Far model name accepted as a string literal
9210 // so the attribute fits inside the existing
9211 // `parse_nested_meta` shape; parsed back into a
9212 // `syn::Ident` so the emitted accessor can
9213 // reference the type directly.
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 "`through(far = \"...\")` must not be empty",
9221 ));
9222 }
9223 far_ident = Some(syn::Ident::new(trimmed, s.span()));
9224 return Ok(());
9225 }
9226 if inner.path.is_ident("far_fk_column") {
9227 far_fk_column =
9228 Some(parse_nonempty_string(&inner, "far_fk_column")?);
9229 return Ok(());
9230 }
9231 if inner.path.is_ident("intermediate") {
9232 let s: LitStr = inner.value()?.parse()?;
9233 let raw = s.value();
9234 let trimmed = raw.trim();
9235 if trimmed.is_empty() {
9236 return Err(syn::Error::new(
9237 s.span(),
9238 "`through(intermediate = \"...\")` must not be empty",
9239 ));
9240 }
9241 intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9242 return Ok(());
9243 }
9244 if inner.path.is_ident("intermediate_fk_column") {
9245 intermediate_fk_column =
9246 Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9247 return Ok(());
9248 }
9249 if inner.path.is_ident("intermediate_pk_column") {
9250 intermediate_pk_column =
9251 Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9252 return Ok(());
9253 }
9254 Err(inner.error(
9255 "unknown `through` attribute (supported: \
9256 `name`, `far`, `far_fk_column`, \
9257 `intermediate`, `intermediate_fk_column`, \
9258 `intermediate_pk_column`)",
9259 ))
9260 })?;
9261 let Some(name) = accessor_name else {
9262 return Err(syn::Error::new(
9263 span,
9264 "`through` requires `name = \"...\"`",
9265 ));
9266 };
9267 let Some(far) = far_ident else {
9268 return Err(syn::Error::new(
9269 span,
9270 "`through` requires `far = \"FarModelType\"`",
9271 ));
9272 };
9273 let Some(far_fk_column) = far_fk_column else {
9274 return Err(syn::Error::new(
9275 span,
9276 "`through` requires `far_fk_column = \"<column>\"`",
9277 ));
9278 };
9279 let Some(intermediate) = intermediate_ident else {
9280 return Err(syn::Error::new(
9281 span,
9282 "`through` requires `intermediate = \"IntermediateModelType\"`",
9283 ));
9284 };
9285 let Some(intermediate_fk_column) = intermediate_fk_column else {
9286 return Err(syn::Error::new(
9287 span,
9288 "`through` requires `intermediate_fk_column = \"<column>\"`",
9289 ));
9290 };
9291 let intermediate_pk_column =
9292 intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9293 if out.through_relations.iter().any(|t| t.name == name) {
9294 return Err(syn::Error::new(
9295 span,
9296 format!(
9297 "duplicate `through(name = \"{name}\")` — \
9298 pick a unique accessor name"
9299 ),
9300 ));
9301 }
9302 out.through_relations.push(ThroughAttr {
9303 name,
9304 far,
9305 far_fk_column,
9306 intermediate,
9307 intermediate_fk_column,
9308 intermediate_pk_column,
9309 });
9310 return Ok(());
9311 }
9312 if meta.path.is_ident("reverse_has") {
9313 // `#[rustango(reverse_has(name = "comments",
9314 // child = "Comment", child_fk_column = "post_id"
9315 // [, self_pk_column = "id"]))]` — issue #830.
9316 let span = meta.path.span();
9317 let mut accessor_name: Option<String> = None;
9318 let mut child_ident: Option<syn::Ident> = None;
9319 let mut child_fk_column: Option<String> = None;
9320 let mut self_pk_column: Option<String> = None;
9321 meta.parse_nested_meta(|inner| {
9322 if inner.path.is_ident("name") {
9323 let s: LitStr = inner.value()?.parse()?;
9324 let raw = s.value();
9325 if raw.trim().is_empty() {
9326 return Err(syn::Error::new(
9327 s.span(),
9328 "`reverse_has(name = \"...\")` must not be empty",
9329 ));
9330 }
9331 accessor_name = Some(raw);
9332 return Ok(());
9333 }
9334 if inner.path.is_ident("child") {
9335 let s: LitStr = inner.value()?.parse()?;
9336 let raw = s.value();
9337 let trimmed = raw.trim();
9338 if trimmed.is_empty() {
9339 return Err(syn::Error::new(
9340 s.span(),
9341 "`reverse_has(child = \"...\")` must not be empty",
9342 ));
9343 }
9344 child_ident = Some(syn::Ident::new(trimmed, s.span()));
9345 return Ok(());
9346 }
9347 if inner.path.is_ident("child_fk_column") {
9348 let s: LitStr = inner.value()?.parse()?;
9349 let raw = s.value();
9350 let trimmed = raw.trim();
9351 if trimmed.is_empty() {
9352 return Err(syn::Error::new(
9353 s.span(),
9354 "`reverse_has(child_fk_column = \"...\")` must not be empty",
9355 ));
9356 }
9357 child_fk_column = Some(trimmed.to_owned());
9358 return Ok(());
9359 }
9360 if inner.path.is_ident("self_pk_column") {
9361 let s: LitStr = inner.value()?.parse()?;
9362 let raw = s.value();
9363 let trimmed = raw.trim();
9364 if trimmed.is_empty() {
9365 return Err(syn::Error::new(
9366 s.span(),
9367 "`reverse_has(self_pk_column = \"...\")` must not be empty",
9368 ));
9369 }
9370 self_pk_column = Some(trimmed.to_owned());
9371 return Ok(());
9372 }
9373 Err(inner.error(
9374 "unknown `reverse_has` attribute (supported: \
9375 `name`, `child`, `child_fk_column`, \
9376 `self_pk_column`)",
9377 ))
9378 })?;
9379 let Some(name) = accessor_name else {
9380 return Err(syn::Error::new(
9381 span,
9382 "`reverse_has` requires `name = \"...\"`",
9383 ));
9384 };
9385 let Some(child) = child_ident else {
9386 return Err(syn::Error::new(
9387 span,
9388 "`reverse_has` requires `child = \"ChildModelType\"`",
9389 ));
9390 };
9391 let Some(child_fk_column) = child_fk_column else {
9392 return Err(syn::Error::new(
9393 span,
9394 "`reverse_has` requires `child_fk_column = \"<column>\"`",
9395 ));
9396 };
9397 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9398 if out.reverse_has_relations.iter().any(|r| r.name == name) {
9399 return Err(syn::Error::new(
9400 span,
9401 format!(
9402 "duplicate `reverse_has(name = \"{name}\")` — \
9403 pick a unique accessor name"
9404 ),
9405 ));
9406 }
9407 out.reverse_has_relations.push(ReverseHasAttr {
9408 name,
9409 child,
9410 child_fk_column,
9411 self_pk_column,
9412 });
9413 return Ok(());
9414 }
9415 if meta.path.is_ident("generic_has") {
9416 // `#[rustango(generic_has(name = "tags",
9417 // child = "Tag", ct_column = "content_type_id",
9418 // pk_column = "object_pk" [, self_pk_column = "id"]))]`
9419 // — issue #830, the reverse generic-FK (GFK) arm of the
9420 // relation-existence family.
9421 let span = meta.path.span();
9422 let mut accessor_name: Option<String> = None;
9423 let mut child_ident: Option<syn::Ident> = None;
9424 let mut ct_column: Option<String> = None;
9425 let mut pk_column: Option<String> = None;
9426 let mut self_pk_column: Option<String> = None;
9427 meta.parse_nested_meta(|inner| {
9428 if inner.path.is_ident("name") {
9429 let s: LitStr = inner.value()?.parse()?;
9430 let raw = s.value();
9431 if raw.trim().is_empty() {
9432 return Err(syn::Error::new(
9433 s.span(),
9434 "`generic_has(name = \"...\")` must not be empty",
9435 ));
9436 }
9437 accessor_name = Some(raw);
9438 return Ok(());
9439 }
9440 if inner.path.is_ident("child") {
9441 let s: LitStr = inner.value()?.parse()?;
9442 let trimmed = s.value().trim().to_owned();
9443 if trimmed.is_empty() {
9444 return Err(syn::Error::new(
9445 s.span(),
9446 "`generic_has(child = \"...\")` must not be empty",
9447 ));
9448 }
9449 child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9450 return Ok(());
9451 }
9452 if inner.path.is_ident("ct_column") {
9453 let s: LitStr = inner.value()?.parse()?;
9454 let trimmed = s.value().trim().to_owned();
9455 if trimmed.is_empty() {
9456 return Err(syn::Error::new(
9457 s.span(),
9458 "`generic_has(ct_column = \"...\")` must not be empty",
9459 ));
9460 }
9461 ct_column = Some(trimmed);
9462 return Ok(());
9463 }
9464 if inner.path.is_ident("pk_column") {
9465 let s: LitStr = inner.value()?.parse()?;
9466 let trimmed = s.value().trim().to_owned();
9467 if trimmed.is_empty() {
9468 return Err(syn::Error::new(
9469 s.span(),
9470 "`generic_has(pk_column = \"...\")` must not be empty",
9471 ));
9472 }
9473 pk_column = Some(trimmed);
9474 return Ok(());
9475 }
9476 if inner.path.is_ident("self_pk_column") {
9477 let s: LitStr = inner.value()?.parse()?;
9478 let trimmed = s.value().trim().to_owned();
9479 if trimmed.is_empty() {
9480 return Err(syn::Error::new(
9481 s.span(),
9482 "`generic_has(self_pk_column = \"...\")` must not be empty",
9483 ));
9484 }
9485 self_pk_column = Some(trimmed);
9486 return Ok(());
9487 }
9488 Err(inner.error(
9489 "unknown `generic_has` attribute (supported: \
9490 `name`, `child`, `ct_column`, `pk_column`, \
9491 `self_pk_column`)",
9492 ))
9493 })?;
9494 let Some(name) = accessor_name else {
9495 return Err(syn::Error::new(
9496 span,
9497 "`generic_has` requires `name = \"...\"`",
9498 ));
9499 };
9500 let Some(child) = child_ident else {
9501 return Err(syn::Error::new(
9502 span,
9503 "`generic_has` requires `child = \"ChildModelType\"`",
9504 ));
9505 };
9506 let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9507 let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9508 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9509 if out.generic_has_relations.iter().any(|r| r.name == name) {
9510 return Err(syn::Error::new(
9511 span,
9512 format!(
9513 "duplicate `generic_has(name = \"{name}\")` — \
9514 pick a unique accessor name"
9515 ),
9516 ));
9517 }
9518 out.generic_has_relations.push(GenericHasAttr {
9519 name,
9520 child,
9521 ct_column,
9522 pk_column,
9523 self_pk_column,
9524 });
9525 return Ok(());
9526 }
9527 if meta.path.is_ident("audit") {
9528 let mut audit = AuditAttrs::default();
9529 meta.parse_nested_meta(|inner| {
9530 if inner.path.is_ident("track") {
9531 let s: LitStr = inner.value()?.parse()?;
9532 audit.track =
9533 Some((split_field_list(&s.value()), s.span()));
9534 return Ok(());
9535 }
9536 Err(inner.error(
9537 "unknown audit attribute (supported: `track`)",
9538 ))
9539 })?;
9540 out.audit = Some(audit);
9541 return Ok(());
9542 }
9543 if meta.path.is_ident("permissions") {
9544 // Two forms accepted:
9545 // #[rustango(permissions)] — flag form, true
9546 // #[rustango(permissions = false)] — explicit opt-out
9547 // #[rustango(permissions = true)] — explicit opt-in
9548 if let Ok(v) = meta.value() {
9549 let lit: syn::LitBool = v.parse()?;
9550 out.permissions = lit.value;
9551 } else {
9552 out.permissions = true;
9553 }
9554 return Ok(());
9555 }
9556 if meta.path.is_ident("view") {
9557 // Issue #293 / T2.10. Two forms accepted, matching
9558 // the `permissions` flag pattern:
9559 // #[rustango(view)] — flag form, true
9560 // #[rustango(view = false)] — explicit opt-out
9561 // #[rustango(view = true)] — explicit opt-in
9562 if let Ok(v) = meta.value() {
9563 let lit: syn::LitBool = v.parse()?;
9564 out.is_view = lit.value;
9565 } else {
9566 out.is_view = true;
9567 }
9568 return Ok(());
9569 }
9570 if meta.path.is_ident("managed") {
9571 // Who owns the table. Issue #321.
9572 // #[rustango(managed = false)] — operator-managed table
9573 // #[rustango(managed = true)] — rustango-managed (the default)
9574 // Bare-flag form is intentionally not accepted: writing
9575 // `#[rustango(managed)]` reads as "yes please manage it"
9576 // which is already the default. The opt-out is the only
9577 // useful state, so it must be explicit.
9578 let v = meta.value()?;
9579 let lit: syn::LitBool = v.parse()?;
9580 out.managed = lit.value;
9581 return Ok(());
9582 }
9583 if meta.path.is_ident("verbose_name") {
9584 let s: LitStr = meta.value()?.parse()?;
9585 out.verbose_name = Some(s.value());
9586 return Ok(());
9587 }
9588 if meta.path.is_ident("verbose_name_plural") {
9589 let s: LitStr = meta.value()?.parse()?;
9590 out.verbose_name_plural = Some(s.value());
9591 return Ok(());
9592 }
9593 if meta.path.is_ident("db_table_comment") {
9594 // Free-form table-level comment attached to the DB
9595 // catalog.
9596 let s: LitStr = meta.value()?.parse()?;
9597 out.db_table_comment = Some(s.value());
9598 return Ok(());
9599 }
9600 if meta.path.is_ident("proxy") {
9601 // Declarative flag
9602 // marking the struct as a proxy of another model that
9603 // shares its DB table. Stored on `ModelSchema::proxy`
9604 // so future codegen can skip `CreateTable` emission
9605 // for proxies (parent owns the table) and route
9606 // per-instance method resolution to the proxy class.
9607 //
9608 // Accepts `proxy` (bare → true) and `proxy = true/false`.
9609 let value = if meta.input.peek(syn::Token![=]) {
9610 meta.value()?.parse::<syn::LitBool>()?.value
9611 } else {
9612 true
9613 };
9614 out.proxy = value;
9615 return Ok(());
9616 }
9617 if meta.path.is_ident("order_with_respect_to") {
9618 // `order_with_respect_to = "parent_fk"` — the model's
9619 // instances are intrinsically ordered relative to
9620 // their parent FK.
9621 //
9622 // rustango stores the FK field name on
9623 // `ModelSchema::order_with_respect_to`. Declarative-only
9624 // today: the migration writer + admin surfaces still
9625 // treat every model identically. Future codegen will
9626 // key off the metadata to auto-emit the `_order`
9627 // column and reorder helpers.
9628 //
9629 // Validated as a Rust-shape identifier so the macro
9630 // can reject typos at derive time.
9631 let s: LitStr = meta.value()?.parse()?;
9632 let raw = s.value();
9633 if raw.is_empty() {
9634 return Err(syn::Error::new(
9635 s.span(),
9636 "`order_with_respect_to` must be a non-empty FK field name",
9637 ));
9638 }
9639 let valid = raw
9640 .chars()
9641 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9642 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9643 if !valid {
9644 return Err(syn::Error::new(
9645 s.span(),
9646 format!(
9647 "`order_with_respect_to` must be a valid Rust \
9648 identifier (letters / digits / underscores, \
9649 not starting with a digit); got `{raw}`"
9650 ),
9651 ));
9652 }
9653 out.order_with_respect_to = Some(raw);
9654 return Ok(());
9655 }
9656 if meta.path.is_ident("required_db_features") {
9657 // Capability tokens the model needs (e.g.
9658 // `"json_extract"`, `"window_functions"`,
9659 // `"row_security"`). Comma-separated.
9660 // `manage check --deploy` walks every model and warns
9661 // when the active backend doesn't advertise the
9662 // capability.
9663 //
9664 // rustango ships a small registry of capability tokens
9665 // each dialect supports — see `Dialect::supports`.
9666 // Unknown tokens still parse (they end up on the
9667 // schema and show up in the warning) so projects can
9668 // declare aspirational capabilities and the check
9669 // verb will keep nagging until the dialect implements
9670 // them.
9671 let s: LitStr = meta.value()?.parse()?;
9672 let raw = s.value();
9673 let features: Vec<String> = raw
9674 .split(',')
9675 .map(str::trim)
9676 .filter(|s| !s.is_empty())
9677 .map(str::to_owned)
9678 .collect();
9679 if features.is_empty() {
9680 return Err(syn::Error::new(
9681 s.span(),
9682 "`required_db_features` must list at least one \
9683 comma-separated capability token",
9684 ));
9685 }
9686 out.required_db_features = features;
9687 return Ok(());
9688 }
9689 if meta.path.is_ident("required_db_vendor") {
9690 // The model is only meant to run against the named DB
9691 // backend. `manage check --deploy` flags a mismatch so
9692 // ops catches "I forgot to switch DATABASE_URL" at
9693 // deploy time rather than runtime.
9694 //
9695 // Restricted to the three backends rustango ships
9696 // dialects for so the check verb can compare reliably.
9697 let s: LitStr = meta.value()?.parse()?;
9698 let raw = s.value().to_ascii_lowercase();
9699 match raw.as_str() {
9700 "postgresql" | "postgres" | "pg" => {
9701 out.required_db_vendor = Some("postgres".to_owned());
9702 }
9703 "mysql" | "mariadb" => {
9704 out.required_db_vendor = Some("mysql".to_owned());
9705 }
9706 "sqlite" | "sqlite3" => {
9707 out.required_db_vendor = Some("sqlite".to_owned());
9708 }
9709 _ => {
9710 return Err(syn::Error::new(
9711 s.span(),
9712 format!(
9713 "unknown required_db_vendor `{raw}` — \
9714 expected `postgres` (aliases: `postgresql`, `pg`), \
9715 `mysql` (alias: `mariadb`), or `sqlite` \
9716 (alias: `sqlite3`)"
9717 ),
9718 ));
9719 }
9720 }
9721 return Ok(());
9722 }
9723 if meta.path.is_ident("base_manager_name") {
9724 // Name of the manager that `<instance>.<relation>_set`
9725 // uses when resolving reverse-relation managers.
9726 // Distinct from `default_manager_name` (what
9727 // `Self::objects()` returns). Stored on
9728 // `ModelSchema::base_manager_name`.
9729 //
9730 // Validated as a Rust identifier so it stays safe to
9731 // re-emit as code in future reverse-manager codegen.
9732 let s: LitStr = meta.value()?.parse()?;
9733 let raw = s.value();
9734 if raw.is_empty() {
9735 return Err(syn::Error::new(
9736 s.span(),
9737 "`base_manager_name` must be a non-empty string",
9738 ));
9739 }
9740 let valid = raw
9741 .chars()
9742 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9743 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9744 if !valid {
9745 return Err(syn::Error::new(
9746 s.span(),
9747 format!(
9748 "`base_manager_name` must be a valid Rust \
9749 identifier (letters / digits / underscores, \
9750 not starting with a digit); got `{raw}`"
9751 ),
9752 ));
9753 }
9754 out.base_manager_name = Some(raw);
9755 return Ok(());
9756 }
9757 if meta.path.is_ident("default_related_name") {
9758 // The name reverse-relation accessors use when callers
9759 // don't override `related_name = "..."` on the FK / M2M
9760 // field. Stored on `ModelSchema::default_related_name`
9761 // so future reverse-manager codegen / schema emit /
9762 // admin templates can pick the right accessor name
9763 // (today rustango doesn't auto-emit reverse managers;
9764 // the metadata is the foundation for that work).
9765 //
9766 // Enforced non-empty + ASCII identifier-shape so the
9767 // string is safe to use as a Rust ident later.
9768 let s: LitStr = meta.value()?.parse()?;
9769 let raw = s.value();
9770 if raw.is_empty() {
9771 return Err(syn::Error::new(
9772 s.span(),
9773 "`default_related_name` must be a non-empty string",
9774 ));
9775 }
9776 let valid = raw
9777 .chars()
9778 .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9779 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9780 if !valid {
9781 return Err(syn::Error::new(
9782 s.span(),
9783 format!(
9784 "`default_related_name` must be snake_case ASCII \
9785 (lowercase letters / digits / underscores, not \
9786 starting with a digit); got `{raw}`"
9787 ),
9788 ));
9789 }
9790 out.default_related_name = Some(raw);
9791 return Ok(());
9792 }
9793 if meta.path.is_ident("extra_permissions") {
9794 // Comma-separated `codename:label` pairs.
9795 let s: LitStr = meta.value()?.parse()?;
9796 let raw = s.value();
9797 let mut pairs = Vec::new();
9798 for entry in raw.split(',') {
9799 let entry = entry.trim();
9800 if entry.is_empty() {
9801 continue;
9802 }
9803 let (codename, label) = match entry.split_once(':') {
9804 Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9805 None => (entry.to_owned(), entry.to_owned()),
9806 };
9807 if codename.is_empty() {
9808 return Err(meta.error(
9809 "`extra_permissions` entries must be `codename:label` pairs",
9810 ));
9811 }
9812 pairs.push((codename, label));
9813 }
9814 if pairs.is_empty() {
9815 return Err(meta
9816 .error("`extra_permissions = \"…\"` must list at least one pair"));
9817 }
9818 out.extra_permissions = pairs;
9819 return Ok(());
9820 }
9821 if meta.path.is_ident("default_permissions") {
9822 // Comma-separated subset of the CRUD action set.
9823 // Empty means all four (the framework default).
9824 let s: LitStr = meta.value()?.parse()?;
9825 let raw = s.value();
9826 let mut actions: Vec<String> = Vec::new();
9827 for entry in raw.split(',') {
9828 let action = entry.trim().to_ascii_lowercase();
9829 if action.is_empty() {
9830 continue;
9831 }
9832 match action.as_str() {
9833 "add" | "change" | "delete" | "view" => {}
9834 other => {
9835 return Err(syn::Error::new(
9836 s.span(),
9837 format!(
9838 "unknown default_permissions action `{other}` — \
9839 expected one of `add`, `change`, `delete`, `view`"
9840 ),
9841 ));
9842 }
9843 }
9844 if !actions.contains(&action) {
9845 actions.push(action);
9846 }
9847 }
9848 if actions.is_empty() {
9849 return Err(syn::Error::new(
9850 s.span(),
9851 "`default_permissions = \"…\"` must list at least one action; \
9852 use `permissions = false` on the container if you want NO \
9853 permissions seeded for this model.",
9854 ));
9855 }
9856 out.default_permissions = actions;
9857 return Ok(());
9858 }
9859 if meta.path.is_ident("get_latest_by") {
9860 // The `-` prefix selects descending order.
9861 let s: LitStr = meta.value()?.parse()?;
9862 let raw = s.value();
9863 let trimmed = raw.trim();
9864 if trimmed.is_empty() {
9865 return Err(meta.error("`get_latest_by` must name a column"));
9866 }
9867 let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9868 (stripped.to_owned(), true)
9869 } else if let Some(stripped) = trimmed.strip_prefix('+') {
9870 (stripped.to_owned(), false)
9871 } else {
9872 (trimmed.to_owned(), false)
9873 };
9874 if col.is_empty() {
9875 return Err(meta.error("`get_latest_by` must name a column"));
9876 }
9877 out.get_latest_by = Some((col, desc));
9878 return Ok(());
9879 }
9880 if meta.path.is_ident("unique_together") {
9881 // Composite UNIQUE index. Two syntaxes:
9882 //
9883 // #[rustango(unique_together = "org_id, user_id")] — auto-derived name
9884 // #[rustango(unique_together(columns = "org_id, user_id", name = "x"))] — explicit name
9885 //
9886 // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9887 // (col1, col2)`, where <name> defaults to
9888 // `<table>_<col1>_<col2>_uq` when not supplied.
9889 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9890 out.indexes.push(IndexAttr {
9891 name,
9892 columns,
9893 unique: true,
9894 method: "btree".to_owned(),
9895 where_clause: None,
9896 include: Vec::new(),
9897 });
9898 return Ok(());
9899 }
9900 if meta.path.is_ident("index_together") {
9901 // Composite (non-unique) index. Two syntaxes
9902 // mirroring `unique_together`.
9903 //
9904 // #[rustango(index_together = "created_at, status")]
9905 // #[rustango(index_together(columns = "created_at, status", name = "x"))]
9906 let (columns, name) = parse_together_attr(&meta, "index_together")?;
9907 out.indexes.push(IndexAttr {
9908 name,
9909 columns,
9910 unique: false,
9911 method: "btree".to_owned(),
9912 where_clause: None,
9913 include: Vec::new(),
9914 });
9915 return Ok(());
9916 }
9917 if meta.path.is_ident("unique_when") {
9918 // Partial unique index. Issue #265 / T1.3.
9919 //
9920 // #[rustango(unique_when(
9921 // columns = "email",
9922 // condition = "deleted_at IS NULL",
9923 // name = "unique_active_email"
9924 // ))]
9925 //
9926 // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9927 // on PG / SQLite (both ship partial indexes natively).
9928 // MySQL falls back to a plain UNIQUE index — the
9929 // condition is lost; document the limitation in the
9930 // generated migration.
9931 let mut columns: Option<Vec<String>> = None;
9932 let mut condition: Option<String> = None;
9933 let mut name: Option<String> = None;
9934 let mut include: Vec<String> = Vec::new();
9935 meta.parse_nested_meta(|inner| {
9936 if inner.path.is_ident("columns") {
9937 let s: LitStr = inner.value()?.parse()?;
9938 columns = Some(split_field_list(&s.value()));
9939 return Ok(());
9940 }
9941 if inner.path.is_ident("condition") {
9942 let s: LitStr = inner.value()?.parse()?;
9943 condition = Some(s.value());
9944 return Ok(());
9945 }
9946 if inner.path.is_ident("name") {
9947 let s: LitStr = inner.value()?.parse()?;
9948 name = Some(s.value());
9949 return Ok(());
9950 }
9951 if inner.path.is_ident("include") {
9952 // PG 11+ covering-index columns. Non-key columns
9953 // travel with the index leaf for index-only
9954 // scans. Dropped on MySQL/SQLite by the writer.
9955 let s: LitStr = inner.value()?.parse()?;
9956 include = split_field_list(&s.value());
9957 return Ok(());
9958 }
9959 Err(inner.error(
9960 "unknown unique_when attribute (supported: \
9961 `columns = \"...\"`, `condition = \"...\"`, \
9962 `name = \"...\"`, `include = \"...\"`)",
9963 ))
9964 })?;
9965 let columns = columns.ok_or_else(|| {
9966 meta.error("`unique_when(...)` requires `columns = \"...\"`")
9967 })?;
9968 let condition = condition.ok_or_else(|| {
9969 meta.error("`unique_when(...)` requires `condition = \"...\"`")
9970 })?;
9971 if columns.is_empty() {
9972 return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9973 }
9974 out.indexes.push(IndexAttr {
9975 name,
9976 columns,
9977 unique: true,
9978 method: "btree".to_owned(),
9979 where_clause: Some(condition),
9980 include,
9981 });
9982 return Ok(());
9983 }
9984 if meta.path.is_ident("index_when") {
9985 // Non-unique partial index. Sibling of `unique_when`
9986 // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9987 //
9988 // #[rustango(index_when(
9989 // columns = "status, created_at",
9990 // condition = "deleted_at IS NULL",
9991 // name = "active_status_created_idx"
9992 // ))]
9993 //
9994 // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9995 // on PG / SQLite (both ship partial indexes natively).
9996 // MySQL has no native partial-index support — the writer
9997 // emits a plain CREATE INDEX and the condition is lost;
9998 // operators wanting that selectivity on MySQL should
9999 // declare a covering index plus an application-level
10000 // filter.
10001 let mut columns: Option<Vec<String>> = None;
10002 let mut condition: Option<String> = None;
10003 let mut name: Option<String> = None;
10004 let mut method: String = "btree".to_owned();
10005 let mut include: Vec<String> = Vec::new();
10006 meta.parse_nested_meta(|inner| {
10007 if inner.path.is_ident("columns") {
10008 let s: LitStr = inner.value()?.parse()?;
10009 columns = Some(split_field_list(&s.value()));
10010 return Ok(());
10011 }
10012 if inner.path.is_ident("condition") {
10013 let s: LitStr = inner.value()?.parse()?;
10014 condition = Some(s.value());
10015 return Ok(());
10016 }
10017 if inner.path.is_ident("name") {
10018 let s: LitStr = inner.value()?.parse()?;
10019 name = Some(s.value());
10020 return Ok(());
10021 }
10022 if inner.path.is_ident("method") {
10023 let s: LitStr = inner.value()?.parse()?;
10024 method = s.value();
10025 return Ok(());
10026 }
10027 if inner.path.is_ident("include") {
10028 // PG 11+ covering-index columns; non-key
10029 // columns travel with the index leaf. Dropped
10030 // on MySQL/SQLite.
10031 let s: LitStr = inner.value()?.parse()?;
10032 include = split_field_list(&s.value());
10033 return Ok(());
10034 }
10035 Err(inner.error(
10036 "unknown index_when attribute (supported: \
10037 `columns = \"...\"`, `condition = \"...\"`, \
10038 `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
10039 `include = \"...\"`)",
10040 ))
10041 })?;
10042 let columns = columns
10043 .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
10044 let condition = condition.ok_or_else(|| {
10045 meta.error("`index_when(...)` requires `condition = \"...\"`")
10046 })?;
10047 if columns.is_empty() {
10048 return Err(meta.error("`index_when(columns = \"\")` is empty"));
10049 }
10050 out.indexes.push(IndexAttr {
10051 name,
10052 columns,
10053 unique: false,
10054 method,
10055 where_clause: Some(condition),
10056 include,
10057 });
10058 return Ok(());
10059 }
10060 if meta.path.is_ident("index") {
10061 // Container-level composite index. Two syntaxes:
10062 // #[rustango(index = "col1, col2")] — bare, non-unique btree
10063 // #[rustango(index("col1, col2"))] — call form (same result)
10064 // #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
10065 // The call form takes the column list as a leading string
10066 // literal, then optional `unique` / `name = "..."` /
10067 // `method = "..."` flags (a leading literal can't compose
10068 // under `parse_nested_meta`, so the paren body is parsed by
10069 // hand). `unique_together` / `index_together` remain
10070 // aliases for the same feature.
10071 let cols_lit: LitStr;
10072 let mut unique = false;
10073 let mut name: Option<String> = None;
10074 let mut method = "btree".to_owned();
10075 if meta.input.peek(syn::token::Paren) {
10076 let content;
10077 syn::parenthesized!(content in meta.input);
10078 cols_lit = content.parse()?;
10079 while content.peek(syn::Token![,]) {
10080 content.parse::<syn::Token![,]>()?;
10081 if content.is_empty() {
10082 break;
10083 }
10084 let flag: syn::Ident = content.parse()?;
10085 if flag == "unique" {
10086 unique = true;
10087 } else if flag == "name" {
10088 content.parse::<syn::Token![=]>()?;
10089 let s: LitStr = content.parse()?;
10090 name = Some(s.value());
10091 } else if flag == "method" {
10092 content.parse::<syn::Token![=]>()?;
10093 let s: LitStr = content.parse()?;
10094 let v = s.value();
10095 match v.as_str() {
10096 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
10097 | "bloom" => method = v,
10098 other => {
10099 return Err(syn::Error::new(
10100 s.span(),
10101 format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
10102 ));
10103 }
10104 }
10105 } else {
10106 return Err(syn::Error::new(
10107 flag.span(),
10108 "unknown index flag (supported: `unique`, `name`, `method`)",
10109 ));
10110 }
10111 }
10112 } else {
10113 cols_lit = meta.value()?.parse()?;
10114 }
10115 let columns = split_field_list(&cols_lit.value());
10116 out.indexes.push(IndexAttr {
10117 name,
10118 columns,
10119 unique,
10120 method,
10121 where_clause: None,
10122 include: Vec::new(),
10123 });
10124 return Ok(());
10125 }
10126 if meta.path.is_ident("check") {
10127 // #[rustango(check(name = "…", expr = "…"))]
10128 let mut name: Option<String> = None;
10129 let mut expr: Option<String> = None;
10130 meta.parse_nested_meta(|inner| {
10131 if inner.path.is_ident("name") {
10132 let s: LitStr = inner.value()?.parse()?;
10133 name = Some(s.value());
10134 return Ok(());
10135 }
10136 if inner.path.is_ident("expr") {
10137 let s: LitStr = inner.value()?.parse()?;
10138 expr = Some(s.value());
10139 return Ok(());
10140 }
10141 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
10142 })?;
10143 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
10144 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
10145 out.checks.push(CheckAttr { name, expr });
10146 return Ok(());
10147 }
10148 if meta.path.is_ident("exclude") {
10149 // #[rustango(exclude(name = "…", using = "gist",
10150 // elements = "col WITH op, col WITH op",
10151 // where = "…"))]
10152 let mut name: Option<String> = None;
10153 let mut using: Option<String> = None;
10154 let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
10155 let mut where_clause: Option<String> = None;
10156 meta.parse_nested_meta(|inner| {
10157 if inner.path.is_ident("name") {
10158 let s: LitStr = inner.value()?.parse()?;
10159 name = Some(s.value());
10160 return Ok(());
10161 }
10162 if inner.path.is_ident("using") {
10163 let s: LitStr = inner.value()?.parse()?;
10164 using = Some(s.value());
10165 return Ok(());
10166 }
10167 if inner.path.is_ident("elements") {
10168 let s: LitStr = inner.value()?.parse()?;
10169 elements_raw = Some((s.value(), s.span()));
10170 return Ok(());
10171 }
10172 if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
10173 let s: LitStr = inner.value()?.parse()?;
10174 where_clause = Some(s.value());
10175 return Ok(());
10176 }
10177 Err(inner.error(
10178 "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
10179 ))
10180 })?;
10181 let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
10182 let using = using.unwrap_or_else(|| "gist".to_owned());
10183 let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
10184 meta.error(
10185 "exclude requires `elements = \"col WITH op, col WITH op\"`",
10186 )
10187 })?;
10188 // Parse `col WITH op` pairs separated by commas.
10189 let mut elements: Vec<(String, String)> = Vec::new();
10190 for pair in elements_str.split(',') {
10191 let pair = pair.trim();
10192 if pair.is_empty() {
10193 continue;
10194 }
10195 let mut split = pair.splitn(2, |c: char| c.is_whitespace());
10196 let col = split.next().unwrap_or("").trim();
10197 let rest = split.next().unwrap_or("").trim();
10198 // `WITH op` — case-insensitive on `WITH`, then op.
10199 let rest_lc = rest.to_ascii_lowercase();
10200 let op = rest_lc
10201 .strip_prefix("with")
10202 .map(|r| r.trim_start())
10203 .filter(|r| !r.is_empty())
10204 .map(|_| {
10205 // Pull the original-case op from `rest` after the
10206 // `WITH ` token (5 chars).
10207 rest[4..].trim_start().to_owned()
10208 });
10209 let Some(op) = op else {
10210 return Err(syn::Error::new(
10211 elements_span,
10212 format!(
10213 "exclude elements: `{pair}` must be `<col> WITH <op>` \
10214 (e.g. `room_id WITH =` or `during WITH &&`)"
10215 ),
10216 ));
10217 };
10218 if col.is_empty() || op.is_empty() {
10219 return Err(syn::Error::new(
10220 elements_span,
10221 format!(
10222 "exclude elements: `{pair}` must be `<col> WITH <op>` \
10223 (both sides non-empty)"
10224 ),
10225 ));
10226 }
10227 elements.push((col.to_owned(), op));
10228 }
10229 if elements.is_empty() {
10230 return Err(syn::Error::new(
10231 elements_span,
10232 "exclude requires at least one `col WITH op` element",
10233 ));
10234 }
10235 out.excludes.push(ExcludeAttr {
10236 name,
10237 using,
10238 elements,
10239 where_clause,
10240 });
10241 return Ok(());
10242 }
10243 if meta.path.is_ident("generic_fk") {
10244 let mut gfk = GenericFkAttr {
10245 name: String::new(),
10246 ct_column: String::new(),
10247 pk_column: String::new(),
10248 };
10249 meta.parse_nested_meta(|inner| {
10250 if inner.path.is_ident("name") {
10251 let s: LitStr = inner.value()?.parse()?;
10252 gfk.name = s.value();
10253 return Ok(());
10254 }
10255 if inner.path.is_ident("ct_column") {
10256 let s: LitStr = inner.value()?.parse()?;
10257 gfk.ct_column = s.value();
10258 return Ok(());
10259 }
10260 if inner.path.is_ident("pk_column") {
10261 let s: LitStr = inner.value()?.parse()?;
10262 gfk.pk_column = s.value();
10263 return Ok(());
10264 }
10265 Err(inner.error(
10266 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10267 ))
10268 })?;
10269 if gfk.name.is_empty() {
10270 return Err(meta.error("generic_fk requires `name = \"...\"`"));
10271 }
10272 if gfk.ct_column.is_empty() {
10273 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10274 }
10275 if gfk.pk_column.is_empty() {
10276 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10277 }
10278 out.generic_fks.push(gfk);
10279 return Ok(());
10280 }
10281 if meta.path.is_ident("fk_composite") {
10282 let mut fk = CompositeFkAttr {
10283 name: String::new(),
10284 to: String::new(),
10285 from: Vec::new(),
10286 on: Vec::new(),
10287 };
10288 meta.parse_nested_meta(|inner| {
10289 if inner.path.is_ident("name") {
10290 let s: LitStr = inner.value()?.parse()?;
10291 fk.name = s.value();
10292 return Ok(());
10293 }
10294 if inner.path.is_ident("to") {
10295 let s: LitStr = inner.value()?.parse()?;
10296 fk.to = s.value();
10297 return Ok(());
10298 }
10299 // `on = ("col1", "col2", ...)` — parse a parenthesised
10300 // comma-list of string literals.
10301 if inner.path.is_ident("on") || inner.path.is_ident("from") {
10302 let value = inner.value()?;
10303 let content;
10304 syn::parenthesized!(content in value);
10305 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10306 content.parse_terminated(
10307 |p| p.parse::<syn::LitStr>(),
10308 syn::Token![,],
10309 )?;
10310 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10311 if inner.path.is_ident("on") {
10312 fk.on = cols;
10313 } else {
10314 fk.from = cols;
10315 }
10316 return Ok(());
10317 }
10318 Err(inner.error(
10319 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10320 ))
10321 })?;
10322 if fk.name.is_empty() {
10323 return Err(meta.error("fk_composite requires `name = \"...\"`"));
10324 }
10325 if fk.to.is_empty() {
10326 return Err(meta.error("fk_composite requires `to = \"...\"`"));
10327 }
10328 if fk.from.is_empty() || fk.on.is_empty() {
10329 return Err(meta.error(
10330 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10331 ));
10332 }
10333 if fk.from.len() != fk.on.len() {
10334 return Err(meta.error(format!(
10335 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10336 fk.from.len(),
10337 fk.on.len(),
10338 )));
10339 }
10340 out.composite_fks.push(fk);
10341 return Ok(());
10342 }
10343 if meta.path.is_ident("m2m") {
10344 let mut m2m = M2MAttr {
10345 name: String::new(),
10346 to: String::new(),
10347 through: String::new(),
10348 src: String::new(),
10349 dst: String::new(),
10350 auto_create: true,
10351 };
10352 meta.parse_nested_meta(|inner| {
10353 if inner.path.is_ident("name") {
10354 let s: LitStr = inner.value()?.parse()?;
10355 m2m.name = s.value();
10356 return Ok(());
10357 }
10358 if inner.path.is_ident("to") {
10359 let s: LitStr = inner.value()?.parse()?;
10360 m2m.to = s.value();
10361 return Ok(());
10362 }
10363 if inner.path.is_ident("through") {
10364 let s: LitStr = inner.value()?.parse()?;
10365 m2m.through = s.value();
10366 return Ok(());
10367 }
10368 if inner.path.is_ident("src") {
10369 let s: LitStr = inner.value()?.parse()?;
10370 m2m.src = s.value();
10371 return Ok(());
10372 }
10373 if inner.path.is_ident("dst") {
10374 let s: LitStr = inner.value()?.parse()?;
10375 m2m.dst = s.value();
10376 return Ok(());
10377 }
10378 if inner.path.is_ident("auto_create") {
10379 let lit: syn::LitBool = inner.value()?.parse()?;
10380 m2m.auto_create = lit.value;
10381 return Ok(());
10382 }
10383 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10384 })?;
10385 if m2m.name.is_empty() {
10386 return Err(meta.error("m2m requires `name = \"...\"`"));
10387 }
10388 if m2m.to.is_empty() {
10389 return Err(meta.error("m2m requires `to = \"...\"`"));
10390 }
10391 if m2m.through.is_empty() {
10392 return Err(meta.error("m2m requires `through = \"...\"`"));
10393 }
10394 if m2m.src.is_empty() {
10395 return Err(meta.error("m2m requires `src = \"...\"`"));
10396 }
10397 if m2m.dst.is_empty() {
10398 return Err(meta.error("m2m requires `dst = \"...\"`"));
10399 }
10400 out.m2m.push(m2m);
10401 return Ok(());
10402 }
10403 if meta.path.is_ident("generic_m2m") {
10404 let mut gm = GenericM2MAttr {
10405 name: String::new(),
10406 through: String::new(),
10407 pk_column: String::new(),
10408 ct_column: String::new(),
10409 related_column: String::new(),
10410 };
10411 meta.parse_nested_meta(|inner| {
10412 let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10413 let s: LitStr = inner.value()?.parse()?;
10414 Ok(s.value())
10415 };
10416 if inner.path.is_ident("name") {
10417 gm.name = field(&inner)?;
10418 return Ok(());
10419 }
10420 if inner.path.is_ident("through") {
10421 gm.through = field(&inner)?;
10422 return Ok(());
10423 }
10424 if inner.path.is_ident("pk_column") {
10425 gm.pk_column = field(&inner)?;
10426 return Ok(());
10427 }
10428 if inner.path.is_ident("ct_column") {
10429 gm.ct_column = field(&inner)?;
10430 return Ok(());
10431 }
10432 if inner.path.is_ident("related_column") {
10433 gm.related_column = field(&inner)?;
10434 return Ok(());
10435 }
10436 Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10437 })?;
10438 for (val, label) in [
10439 (&gm.name, "name"),
10440 (&gm.through, "through"),
10441 (&gm.pk_column, "pk_column"),
10442 (&gm.ct_column, "ct_column"),
10443 (&gm.related_column, "related_column"),
10444 ] {
10445 if val.is_empty() {
10446 return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10447 }
10448 }
10449 out.generic_m2m.push(gm);
10450 return Ok(());
10451 }
10452 Err(meta.error("unknown rustango container attribute"))
10453 })?;
10454 }
10455 Ok(out)
10456}
10457
10458/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10459/// owned field names, trimming whitespace and skipping empty entries.
10460/// Field-name validation against the model is done by the caller.
10461fn split_field_list(raw: &str) -> Vec<String> {
10462 raw.split(',')
10463 .map(str::trim)
10464 .filter(|s| !s.is_empty())
10465 .map(str::to_owned)
10466 .collect()
10467}
10468
10469/// Shared parser for `unique_together` and `index_together` container
10470/// attrs. Accepts both shapes:
10471///
10472/// * `attr = "col1, col2"` — auto-derived index name.
10473/// * `attr(columns = "col1, col2", name = "...")` — explicit name.
10474///
10475/// Returns `(columns, name)`.
10476fn parse_together_attr(
10477 meta: &syn::meta::ParseNestedMeta<'_>,
10478 attr: &str,
10479) -> syn::Result<(Vec<String>, Option<String>)> {
10480 // Disambiguate by whether the next token is `=` (key-value) or
10481 // `(` (parenthesized).
10482 if meta.input.peek(syn::Token![=]) {
10483 let cols_lit: LitStr = meta.value()?.parse()?;
10484 let columns = split_field_list(&cols_lit.value());
10485 check_together_columns(meta, attr, &columns)?;
10486 return Ok((columns, None));
10487 }
10488 let mut columns: Option<Vec<String>> = None;
10489 let mut name: Option<String> = None;
10490 meta.parse_nested_meta(|inner| {
10491 if inner.path.is_ident("columns") {
10492 let s: LitStr = inner.value()?.parse()?;
10493 columns = Some(split_field_list(&s.value()));
10494 return Ok(());
10495 }
10496 if inner.path.is_ident("name") {
10497 let s: LitStr = inner.value()?.parse()?;
10498 name = Some(s.value());
10499 return Ok(());
10500 }
10501 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10502 })?;
10503 let columns = columns.ok_or_else(|| {
10504 meta.error(format!(
10505 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10506 ))
10507 })?;
10508 check_together_columns(meta, attr, &columns)?;
10509 Ok((columns, name))
10510}
10511
10512fn check_together_columns(
10513 meta: &syn::meta::ParseNestedMeta<'_>,
10514 attr: &str,
10515 columns: &[String],
10516) -> syn::Result<()> {
10517 if columns.len() < 2 {
10518 let single = if attr == "unique_together" {
10519 "#[rustango(unique)] on the field"
10520 } else {
10521 "#[rustango(index)] on the field"
10522 };
10523 return Err(meta.error(format!(
10524 "{attr} expects two or more columns; for a single-column equivalent use {single}",
10525 )));
10526 }
10527 Ok(())
10528}
10529
10530/// Parse the fieldsets DSL: pipe-separated sections, optional
10531/// `"Title:"` prefix on each, comma-separated field names after.
10532/// Examples:
10533/// * `"name, office"` → one untitled section with two fields
10534/// * `"Identity: name, office | Metadata: created_at"` → two titled
10535/// sections
10536///
10537/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10538fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10539 raw.split('|')
10540 .map(str::trim)
10541 .filter(|s| !s.is_empty())
10542 .map(|section| {
10543 // Split off an optional `Title:` prefix (first colon).
10544 let (title, rest) = match section.split_once(':') {
10545 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10546 _ => (String::new(), section),
10547 };
10548 let fields = split_field_list(rest);
10549 (title, fields)
10550 })
10551 .collect()
10552}
10553
10554/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10555/// comma-separated entry maps a target field to one or more source
10556/// fields joined with `+`. Whitespace around tokens is trimmed.
10557/// Entries missing `:` or with empty target/source lists are dropped.
10558fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10559 raw.split(',')
10560 .map(str::trim)
10561 .filter(|s| !s.is_empty())
10562 .filter_map(|entry| {
10563 let (target, sources_raw) = entry.split_once(':')?;
10564 let target = target.trim().to_owned();
10565 if target.is_empty() {
10566 return None;
10567 }
10568 let sources: Vec<String> = sources_raw
10569 .split('+')
10570 .map(|s| s.trim().to_owned())
10571 .filter(|s| !s.is_empty())
10572 .collect();
10573 if sources.is_empty() {
10574 return None;
10575 }
10576 Some((target, sources))
10577 })
10578 .collect()
10579}
10580
10581/// Parse `formfield_overrides` — `"field:widget,field2:widget2"`
10582/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10583/// and empty halves drop silently — the macro layer only enforces shape,
10584/// not field-name vs. widget-name validity (those checks happen at
10585/// `AdminConfig` consumption time). Issue #359.
10586fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10587 raw.split(',')
10588 .map(str::trim)
10589 .filter(|s| !s.is_empty())
10590 .filter_map(|entry| {
10591 let (field, widget) = entry.split_once(':')?;
10592 let field = field.trim().to_owned();
10593 let widget = widget.trim().to_owned();
10594 if field.is_empty() || widget.is_empty() {
10595 return None;
10596 }
10597 Some((field, widget))
10598 })
10599 .collect()
10600}
10601
10602/// Parse an ordering list — `"name"` is ASC, `"-name"` is DESC.
10603/// Returns `(field_name, desc)` pairs in the same order as the input.
10604fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10605 raw.split(',')
10606 .map(str::trim)
10607 .filter(|s| !s.is_empty())
10608 .map(|spec| {
10609 spec.strip_prefix('-')
10610 .map_or((spec.to_owned(), false), |rest| {
10611 (rest.trim().to_owned(), true)
10612 })
10613 })
10614 .collect()
10615}
10616
10617struct FieldAttrs {
10618 column: Option<String>,
10619 primary_key: bool,
10620 fk: Option<String>,
10621 o2o: Option<String>,
10622 on: Option<String>,
10623 /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10624 /// "set_default" | "no_action")]` — what happens to this row when
10625 /// the row it points at is deleted.
10626 /// Only meaningful on a `ForeignKey<T>` field or with `fk` / `o2o`
10627 /// set; the macro errors at compile time if applied to a
10628 /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10629 /// DDL writer renders `ON DELETE <action>` after the constraint
10630 /// clause when this is `Some`; `None` falls back to the database
10631 /// default (NO ACTION on every backend rustango supports).
10632 on_delete: Option<String>,
10633 /// `#[rustango(related_name = "...")]` — per-FK
10634 /// reverse-accessor override. When set, the derive emits
10635 /// `Parent::<related_name>[_pool]` instead of the container-level
10636 /// `default_related_name` or the `<child_snake>_set[_pool]`
10637 /// fallback. Only meaningful when `fk` / `o2o` is also set;
10638 /// silently ignored on non-FK fields. Follow-up to #816.
10639 /// `"+"` emits none, for a parent defined in another crate.
10640 related_name: Option<String>,
10641 max_length: Option<u32>,
10642 /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10643 /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10644 /// unconstrained `vector` column.
10645 vector_dims: Option<u32>,
10646 /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10647 /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10648 /// unconstrained `geometry(Point)` column (SRID 0).
10649 geometry_srid: Option<u32>,
10650 min: Option<i64>,
10651 max: Option<i64>,
10652 default: Option<String>,
10653 /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10654 /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10655 /// "gen_random_uuid()"`. The Rust field type must be
10656 /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10657 /// INSERTs so the DB DEFAULT fires.
10658 auto_uuid: bool,
10659 /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10660 /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10661 /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10662 /// the field is `Auto::Unset`, then bound as a normal parameter
10663 /// rather than relying on a per-dialect DB function. Issue #823
10664 /// (Eloquent `HasUuids`).
10665 ///
10666 /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10667 /// Composes with every backend (PG / MySQL / SQLite) — no
10668 /// `gen_random_uuid()` requirement on the database.
10669 default_uuid_v7: bool,
10670 /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10671 /// Server-set on insert, immutable from app code afterwards.
10672 /// Implies `auto + default = "now()"`. Field type must be
10673 /// `DateTime<Utc>`.
10674 auto_now_add: bool,
10675 /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10676 /// every insert AND every update. Implies `auto + default =
10677 /// "now()"`; the macro additionally rewrites `update_on` /
10678 /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10679 /// field value.
10680 auto_now: bool,
10681 /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10682 /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10683 /// `soft_delete_on(executor)` and `restore_on(executor)`
10684 /// methods on the model.
10685 soft_delete: bool,
10686 /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10687 /// the column in the generated DDL.
10688 unique: bool,
10689 /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10690 /// generates a `CREATE INDEX` for this column. `unique` here means
10691 /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10692 index: bool,
10693 index_unique: bool,
10694 index_name: Option<String>,
10695 /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10696 /// `"btree"`. Issue #34.
10697 index_method: String,
10698 /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10699 /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10700 /// the macro skips this column from every INSERT and UPDATE
10701 /// path, so the database always recomputes the value from
10702 /// `EXPR`. Backlog item #35.
10703 generated_as: Option<String>,
10704 /// `#[rustango(help_text = "…")]` — help text
10705 /// rendered below the admin form's input. Threaded into
10706 /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10707 /// layers can read it.
10708 help_text: Option<String>,
10709 /// `#[rustango(choices = "value:Label, value:Label")]` — the
10710 /// enumerated allowed values. Threaded into `FieldSchema::choices`
10711 /// as a `&'static [(&'static str, &'static str)]` slice. When
10712 /// present, the admin form renders a `<select>` instead of `<input>`
10713 /// and the validator rejects values not in the list. Only meaningful
10714 /// for `FieldType::String`; the macro errors at compile time if
10715 /// applied to a non-string field.
10716 choices: Option<Vec<(String, String)>>,
10717 /// `#[rustango(db_comment = "…")]` — DB-side column
10718 /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10719 /// the comment in CREATE TABLE; Postgres emits a separate
10720 /// `COMMENT ON COLUMN` statement after the table is created;
10721 /// SQLite silently drops the value (no native column comments).
10722 db_comment: Option<String>,
10723 /// `#[rustango(verbose_name = "…")]` — human-readable
10724 /// label for the field. Threaded into `FieldSchema::verbose_name`
10725 /// so admin column headers, form labels, and other display
10726 /// surfaces can prefer the friendly caption over the Rust
10727 /// identifier. `None` means renderers fall back to the field name.
10728 verbose_name: Option<String>,
10729 /// `#[rustango(editable = false)]` — opt out of
10730 /// auto-generated form rendering. Defaults to `true` so existing
10731 /// fields keep their current admin / form behavior; setting
10732 /// `false` removes the field from the admin change-form entirely
10733 /// (the value is still visible on detail / list views, just not
10734 /// editable).
10735 editable: bool,
10736 /// `#[rustango(blank)]` / `#[rustango(blank = true)]` —
10737 /// "form may submit empty even when DB is NOT NULL". Threaded into
10738 /// `FieldSchema::blank`. Defaults to `false`.
10739 blank: bool,
10740 /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10741 /// case-insensitive text column. Threaded into
10742 /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10743 /// fields; the macro errors at derive time if applied elsewhere.
10744 case_insensitive: bool,
10745 /// `#[rustango(validators = "email,url")]` — the
10746 /// model-level validator chain. Comma-separated names that
10747 /// dispatch to the `validators::*` family in `validate_value`.
10748 /// Empty by default; fires on every typed INSERT/UPDATE.
10749 validators: Vec<String>,
10750}
10751
10752fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10753 let mut out = FieldAttrs {
10754 column: None,
10755 primary_key: false,
10756 fk: None,
10757 o2o: None,
10758 on: None,
10759 on_delete: None,
10760 related_name: None,
10761 max_length: None,
10762 vector_dims: None,
10763 geometry_srid: None,
10764 min: None,
10765 max: None,
10766 default: None,
10767 auto_uuid: false,
10768 default_uuid_v7: false,
10769 auto_now_add: false,
10770 auto_now: false,
10771 soft_delete: false,
10772 unique: false,
10773 index: false,
10774 index_unique: false,
10775 index_name: None,
10776 index_method: "btree".to_owned(),
10777 generated_as: None,
10778 help_text: None,
10779 choices: None,
10780 db_comment: None,
10781 verbose_name: None,
10782 editable: true,
10783 blank: false,
10784 case_insensitive: false,
10785 validators: Vec::new(),
10786 };
10787 for attr in &field.attrs {
10788 if !attr.path().is_ident("rustango") {
10789 continue;
10790 }
10791 attr.parse_nested_meta(|meta| {
10792 if meta.path.is_ident("column") {
10793 let s: LitStr = meta.value()?.parse()?;
10794 let name = s.value();
10795 validate_sql_identifier(&name, "column", s.span())?;
10796 out.column = Some(name);
10797 return Ok(());
10798 }
10799 if meta.path.is_ident("primary_key") {
10800 out.primary_key = true;
10801 return Ok(());
10802 }
10803 if meta.path.is_ident("fk") {
10804 let s: LitStr = meta.value()?.parse()?;
10805 out.fk = Some(s.value());
10806 return Ok(());
10807 }
10808 if meta.path.is_ident("o2o") {
10809 let s: LitStr = meta.value()?.parse()?;
10810 out.o2o = Some(s.value());
10811 return Ok(());
10812 }
10813 if meta.path.is_ident("on") {
10814 let s: LitStr = meta.value()?.parse()?;
10815 out.on = Some(s.value());
10816 return Ok(());
10817 }
10818 if meta.path.is_ident("on_delete") {
10819 let s: LitStr = meta.value()?.parse()?;
10820 let raw = s.value();
10821 let normalized = raw.trim().to_ascii_lowercase();
10822 // Validate at parse time so the user gets a clear span
10823 // rather than a downstream compile error in the emit.
10824 match normalized.as_str() {
10825 "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10826 _ => {
10827 return Err(syn::Error::new(
10828 s.span(),
10829 format!(
10830 "unknown on_delete action `{raw}`; expected one of \
10831 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10832 ),
10833 ));
10834 }
10835 }
10836 out.on_delete = Some(normalized);
10837 return Ok(());
10838 }
10839 if meta.path.is_ident("related_name") {
10840 let s: LitStr = meta.value()?.parse()?;
10841 let raw = s.value();
10842 if raw.trim().is_empty() {
10843 return Err(syn::Error::new(
10844 s.span(),
10845 "`related_name` must be a non-empty identifier",
10846 ));
10847 }
10848 // Validate as a Rust-ident shape — the value becomes
10849 // a method name on the parent type. Same rule as
10850 // `default_related_name` so the two surfaces match.
10851 // `"+"` emits no accessor at all.
10852 let snake = raw
10853 .chars()
10854 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10855 && !raw.starts_with(char::is_numeric);
10856 if !snake && raw != NO_REVERSE_ACCESSOR {
10857 return Err(syn::Error::new(
10858 s.span(),
10859 "`related_name` must be snake_case ASCII (lowercase letters, \
10860 digits, underscores; no leading digit), or \"+\" for none",
10861 ));
10862 }
10863 out.related_name = Some(raw);
10864 return Ok(());
10865 }
10866 if meta.path.is_ident("max_length") {
10867 let lit: syn::LitInt = meta.value()?.parse()?;
10868 out.max_length = Some(lit.base10_parse::<u32>()?);
10869 return Ok(());
10870 }
10871 // `#[rustango(vector(dims = N))]` — pgvector column
10872 // dimension (#824). Nested-meta form so it reads like the
10873 // other typed-column attrs.
10874 if meta.path.is_ident("vector") {
10875 meta.parse_nested_meta(|inner| {
10876 if inner.path.is_ident("dims") {
10877 let lit: syn::LitInt = inner.value()?.parse()?;
10878 out.vector_dims = Some(lit.base10_parse::<u32>()?);
10879 return Ok(());
10880 }
10881 Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10882 })?;
10883 return Ok(());
10884 }
10885 // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10886 // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10887 if meta.path.is_ident("geometry") {
10888 meta.parse_nested_meta(|inner| {
10889 if inner.path.is_ident("srid") {
10890 let lit: syn::LitInt = inner.value()?.parse()?;
10891 out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10892 return Ok(());
10893 }
10894 Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10895 })?;
10896 return Ok(());
10897 }
10898 if meta.path.is_ident("min") {
10899 out.min = Some(parse_signed_i64(&meta)?);
10900 return Ok(());
10901 }
10902 if meta.path.is_ident("max") {
10903 out.max = Some(parse_signed_i64(&meta)?);
10904 return Ok(());
10905 }
10906 if meta.path.is_ident("default") {
10907 let s: LitStr = meta.value()?.parse()?;
10908 out.default = Some(s.value());
10909 return Ok(());
10910 }
10911 if meta.path.is_ident("generated_as") {
10912 let s: LitStr = meta.value()?.parse()?;
10913 out.generated_as = Some(s.value());
10914 return Ok(());
10915 }
10916 if meta.path.is_ident("help_text") {
10917 let s: LitStr = meta.value()?.parse()?;
10918 out.help_text = Some(s.value());
10919 return Ok(());
10920 }
10921 if meta.path.is_ident("choices") {
10922 let s: LitStr = meta.value()?.parse()?;
10923 let raw = s.value();
10924 let mut pairs: Vec<(String, String)> = Vec::new();
10925 for chunk in raw.split(',') {
10926 let chunk = chunk.trim();
10927 if chunk.is_empty() {
10928 continue;
10929 }
10930 let (value, label) = match chunk.split_once(':') {
10931 Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10932 None => (chunk.to_owned(), chunk.to_owned()),
10933 };
10934 if value.is_empty() {
10935 return Err(syn::Error::new(
10936 s.span(),
10937 "`choices` entry has empty value before `:`",
10938 ));
10939 }
10940 pairs.push((value, label));
10941 }
10942 if pairs.is_empty() {
10943 return Err(syn::Error::new(
10944 s.span(),
10945 "`choices = \"…\"` must contain at least one value",
10946 ));
10947 }
10948 out.choices = Some(pairs);
10949 return Ok(());
10950 }
10951 if meta.path.is_ident("db_comment") {
10952 let s: LitStr = meta.value()?.parse()?;
10953 out.db_comment = Some(s.value());
10954 return Ok(());
10955 }
10956 if meta.path.is_ident("verbose_name") {
10957 let s: LitStr = meta.value()?.parse()?;
10958 out.verbose_name = Some(s.value());
10959 return Ok(());
10960 }
10961 if meta.path.is_ident("editable") {
10962 // Two forms accepted:
10963 // #[rustango(editable = false)] / true — explicit
10964 // #[rustango(editable)] — flag form (= true, the
10965 // default, so harmless; included for symmetry)
10966 if let Ok(v) = meta.value() {
10967 let lit: syn::LitBool = v.parse()?;
10968 out.editable = lit.value;
10969 } else {
10970 out.editable = true;
10971 }
10972 return Ok(());
10973 }
10974 if meta.path.is_ident("blank") {
10975 // Two forms accepted:
10976 // #[rustango(blank)] — flag form, true
10977 // #[rustango(blank = false)] / true — explicit
10978 if let Ok(v) = meta.value() {
10979 let lit: syn::LitBool = v.parse()?;
10980 out.blank = lit.value;
10981 } else {
10982 out.blank = true;
10983 }
10984 return Ok(());
10985 }
10986 if meta.path.is_ident("citext") {
10987 // Case-insensitive text column (#344). Two forms:
10988 // #[rustango(citext)] — flag form, true
10989 // #[rustango(citext = true)] — explicit
10990 // #[rustango(citext = false)] — explicit opt-out
10991 // String-only validation lives in the field-type
10992 // emitter (the FieldType discriminant is computed in
10993 // `detect_type`); the macro records the flag and the
10994 // DDL writer emits dialect-specific COLLATE / CITEXT.
10995 if let Ok(v) = meta.value() {
10996 let lit: syn::LitBool = v.parse()?;
10997 out.case_insensitive = lit.value;
10998 } else {
10999 out.case_insensitive = true;
11000 }
11001 return Ok(());
11002 }
11003 if meta.path.is_ident("validators") {
11004 let s: LitStr = meta.value()?.parse()?;
11005 let raw = s.value();
11006 out.validators = raw
11007 .split(',')
11008 .map(str::trim)
11009 .filter(|s| !s.is_empty())
11010 .map(str::to_owned)
11011 .collect();
11012 if out.validators.is_empty() {
11013 return Err(syn::Error::new(
11014 s.span(),
11015 "`validators = \"…\"` must list at least one name",
11016 ));
11017 }
11018 return Ok(());
11019 }
11020 if meta.path.is_ident("auto_uuid") {
11021 out.auto_uuid = true;
11022 // Implied: PK + auto + DEFAULT gen_random_uuid().
11023 // Each is also explicitly settable; the explicit
11024 // value wins if conflicting.
11025 out.primary_key = true;
11026 if out.default.is_none() {
11027 out.default = Some("gen_random_uuid()".into());
11028 }
11029 return Ok(());
11030 }
11031 if meta.path.is_ident("default_uuid_v7") {
11032 // Backend-neutral counterpart of `auto_uuid` — issue #823.
11033 // No SQL DEFAULT (the macro fills the value Rust-side
11034 // before binding); just mark the field as PK + Auto
11035 // so the insert path is the `Auto::Unset → generate`
11036 // branch.
11037 out.default_uuid_v7 = true;
11038 out.primary_key = true;
11039 return Ok(());
11040 }
11041 if meta.path.is_ident("auto_now_add") {
11042 out.auto_now_add = true;
11043 if out.default.is_none() {
11044 out.default = Some("now()".into());
11045 }
11046 return Ok(());
11047 }
11048 if meta.path.is_ident("auto_now") {
11049 out.auto_now = true;
11050 if out.default.is_none() {
11051 out.default = Some("now()".into());
11052 }
11053 return Ok(());
11054 }
11055 if meta.path.is_ident("soft_delete") {
11056 out.soft_delete = true;
11057 return Ok(());
11058 }
11059 if meta.path.is_ident("unique") {
11060 out.unique = true;
11061 return Ok(());
11062 }
11063 if meta.path.is_ident("index") {
11064 out.index = true;
11065 // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
11066 if meta.input.peek(syn::token::Paren) {
11067 meta.parse_nested_meta(|inner| {
11068 if inner.path.is_ident("unique") {
11069 out.index_unique = true;
11070 return Ok(());
11071 }
11072 if inner.path.is_ident("name") {
11073 let s: LitStr = inner.value()?.parse()?;
11074 out.index_name = Some(s.value());
11075 return Ok(());
11076 }
11077 if inner.path.is_ident("method") {
11078 let s: LitStr = inner.value()?.parse()?;
11079 let v = s.value();
11080 match v.as_str() {
11081 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
11082 out.index_method = v;
11083 }
11084 other => {
11085 return Err(inner.error(format!(
11086 "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
11087 )));
11088 }
11089 }
11090 return Ok(());
11091 }
11092 Err(inner.error(
11093 "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
11094 ))
11095 })?;
11096 }
11097 return Ok(());
11098 }
11099 Err(meta.error("unknown rustango field attribute"))
11100 })?;
11101 }
11102 Ok(out)
11103}
11104
11105/// Parse a signed integer literal, accepting optional leading `-`.
11106fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
11107 let expr: syn::Expr = meta.value()?.parse()?;
11108 match expr {
11109 syn::Expr::Lit(syn::ExprLit {
11110 lit: syn::Lit::Int(lit),
11111 ..
11112 }) => lit.base10_parse::<i64>(),
11113 syn::Expr::Unary(syn::ExprUnary {
11114 op: syn::UnOp::Neg(_),
11115 expr,
11116 ..
11117 }) => {
11118 if let syn::Expr::Lit(syn::ExprLit {
11119 lit: syn::Lit::Int(lit),
11120 ..
11121 }) = *expr
11122 {
11123 let v: i64 = lit.base10_parse()?;
11124 Ok(-v)
11125 } else {
11126 Err(syn::Error::new_spanned(expr, "expected integer literal"))
11127 }
11128 }
11129 other => Err(syn::Error::new_spanned(
11130 other,
11131 "expected integer literal (signed)",
11132 )),
11133 }
11134}
11135
11136struct FieldInfo<'a> {
11137 ident: &'a syn::Ident,
11138 column: String,
11139 primary_key: bool,
11140 /// `true` when the Rust type was `Auto<T>` — the INSERT path will
11141 /// skip this column when `Auto::Unset` and emit it under
11142 /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
11143 auto: bool,
11144 /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
11145 /// the `Column::Value` associated type for typed-column tokens.
11146 value_ty: &'a Type,
11147 /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
11148 field_type_tokens: TokenStream2,
11149 schema: TokenStream2,
11150 from_row_init: TokenStream2,
11151 /// Variant of [`Self::from_row_init`] that reads the column via
11152 /// `format!("{prefix}__{col}")` so a model can be decoded out of
11153 /// the aliased columns of a JOINed row. Drives slice 9.0d's
11154 /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
11155 /// helper that `select_related` calls when stitching loaded FKs.
11156 from_aliased_row_init: TokenStream2,
11157 /// MySQL twins of the two inits above. They differ only for a UUID
11158 /// column, which MySQL keeps as `CHAR(36)` text (#1733).
11159 from_row_init_my: TokenStream2,
11160 from_aliased_row_init_my: TokenStream2,
11161 /// A bare or optional `Uuid` field.
11162 uuid_column: bool,
11163 /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
11164 /// relation helper emit (`Author::<child>_set`) needs to know `T`
11165 /// to point the generated method at the right child model.
11166 fk_inner: Option<Type>,
11167 /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
11168 /// `kind` (since ForeignKey detection sets `kind` to K's
11169 /// underlying type) but stored separately for clarity at the
11170 /// `FkRelation` construction site, which only sees the FK's
11171 /// surface fields.
11172 fk_pk_kind: DetectedKind,
11173 /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
11174 /// the bare `ForeignKey<T, K>`. Routes the load_related and
11175 /// fk_pk_access emitters to wrap assignments / accessors in
11176 /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
11177 /// FK column compiles end-to-end. The DDL writer reads this off
11178 /// the field schema (`nullable` flag); the macro just needs to
11179 /// keep the Rust-side codegen consistent.
11180 nullable: bool,
11181 /// `true` when this column was marked `#[rustango(auto_now)]` —
11182 /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
11183 /// column instead of the user-supplied value, so `updated_at`
11184 /// always reflects the latest write without the caller having
11185 /// to remember to set it.
11186 auto_now: bool,
11187 /// `true` when this column was marked `#[rustango(auto_now_add)]`
11188 /// — the column is server-set on INSERT (DB DEFAULT) and
11189 /// **immutable** afterwards. `update_on` / `save_on` skip the
11190 /// column entirely so a stale `created_at` value in memory never
11191 /// rewrites the persisted timestamp.
11192 auto_now_add: bool,
11193 /// `true` when this column was marked `#[rustango(soft_delete)]`.
11194 /// Triggers emission of `soft_delete_on(executor)` and
11195 /// `restore_on(executor)` on the model's inherent impl. There is
11196 /// at most one such column per model — emission asserts this.
11197 soft_delete: bool,
11198 /// `Some` when this column was marked
11199 /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
11200 /// every INSERT and UPDATE path; the database recomputes the
11201 /// value from `EXPR`. Backlog item #35.
11202 generated_as: Option<String>,
11203 /// `true` when this column was marked
11204 /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
11205 /// emit an `insert_push` that auto-fills an `Auto::Unset` value
11206 /// with `Uuid::now_v7()` before binding, so the PK is generated
11207 /// Rust-side and the column is always present in the INSERT
11208 /// statement (no DB DEFAULT requirement). Issue #823.
11209 default_uuid_v7: bool,
11210 /// `Some` when this FK field carried `#[rustango(related_name =
11211 /// "...")]`. Threaded into [`FkRelation::related_name`] and
11212 /// consumed by [`reverse_helper_tokens`] to override the default
11213 /// `<child_snake>_set` accessor name. Follow-up to #816.
11214 related_name: Option<String>,
11215}
11216
11217/// Reject table names that won't survive SQL identifier
11218/// derivation downstream. Postgres' regular-identifier rule
11219/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
11220/// through the framework's unquoted FK / index / constraint name
11221/// emission without surprises. We also disallow leading-digit and
11222/// the empty string for clarity.
11223///
11224/// Reserved-word collisions (`select`, `from`, …) aren't flagged
11225/// here — those produce a runtime error from the SQL parser,
11226/// which is loud enough; statically enumerating reserved words
11227/// across the three supported dialects is more friction than help.
11228///
11229/// Backlog item #65.
11230fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11231 validate_sql_identifier(name, "table", span)
11232}
11233
11234/// Reject SQL identifiers that compile but break downstream SQL
11235/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11236/// `kind` is "table" / "column" — used for the error message so users
11237/// see which attribute caused the failure.
11238fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11239 if name.is_empty() {
11240 return Err(syn::Error::new(
11241 span,
11242 format!("`{kind} = \"\"` is not a valid SQL identifier"),
11243 ));
11244 }
11245 let mut chars = name.chars();
11246 let first = chars.next().unwrap();
11247 if !(first.is_ascii_alphabetic() || first == '_') {
11248 return Err(syn::Error::new(
11249 span,
11250 format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11251 ));
11252 }
11253 for c in chars {
11254 if !(c.is_ascii_alphanumeric() || c == '_') {
11255 return Err(syn::Error::new(
11256 span,
11257 format!(
11258 "{kind} name `{name}` contains invalid character {c:?} — \
11259 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11260 Hyphens in particular break FK / index name derivation \
11261 downstream; use underscores instead (e.g. `{}`)",
11262 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11263 ),
11264 ));
11265 }
11266 }
11267 Ok(())
11268}
11269
11270fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11271 let root = rustango_root();
11272 let attrs = parse_field_attrs(field)?;
11273 let ident = field
11274 .ident
11275 .as_ref()
11276 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11277 let name = ident_name(ident);
11278 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11279 let primary_key = attrs.primary_key;
11280 let DetectedType {
11281 kind,
11282 nullable,
11283 auto: detected_auto,
11284 fk_inner,
11285 } = detect_type(&field.ty)?;
11286 check_bound_compatibility(field, &attrs, kind)?;
11287 let auto = detected_auto;
11288 // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11289 // INSERT path: the user must wrap the field in `Auto<T>`, which
11290 // marks the column as DB-default-supplied. The mixin attrs then
11291 // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11292 // for `auto_now`, force the value on UPDATE too.
11293 if attrs.auto_uuid {
11294 if kind != DetectedKind::Uuid {
11295 return Err(syn::Error::new_spanned(
11296 field,
11297 "`#[rustango(auto_uuid)]` requires the field type to be \
11298 `Auto<uuid::Uuid>`",
11299 ));
11300 }
11301 if !detected_auto {
11302 return Err(syn::Error::new_spanned(
11303 field,
11304 "`#[rustango(auto_uuid)]` requires the field type to be \
11305 wrapped in `Auto<...>` so the macro skips the column on \
11306 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11307 ));
11308 }
11309 }
11310 if attrs.default_uuid_v7 {
11311 if kind != DetectedKind::Uuid {
11312 return Err(syn::Error::new_spanned(
11313 field,
11314 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11315 `Auto<uuid::Uuid>`",
11316 ));
11317 }
11318 if !detected_auto {
11319 return Err(syn::Error::new_spanned(
11320 field,
11321 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11322 wrapped in `Auto<...>` so the macro can detect the \
11323 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11324 ));
11325 }
11326 if attrs.auto_uuid {
11327 return Err(syn::Error::new_spanned(
11328 field,
11329 "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11330 `#[rustango(auto_uuid)]` — the former generates the UUID \
11331 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11332 Pick one.",
11333 ));
11334 }
11335 }
11336 if attrs.auto_now_add || attrs.auto_now {
11337 if kind != DetectedKind::DateTime {
11338 return Err(syn::Error::new_spanned(
11339 field,
11340 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11341 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11342 ));
11343 }
11344 if !detected_auto {
11345 return Err(syn::Error::new_spanned(
11346 field,
11347 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11348 the field type to be wrapped in `Auto<...>` so the macro skips \
11349 the column on INSERT and the DB DEFAULT (`now()`) fires",
11350 ));
11351 }
11352 }
11353 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11354 return Err(syn::Error::new_spanned(
11355 field,
11356 "`#[rustango(soft_delete)]` requires the field type to be \
11357 `Option<chrono::DateTime<chrono::Utc>>`",
11358 ));
11359 }
11360 let is_mixin_auto =
11361 attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11362 if detected_auto && !primary_key && !is_mixin_auto {
11363 return Err(syn::Error::new_spanned(
11364 field,
11365 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11366 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11367 `auto_now`",
11368 ));
11369 }
11370 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11371 return Err(syn::Error::new_spanned(
11372 field,
11373 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11374 SERIAL / BIGSERIAL already supplies a default sequence.",
11375 ));
11376 }
11377 if fk_inner.is_some() && primary_key {
11378 return Err(syn::Error::new_spanned(
11379 field,
11380 "`ForeignKey<T>` is not allowed on a primary-key field — \
11381 a row's PK is its own identity, not a reference to a parent.",
11382 ));
11383 }
11384 if attrs.generated_as.is_some() {
11385 if primary_key {
11386 return Err(syn::Error::new_spanned(
11387 field,
11388 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11389 primary-key field — a PK must be writable so the row \
11390 has an identity at INSERT time.",
11391 ));
11392 }
11393 if attrs.default.is_some() {
11394 return Err(syn::Error::new_spanned(
11395 field,
11396 "`#[rustango(generated_as = \"…\")]` cannot combine with \
11397 `default = \"…\"` — Postgres rejects DEFAULT on \
11398 generated columns. The expression IS the default.",
11399 ));
11400 }
11401 if detected_auto {
11402 return Err(syn::Error::new_spanned(
11403 field,
11404 "`#[rustango(generated_as = \"…\")]` is not allowed on \
11405 an `Auto<T>` field — generated columns are computed \
11406 by the DB, not server-assigned via a sequence. Use a \
11407 plain Rust type (e.g. `f64`).",
11408 ));
11409 }
11410 if fk_inner.is_some() {
11411 return Err(syn::Error::new_spanned(
11412 field,
11413 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11414 ForeignKey field.",
11415 ));
11416 }
11417 }
11418 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11419 let column_lit = column.as_str();
11420 // pgvector (#824): the `vector(dims = N)` attribute supplies the
11421 // dimension that the bare `Vector` Rust type can't carry, so emit
11422 // `FieldType::Vector(N)` here rather than the `variant_tokens`
11423 // fallback of `Vector(0)`.
11424 let field_type_tokens = if kind == DetectedKind::Vector {
11425 let root = rustango_root();
11426 let dims = attrs.vector_dims.unwrap_or(0);
11427 quote!(#root::core::FieldType::Vector(#dims))
11428 } else if kind == DetectedKind::Geometry {
11429 // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11430 // SRID that the bare `Point` Rust type can't carry, so emit
11431 // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11432 let root = rustango_root();
11433 let srid = attrs.geometry_srid.unwrap_or(0);
11434 quote!(#root::core::FieldType::Geometry(#srid))
11435 } else {
11436 kind.variant_tokens()
11437 };
11438 let max_length = optional_u32(attrs.max_length);
11439 let min = optional_i64(attrs.min);
11440 let max = optional_i64(attrs.max);
11441 let default = optional_str(attrs.default.as_deref());
11442
11443 let unique = attrs.unique;
11444 let generated_as = optional_str(attrs.generated_as.as_deref());
11445 let help_text = optional_str(attrs.help_text.as_deref());
11446 let choices = optional_choices(attrs.choices.as_deref());
11447 let db_comment = optional_str(attrs.db_comment.as_deref());
11448 let verbose_name = optional_str(attrs.verbose_name.as_deref());
11449 let editable = attrs.editable;
11450 let blank = attrs.blank;
11451 let case_insensitive = attrs.case_insensitive;
11452 let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11453 if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() && fk_inner.is_none()
11454 {
11455 return Err(syn::Error::new_spanned(
11456 field,
11457 "`#[rustango(on_delete = \"…\")]` requires a `ForeignKey<T>` field \
11458 or `fk = \"<table>\"` / `o2o = \"<table>\"` on the same field — it \
11459 has no meaning on a non-FK column.",
11460 ));
11461 }
11462 if attrs.on_delete.as_deref() == Some("set_null") && !nullable {
11463 return Err(syn::Error::new_spanned(
11464 field,
11465 "`on_delete = \"set_null\"` needs a nullable column: use \
11466 `Option<ForeignKey<T>>` or `Option<…>` (spelled out; a type \
11467 alias for `Option` is not seen).",
11468 ));
11469 }
11470 let fk_on_delete = match attrs.on_delete.as_deref() {
11471 None => quote!(::core::option::Option::None),
11472 Some(action) => {
11473 let variant = match action {
11474 "cascade" => quote!(Cascade),
11475 "restrict" => quote!(Restrict),
11476 "set_null" => quote!(SetNull),
11477 "set_default" => quote!(SetDefault),
11478 "no_action" => quote!(NoAction),
11479 // parse_field_attrs already validated this — guard against future drift.
11480 other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11481 };
11482 quote!(::core::option::Option::Some(
11483 #root::core::OnDeleteAction::#variant
11484 ))
11485 }
11486 };
11487 let auto_now_flag = attrs.auto_now;
11488 let schema = quote! {{
11489 #root::core::FieldSchema::from_parts(#root::core::FieldSchemaParts {
11490 name: #name,
11491 column: #column_lit,
11492 ty: #field_type_tokens,
11493 nullable: #nullable,
11494 primary_key: #primary_key,
11495 relation: #relation,
11496 max_length: #max_length,
11497 min: #min,
11498 max: #max,
11499 default: #default,
11500 auto: #auto,
11501 auto_now: #auto_now_flag,
11502 unique: #unique,
11503 generated_as: #generated_as,
11504 help_text: #help_text,
11505 choices: #choices,
11506 db_comment: #db_comment,
11507 verbose_name: #verbose_name,
11508 editable: #editable,
11509 blank: #blank,
11510 case_insensitive: #case_insensitive,
11511 fk_on_delete: #fk_on_delete,
11512 validators: &[ #(#validators_lits),* ],
11513 })
11514 }};
11515
11516 let from_row_init = quote! {
11517 #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11518 };
11519 let from_aliased_row_init = quote! {
11520 #ident: #root::sql::sqlx::Row::try_get(
11521 row,
11522 #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11523 )?
11524 };
11525 // `Auto<Uuid>` and `ForeignKey<_, Uuid>` decode through their own
11526 // MySQL impls; a bare or optional `Uuid` needs `FlatScalar`'s cell.
11527 let uuid_column = kind == DetectedKind::Uuid && !detected_auto && fk_inner.is_none();
11528 let (from_row_init_my, from_aliased_row_init_my) = if uuid_column {
11529 (
11530 quote! { #ident: #root::sql::try_get_flat_my(row, #column_lit)? },
11531 quote! {
11532 #ident: #root::sql::try_get_flat_my(
11533 row,
11534 #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11535 )?
11536 },
11537 )
11538 } else {
11539 (from_row_init.clone(), from_aliased_row_init.clone())
11540 };
11541
11542 Ok(FieldInfo {
11543 ident,
11544 column,
11545 primary_key,
11546 auto,
11547 value_ty: &field.ty,
11548 field_type_tokens,
11549 schema,
11550 from_row_init,
11551 from_aliased_row_init,
11552 from_row_init_my,
11553 from_aliased_row_init_my,
11554 uuid_column,
11555 fk_inner: fk_inner.cloned(),
11556 fk_pk_kind: kind,
11557 nullable,
11558 auto_now: attrs.auto_now,
11559 auto_now_add: attrs.auto_now_add,
11560 soft_delete: attrs.soft_delete,
11561 generated_as: attrs.generated_as.clone(),
11562 default_uuid_v7: attrs.default_uuid_v7,
11563 related_name: attrs.related_name.clone(),
11564 })
11565}
11566
11567fn check_bound_compatibility(
11568 field: &syn::Field,
11569 attrs: &FieldAttrs,
11570 kind: DetectedKind,
11571) -> syn::Result<()> {
11572 if attrs.max_length.is_some() && kind != DetectedKind::String {
11573 return Err(syn::Error::new_spanned(
11574 field,
11575 "`max_length` is only valid on `String` fields (or `Option<String>`)",
11576 ));
11577 }
11578 // These were silently dropped on the wrong type (#1937).
11579 if attrs.case_insensitive && kind != DetectedKind::String {
11580 return Err(syn::Error::new_spanned(
11581 field,
11582 "`citext` is only valid on `String` fields (or `Option<String>`)",
11583 ));
11584 }
11585 if attrs.vector_dims.is_some() && kind != DetectedKind::Vector {
11586 return Err(syn::Error::new_spanned(
11587 field,
11588 "`vector(dims = …)` is only valid on a pgvector `Vector` field",
11589 ));
11590 }
11591 if attrs.geometry_srid.is_some() && kind != DetectedKind::Geometry {
11592 return Err(syn::Error::new_spanned(
11593 field,
11594 "`geometry(srid = …)` is only valid on a PostGIS `Point` field",
11595 ));
11596 }
11597 if attrs.choices.is_some() && kind != DetectedKind::String {
11598 return Err(syn::Error::new_spanned(
11599 field,
11600 "`choices` is only valid on `String` fields (or `Option<String>`) — \
11601 integer-valued enumerations should be modeled with a Rust enum and \
11602 custom (de)serializer for now",
11603 ));
11604 }
11605 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11606 return Err(syn::Error::new_spanned(
11607 field,
11608 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11609 ));
11610 }
11611 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11612 if min > max {
11613 return Err(syn::Error::new_spanned(
11614 field,
11615 format!("`min` ({min}) is greater than `max` ({max})"),
11616 ));
11617 }
11618 }
11619 Ok(())
11620}
11621
11622/// A `&'static [ty]` of `const fn` results. The slice needs its own
11623/// `const`: a `&[..]` assigned in a statement is not extended to `'static`.
11624fn const_slice(ty: &TokenStream2, items: impl Iterator<Item = TokenStream2>) -> TokenStream2 {
11625 quote!({
11626 const ITEMS: &[#ty] = &[ #(#items),* ];
11627 ITEMS
11628 })
11629}
11630
11631fn optional_u32(value: Option<u32>) -> TokenStream2 {
11632 if let Some(v) = value {
11633 quote!(::core::option::Option::Some(#v))
11634 } else {
11635 quote!(::core::option::Option::None)
11636 }
11637}
11638
11639fn optional_i64(value: Option<i64>) -> TokenStream2 {
11640 if let Some(v) = value {
11641 quote!(::core::option::Option::Some(#v))
11642 } else {
11643 quote!(::core::option::Option::None)
11644 }
11645}
11646
11647fn optional_str(value: Option<&str>) -> TokenStream2 {
11648 if let Some(v) = value {
11649 quote!(::core::option::Option::Some(#v))
11650 } else {
11651 quote!(::core::option::Option::None)
11652 }
11653}
11654
11655fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11656 let Some(pairs) = pairs else {
11657 return quote!(::core::option::Option::None);
11658 };
11659 let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11660 quote!(::core::option::Option::Some(&[#(#entries),*]))
11661}
11662
11663fn relation_tokens(
11664 field: &syn::Field,
11665 attrs: &FieldAttrs,
11666 fk_inner: Option<&syn::Type>,
11667 table: &str,
11668) -> syn::Result<TokenStream2> {
11669 let root = rustango_root();
11670 if let Some(inner) = fk_inner {
11671 if attrs.fk.is_some() || attrs.o2o.is_some() {
11672 return Err(syn::Error::new_spanned(
11673 field,
11674 "`ForeignKey<T>` already declares the FK target via the type parameter — \
11675 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11676 ));
11677 }
11678 // No `on`: the target's PK column, which may be renamed.
11679 let on = match attrs.on.as_deref() {
11680 Some(on) => quote!(#on),
11681 None => quote! {
11682 match <#inner as #root::core::Model>::SCHEMA.pk_column() {
11683 ::core::option::Option::Some(c) => c,
11684 ::core::option::Option::None => "id",
11685 }
11686 },
11687 };
11688 return Ok(quote! {
11689 ::core::option::Option::Some(#root::core::Relation::fk(
11690 <#inner as #root::core::Model>::SCHEMA.table,
11691 #on,
11692 ))
11693 });
11694 }
11695 match (&attrs.fk, &attrs.o2o) {
11696 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11697 field,
11698 "`fk` and `o2o` are mutually exclusive",
11699 )),
11700 (Some(to), None) => {
11701 let on = attrs.on.as_deref().unwrap_or("id");
11702 // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11703 // the model's own table. Threaded as a literal string at
11704 // macro-expansion time to sidestep the const-eval cycle
11705 // that `Self::SCHEMA.table` would create when referenced
11706 // inside Self::SCHEMA's own initializer.
11707 let resolved = if to == "self" { table } else { to };
11708 Ok(quote! {
11709 ::core::option::Option::Some(#root::core::Relation::fk(#resolved, #on))
11710 })
11711 }
11712 (None, Some(to)) => {
11713 let on = attrs.on.as_deref().unwrap_or("id");
11714 let resolved = if to == "self" { table } else { to };
11715 Ok(quote! {
11716 ::core::option::Option::Some(#root::core::Relation::o2o(#resolved, #on))
11717 })
11718 }
11719 (None, None) => {
11720 if attrs.on.is_some() {
11721 return Err(syn::Error::new_spanned(
11722 field,
11723 "`on` requires `fk` or `o2o`",
11724 ));
11725 }
11726 Ok(quote!(::core::option::Option::None))
11727 }
11728 }
11729}
11730
11731/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11732/// about kinds without depending on `rustango-core` (which would require a
11733/// proc-macro/normal split it doesn't have today).
11734#[derive(Clone, Copy, PartialEq, Eq)]
11735enum DetectedKind {
11736 I16,
11737 I32,
11738 I64,
11739 F32,
11740 F64,
11741 Bool,
11742 String,
11743 DateTime,
11744 Date,
11745 Time,
11746 Uuid,
11747 Json,
11748 Decimal,
11749 Binary,
11750 /// `Array<String>` → PG `text[]` (#341).
11751 ArrayText,
11752 /// `Array<i32>` → PG `integer[]` (#341).
11753 ArrayInt,
11754 /// `Array<i64>` → PG `bigint[]` (#341).
11755 ArrayBigInt,
11756 /// `Range<i32>` → PG `int4range` (#343).
11757 RangeInt,
11758 /// `Range<i64>` → PG `int8range` (#343).
11759 RangeBigInt,
11760 /// `Range<Decimal>` → PG `numrange` (#343).
11761 RangeNumeric,
11762 /// `Range<NaiveDate>` → PG `daterange` (#343).
11763 RangeDate,
11764 /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11765 RangeDateTime,
11766 /// `HStore` → PG `hstore` (#342).
11767 HStore,
11768 /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11769 /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11770 /// in at the `FieldType` emission site (not carried on this enum).
11771 Vector,
11772 /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11773 /// from the `#[rustango(geometry(srid = N))]` field attribute,
11774 /// threaded in at the `FieldType` emission site.
11775 Geometry,
11776}
11777
11778impl DetectedKind {
11779 fn variant_tokens(self) -> TokenStream2 {
11780 let root = rustango_root();
11781 match self {
11782 Self::I16 => quote!(#root::core::FieldType::I16),
11783 Self::I32 => quote!(#root::core::FieldType::I32),
11784 Self::I64 => quote!(#root::core::FieldType::I64),
11785 Self::F32 => quote!(#root::core::FieldType::F32),
11786 Self::F64 => quote!(#root::core::FieldType::F64),
11787 Self::Bool => quote!(#root::core::FieldType::Bool),
11788 Self::String => quote!(#root::core::FieldType::String),
11789 Self::DateTime => quote!(#root::core::FieldType::DateTime),
11790 Self::Date => quote!(#root::core::FieldType::Date),
11791 Self::Time => quote!(#root::core::FieldType::Time),
11792 Self::Uuid => quote!(#root::core::FieldType::Uuid),
11793 Self::Json => quote!(#root::core::FieldType::Json),
11794 Self::Decimal => quote!(#root::core::FieldType::Decimal),
11795 Self::Binary => quote!(#root::core::FieldType::Binary),
11796 Self::ArrayText => {
11797 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11798 }
11799 Self::ArrayInt => {
11800 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11801 }
11802 Self::ArrayBigInt => {
11803 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11804 }
11805 Self::RangeInt => {
11806 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11807 }
11808 Self::RangeBigInt => {
11809 quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11810 }
11811 Self::RangeNumeric => {
11812 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11813 }
11814 Self::RangeDate => {
11815 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11816 }
11817 Self::RangeDateTime => {
11818 quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11819 }
11820 Self::HStore => quote!(#root::core::FieldType::HStore),
11821 // Dimension comes from the `vector(dims = N)` attribute,
11822 // applied at the emission site; `0` here is just a fallback.
11823 Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11824 // SRID comes from the `geometry(srid = N)` attribute, applied
11825 // at the emission site; `0` here is just a fallback.
11826 Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11827 }
11828 }
11829
11830 fn is_integer(self) -> bool {
11831 matches!(self, Self::I16 | Self::I32 | Self::I64)
11832 }
11833}
11834
11835/// Result of walking a field's Rust type. `kind` is the underlying
11836/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11837/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11838/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11839/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11840#[derive(Clone, Copy)]
11841struct DetectedType<'a> {
11842 kind: DetectedKind,
11843 nullable: bool,
11844 auto: bool,
11845 fk_inner: Option<&'a syn::Type>,
11846}
11847
11848/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11849/// non-`Auto` types — the caller should already have routed Auto-only
11850/// codegen through this helper, so a `None` indicates a macro-internal
11851/// invariant break.
11852fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11853 let Type::Path(TypePath { path, qself: None }) = ty else {
11854 return None;
11855 };
11856 let last = path.segments.last()?;
11857 if last.ident != "Auto" {
11858 return None;
11859 }
11860 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11861 return None;
11862 };
11863 args.args.iter().find_map(|a| match a {
11864 syn::GenericArgument::Type(t) => Some(t),
11865 _ => None,
11866 })
11867}
11868
11869fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11870 let Type::Path(TypePath { path, qself: None }) = ty else {
11871 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11872 };
11873 let last = path
11874 .segments
11875 .last()
11876 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11877
11878 if last.ident == "Option" {
11879 let inner = generic_inner(ty, &last.arguments, "Option")?;
11880 let inner_det = detect_type(inner)?;
11881 if inner_det.nullable {
11882 return Err(syn::Error::new_spanned(
11883 ty,
11884 "nested Option is not supported",
11885 ));
11886 }
11887 if inner_det.auto {
11888 return Err(syn::Error::new_spanned(
11889 ty,
11890 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11891 ));
11892 }
11893 return Ok(DetectedType {
11894 nullable: true,
11895 ..inner_det
11896 });
11897 }
11898
11899 if last.ident == "Auto" {
11900 let inner = generic_inner(ty, &last.arguments, "Auto")?;
11901 let inner_det = detect_type(inner)?;
11902 if inner_det.auto {
11903 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11904 }
11905 if inner_det.nullable {
11906 return Err(syn::Error::new_spanned(
11907 ty,
11908 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11909 ));
11910 }
11911 if inner_det.fk_inner.is_some() {
11912 return Err(syn::Error::new_spanned(
11913 ty,
11914 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11915 ));
11916 }
11917 if !matches!(
11918 inner_det.kind,
11919 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11920 ) {
11921 return Err(syn::Error::new_spanned(
11922 ty,
11923 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11924 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11925 (DEFAULT now())",
11926 ));
11927 }
11928 return Ok(DetectedType {
11929 auto: true,
11930 ..inner_det
11931 });
11932 }
11933
11934 if last.ident == "ForeignKey" {
11935 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11936 // Resolve the FK column's underlying SQL type from `K`. When the
11937 // user wrote `ForeignKey<T>` without a key parameter, the type
11938 // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11939 // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11940 // recurse into K so the column DDL emits the right SQL type
11941 // (VARCHAR for String, UUID for Uuid, …) and the load_related
11942 // emitter knows which `SqlValue` variant to match.
11943 let kind = match key_ty {
11944 Some(k) => detect_type(k)?.kind,
11945 None => DetectedKind::I64,
11946 };
11947 return Ok(DetectedType {
11948 kind,
11949 nullable: false,
11950 auto: false,
11951 fk_inner: Some(inner),
11952 });
11953 }
11954
11955 let kind = match last.ident.to_string().as_str() {
11956 "i16" => DetectedKind::I16,
11957 "i32" => DetectedKind::I32,
11958 "i64" => DetectedKind::I64,
11959 "f32" => DetectedKind::F32,
11960 "f64" => DetectedKind::F64,
11961 "bool" => DetectedKind::Bool,
11962 "String" => DetectedKind::String,
11963 "DateTime" => DetectedKind::DateTime,
11964 "NaiveDate" => DetectedKind::Date,
11965 "NaiveTime" => DetectedKind::Time,
11966 "Uuid" => DetectedKind::Uuid,
11967 "Value" => DetectedKind::Json,
11968 "Decimal" => DetectedKind::Decimal,
11969 // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11970 // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11971 // — that would emit Binary DDL and decode-fail at runtime.
11972 "Vec" => {
11973 let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11974 if let Type::Path(TypePath { path, qself: None }) = inner {
11975 if let Some(seg) = path.segments.last() {
11976 if seg.ident == "u8" && seg.arguments.is_empty() {
11977 return Ok(DetectedType {
11978 kind: DetectedKind::Binary,
11979 nullable: false,
11980 auto: false,
11981 fk_inner: None,
11982 });
11983 }
11984 }
11985 }
11986 return Err(syn::Error::new_spanned(
11987 ty,
11988 "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11989 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11990 ));
11991 }
11992 // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11993 // `integer[]` / `bigint[]` (#341).
11994 "Array" => {
11995 let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11996 let elem = match inner {
11997 Type::Path(TypePath { path, qself: None }) => {
11998 path.segments.last().map(|s| s.ident.to_string())
11999 }
12000 _ => None,
12001 };
12002 let kind = match elem.as_deref() {
12003 Some("String") => DetectedKind::ArrayText,
12004 Some("i32") => DetectedKind::ArrayInt,
12005 Some("i64") => DetectedKind::ArrayBigInt,
12006 _ => {
12007 return Err(syn::Error::new_spanned(
12008 ty,
12009 "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
12010 `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
12011 ));
12012 }
12013 };
12014 return Ok(DetectedType {
12015 kind,
12016 nullable: false,
12017 auto: false,
12018 fk_inner: None,
12019 });
12020 }
12021 // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
12022 // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
12023 // `int8range` / `numrange` / `daterange` / `tstzrange` (#343).
12024 "Range" => {
12025 let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
12026 let elem = match inner {
12027 Type::Path(TypePath { path, qself: None }) => {
12028 path.segments.last().map(|s| s.ident.to_string())
12029 }
12030 _ => None,
12031 };
12032 let kind = match elem.as_deref() {
12033 Some("i32") => DetectedKind::RangeInt,
12034 Some("i64") => DetectedKind::RangeBigInt,
12035 Some("Decimal") => DetectedKind::RangeNumeric,
12036 Some("NaiveDate") => DetectedKind::RangeDate,
12037 Some("DateTime") => DetectedKind::RangeDateTime,
12038 _ => {
12039 return Err(syn::Error::new_spanned(
12040 ty,
12041 "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
12042 `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
12043 `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
12044 (→ tstzrange) are supported (#343)",
12045 ));
12046 }
12047 };
12048 return Ok(DetectedType {
12049 kind,
12050 nullable: false,
12051 auto: false,
12052 fk_inner: None,
12053 });
12054 }
12055 // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
12056 // (the `CastValue` impl bridges logical↔stored); the field's own
12057 // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
12058 // schema just needs `FieldType::String`.
12059 "Cast" => {
12060 return Ok(DetectedType {
12061 kind: DetectedKind::String,
12062 nullable: false,
12063 auto: false,
12064 fk_inner: None,
12065 });
12066 }
12067 // `HStore` → PG `hstore` (#342). No generic
12068 // parameter — always a string→string map.
12069 "HStore" => {
12070 return Ok(DetectedType {
12071 kind: DetectedKind::HStore,
12072 nullable: false,
12073 auto: false,
12074 fk_inner: None,
12075 });
12076 }
12077 // `Vector` → pgvector `vector(N)` (#824). The dimension is
12078 // supplied by `#[rustango(vector(dims = N))]`, not the type.
12079 "Vector" => {
12080 return Ok(DetectedType {
12081 kind: DetectedKind::Vector,
12082 nullable: false,
12083 auto: false,
12084 fk_inner: None,
12085 });
12086 }
12087 // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
12088 // supplied by `#[rustango(geometry(srid = N))]`, not the type.
12089 "Point" => {
12090 return Ok(DetectedType {
12091 kind: DetectedKind::Geometry,
12092 nullable: false,
12093 auto: false,
12094 fk_inner: None,
12095 });
12096 }
12097 other => {
12098 return Err(syn::Error::new_spanned(
12099 ty,
12100 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)"),
12101 ));
12102 }
12103 };
12104 Ok(DetectedType {
12105 kind,
12106 nullable: false,
12107 auto: false,
12108 fk_inner: None,
12109 })
12110}
12111
12112fn generic_inner<'a>(
12113 ty: &'a Type,
12114 arguments: &'a PathArguments,
12115 wrapper: &str,
12116) -> syn::Result<&'a Type> {
12117 let PathArguments::AngleBracketed(args) = arguments else {
12118 return Err(syn::Error::new_spanned(
12119 ty,
12120 format!("{wrapper} requires a generic argument"),
12121 ));
12122 };
12123 args.args
12124 .iter()
12125 .find_map(|a| match a {
12126 GenericArgument::Type(t) => Some(t),
12127 _ => None,
12128 })
12129 .ok_or_else(|| {
12130 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12131 })
12132}
12133
12134/// Like [`generic_inner`] but pulls *two* type args — the first is
12135/// required, the second is optional. Used by the `ForeignKey<T, K>`
12136/// detection where K defaults to `i64` when omitted.
12137fn generic_pair<'a>(
12138 ty: &'a Type,
12139 arguments: &'a PathArguments,
12140 wrapper: &str,
12141) -> syn::Result<(&'a Type, Option<&'a Type>)> {
12142 let PathArguments::AngleBracketed(args) = arguments else {
12143 return Err(syn::Error::new_spanned(
12144 ty,
12145 format!("{wrapper} requires a generic argument"),
12146 ));
12147 };
12148 let mut types = args.args.iter().filter_map(|a| match a {
12149 GenericArgument::Type(t) => Some(t),
12150 _ => None,
12151 });
12152 let first = types.next().ok_or_else(|| {
12153 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12154 })?;
12155 let second = types.next();
12156 Ok((first, second))
12157}
12158
12159fn to_snake_case(s: &str) -> String {
12160 let mut out = String::with_capacity(s.len() + 4);
12161 for (i, ch) in s.chars().enumerate() {
12162 if ch.is_ascii_uppercase() {
12163 if i > 0 {
12164 out.push('_');
12165 }
12166 out.push(ch.to_ascii_lowercase());
12167 } else {
12168 out.push(ch);
12169 }
12170 }
12171 out
12172}
12173
12174// ============================================================
12175// #[derive(Form)] — slice 8.4B
12176// ============================================================
12177
12178/// Per-field `#[form(...)]` attributes recognised by the derive.
12179#[derive(Default)]
12180struct FormFieldAttrs {
12181 min: Option<i64>,
12182 max: Option<i64>,
12183 min_length: Option<u32>,
12184 max_length: Option<u32>,
12185 /// `#[form(clean = "fn_name")]` — per-field clean hook.
12186 /// The named static method on the form struct is called after the
12187 /// field's typed parse + length/range checks; it gets the parsed
12188 /// value by reference and returns `Result<<FieldType>, String>`.
12189 /// On Ok, the returned value replaces the parsed one; on Err, the
12190 /// message is attached to the field error list. Issue #372.
12191 clean: Option<syn::Ident>,
12192}
12193
12194/// Container-level `#[form(...)]` attributes. Currently only the
12195/// cross-field `validate` hook (issue #373).
12196#[derive(Default)]
12197struct FormContainerAttrs {
12198 /// `#[form(validate = "fn_name")]` — whole-form clean hook.
12199 /// After every per-field parse succeeds, the named method on the
12200 /// form struct is called with `&self` and may return
12201 /// `Result<(), FormErrors>`. Errors merge into the field error
12202 /// list. Issue #373.
12203 validate: Option<syn::Ident>,
12204}
12205
12206/// Detected shape of a form field's Rust type.
12207#[derive(Clone, Copy)]
12208enum FormFieldKind {
12209 String,
12210 I16,
12211 I32,
12212 I64,
12213 F32,
12214 F64,
12215 Bool,
12216}
12217
12218impl FormFieldKind {
12219 fn parse_method(self) -> &'static str {
12220 match self {
12221 Self::I16 => "i16",
12222 Self::I32 => "i32",
12223 Self::I64 => "i64",
12224 Self::F32 => "f32",
12225 Self::F64 => "f64",
12226 // String + Bool don't go through `str::parse`; the codegen
12227 // handles them inline.
12228 Self::String | Self::Bool => "",
12229 }
12230 }
12231}
12232
12233fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
12234 let root = rustango_root();
12235 let struct_name = &input.ident;
12236
12237 let Data::Struct(data) = &input.data else {
12238 return Err(syn::Error::new_spanned(
12239 struct_name,
12240 "Form can only be derived on structs",
12241 ));
12242 };
12243 let Fields::Named(named) = &data.fields else {
12244 return Err(syn::Error::new_spanned(
12245 struct_name,
12246 "Form requires a struct with named fields",
12247 ));
12248 };
12249
12250 // #373 — container-level `#[form(validate = "fn")]` hook.
12251 let container = parse_form_container_attrs(input)?;
12252 let post_field_clean: Vec<TokenStream2> = Vec::new();
12253 let _ = post_field_clean;
12254
12255 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12256 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12257
12258 for field in &named.named {
12259 let ident = field
12260 .ident
12261 .as_ref()
12262 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12263 let attrs = parse_form_field_attrs(field)?;
12264 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12265 let is_string = matches!(kind, FormFieldKind::String);
12266 let is_number = !is_string && !matches!(kind, FormFieldKind::Bool);
12267 if !is_string && (attrs.min_length.is_some() || attrs.max_length.is_some()) {
12268 return Err(syn::Error::new(
12269 field.span(),
12270 "`min_length` / `max_length` need a String field",
12271 ));
12272 }
12273 if !is_number && (attrs.min.is_some() || attrs.max.is_some()) {
12274 return Err(syn::Error::new(
12275 field.span(),
12276 "`min` / `max` need a numeric field",
12277 ));
12278 }
12279
12280 let name_lit = ident_name(ident);
12281 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12282 // #372 — append the per-field `clean_<field>` call right after
12283 // the parse block when the attribute is set. The clean fn
12284 // takes &T and returns Result<T, String>; on Err we attach
12285 // the message to the field error list without aborting, so
12286 // the caller sees every field error at once.
12287 let clean_block = if let Some(clean_fn) = &attrs.clean {
12288 quote! {
12289 if __errors.fields().get(#name_lit).is_none() {
12290 match Self::#clean_fn(&#ident) {
12291 ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
12292 ::core::result::Result::Err(__msg) => {
12293 __errors.add(#name_lit, __msg);
12294 }
12295 }
12296 }
12297 }
12298 } else {
12299 quote! {}
12300 };
12301 field_blocks.push(quote! {
12302 #parse_block
12303 #clean_block
12304 });
12305 field_idents.push(ident);
12306 }
12307
12308 // #373 — after every per-field parse + clean succeeds, call the
12309 // cross-field validator if declared. Errors merge into the
12310 // outgoing FormErrors via the existing `FormErrors::merge` helper
12311 // (same primitive the serializer cross-field hook uses).
12312 let cross_field_call = if let Some(validate_fn) = &container.validate {
12313 quote! {
12314 if __errors.is_empty() {
12315 let __candidate = Self { #( #field_idents ),* };
12316 if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12317 __errors.merge(__other);
12318 }
12319 if !__errors.is_empty() {
12320 return ::core::result::Result::Err(__errors);
12321 }
12322 return ::core::result::Result::Ok(__candidate);
12323 }
12324 }
12325 } else {
12326 quote! {}
12327 };
12328
12329 Ok(quote! {
12330 impl #root::forms::Form for #struct_name {
12331 fn parse(
12332 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12333 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12334 let mut __errors = #root::forms::FormErrors::default();
12335 #( #field_blocks )*
12336 #cross_field_call
12337 if !__errors.is_empty() {
12338 return ::core::result::Result::Err(__errors);
12339 }
12340 ::core::result::Result::Ok(Self {
12341 #( #field_idents ),*
12342 })
12343 }
12344 }
12345 })
12346}
12347
12348fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12349 let mut out = FormContainerAttrs::default();
12350 for attr in &input.attrs {
12351 if !attr.path().is_ident("form") {
12352 continue;
12353 }
12354 attr.parse_nested_meta(|meta| {
12355 if meta.path.is_ident("validate") {
12356 let s: LitStr = meta.value()?.parse()?;
12357 out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12358 return Ok(());
12359 }
12360 Err(meta.error("unknown form container attribute (supported: `validate`)"))
12361 })?;
12362 }
12363 Ok(out)
12364}
12365
12366fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12367 let mut out = FormFieldAttrs::default();
12368 for attr in &field.attrs {
12369 if !attr.path().is_ident("form") {
12370 continue;
12371 }
12372 attr.parse_nested_meta(|meta| {
12373 if meta.path.is_ident("min") {
12374 let lit: syn::LitInt = meta.value()?.parse()?;
12375 out.min = Some(lit.base10_parse::<i64>()?);
12376 return Ok(());
12377 }
12378 if meta.path.is_ident("max") {
12379 let lit: syn::LitInt = meta.value()?.parse()?;
12380 out.max = Some(lit.base10_parse::<i64>()?);
12381 return Ok(());
12382 }
12383 if meta.path.is_ident("min_length") {
12384 let lit: syn::LitInt = meta.value()?.parse()?;
12385 out.min_length = Some(lit.base10_parse::<u32>()?);
12386 return Ok(());
12387 }
12388 if meta.path.is_ident("max_length") {
12389 let lit: syn::LitInt = meta.value()?.parse()?;
12390 out.max_length = Some(lit.base10_parse::<u32>()?);
12391 return Ok(());
12392 }
12393 if meta.path.is_ident("clean") {
12394 let s: LitStr = meta.value()?.parse()?;
12395 out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12396 return Ok(());
12397 }
12398 Err(meta.error(
12399 "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12400 ))
12401 })?;
12402 }
12403 Ok(out)
12404}
12405
12406fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12407 let Type::Path(TypePath { path, qself: None }) = ty else {
12408 return Err(syn::Error::new(
12409 span,
12410 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12411 ));
12412 };
12413 let last = path
12414 .segments
12415 .last()
12416 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12417
12418 if last.ident == "Option" {
12419 let inner = generic_inner(ty, &last.arguments, "Option")?;
12420 let (kind, nested) = detect_form_field(inner, span)?;
12421 if nested {
12422 return Err(syn::Error::new(
12423 span,
12424 "nested Option in Form fields is not supported",
12425 ));
12426 }
12427 return Ok((kind, true));
12428 }
12429
12430 let kind = match last.ident.to_string().as_str() {
12431 "String" => FormFieldKind::String,
12432 "i16" => FormFieldKind::I16,
12433 "i32" => FormFieldKind::I32,
12434 "i64" => FormFieldKind::I64,
12435 "f32" => FormFieldKind::F32,
12436 "f64" => FormFieldKind::F64,
12437 "bool" => FormFieldKind::Bool,
12438 other => {
12439 return Err(syn::Error::new(
12440 span,
12441 format!(
12442 "Form field type `{other}` is not supported in v0.8 — use String / \
12443 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12444 ),
12445 ));
12446 }
12447 };
12448 Ok((kind, false))
12449}
12450
12451#[allow(clippy::too_many_lines)]
12452fn render_form_field_parse(
12453 ident: &syn::Ident,
12454 name_lit: &str,
12455 kind: FormFieldKind,
12456 nullable: bool,
12457 attrs: &FormFieldAttrs,
12458) -> TokenStream2 {
12459 // Pull the raw &str from the payload. Uses variable name `data` to
12460 // match the new `Form::parse(data: &HashMap<…>)` signature.
12461 let lookup = quote! {
12462 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12463 };
12464
12465 let parsed_value = match kind {
12466 // `Option<bool>`: absent or blank is `None`, as for the other types.
12467 FormFieldKind::Bool if nullable => quote! {
12468 let __v: ::core::option::Option<bool> = match __raw {
12469 ::core::option::Option::None => ::core::option::Option::None,
12470 ::core::option::Option::Some(__s) if __s.is_empty() => {
12471 ::core::option::Option::None
12472 }
12473 ::core::option::Option::Some(__s) => ::core::option::Option::Some(!matches!(
12474 __s.to_ascii_lowercase().as_str(),
12475 "false" | "0" | "off" | "no"
12476 )),
12477 };
12478 },
12479 FormFieldKind::Bool => quote! {
12480 let __v: bool = match __raw {
12481 ::core::option::Option::None => false,
12482 ::core::option::Option::Some(__s) => !matches!(
12483 __s.to_ascii_lowercase().as_str(),
12484 "" | "false" | "0" | "off" | "no"
12485 ),
12486 };
12487 },
12488 FormFieldKind::String => {
12489 if nullable {
12490 quote! {
12491 let __v: ::core::option::Option<::std::string::String> = match __raw {
12492 ::core::option::Option::None => ::core::option::Option::None,
12493 ::core::option::Option::Some(__s) if __s.is_empty() => {
12494 ::core::option::Option::None
12495 }
12496 ::core::option::Option::Some(__s) => {
12497 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12498 }
12499 };
12500 }
12501 } else {
12502 quote! {
12503 let __v: ::std::string::String = match __raw {
12504 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12505 ::core::clone::Clone::clone(__s)
12506 }
12507 _ => {
12508 __errors.add(#name_lit, "This field is required.");
12509 ::std::string::String::new()
12510 }
12511 };
12512 }
12513 }
12514 }
12515 FormFieldKind::I16
12516 | FormFieldKind::I32
12517 | FormFieldKind::I64
12518 | FormFieldKind::F32
12519 | FormFieldKind::F64 => {
12520 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12521 let ty_lit = kind.parse_method();
12522 let default_val = match kind {
12523 FormFieldKind::I16 => quote! { 0i16 },
12524 FormFieldKind::I32 => quote! { 0i32 },
12525 FormFieldKind::I64 => quote! { 0i64 },
12526 FormFieldKind::F32 => quote! { 0f32 },
12527 FormFieldKind::F64 => quote! { 0f64 },
12528 _ => quote! { Default::default() },
12529 };
12530 if nullable {
12531 quote! {
12532 let __v: ::core::option::Option<#parse_ty> = match __raw {
12533 ::core::option::Option::None => ::core::option::Option::None,
12534 ::core::option::Option::Some(__s) if __s.is_empty() => {
12535 ::core::option::Option::None
12536 }
12537 ::core::option::Option::Some(__s) => {
12538 match __s.parse::<#parse_ty>() {
12539 ::core::result::Result::Ok(__n) => {
12540 ::core::option::Option::Some(__n)
12541 }
12542 ::core::result::Result::Err(__e) => {
12543 __errors.add(
12544 #name_lit,
12545 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12546 );
12547 ::core::option::Option::None
12548 }
12549 }
12550 }
12551 };
12552 }
12553 } else {
12554 quote! {
12555 let __v: #parse_ty = match __raw {
12556 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12557 match __s.parse::<#parse_ty>() {
12558 ::core::result::Result::Ok(__n) => __n,
12559 ::core::result::Result::Err(__e) => {
12560 __errors.add(
12561 #name_lit,
12562 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12563 );
12564 #default_val
12565 }
12566 }
12567 }
12568 _ => {
12569 __errors.add(#name_lit, "This field is required.");
12570 #default_val
12571 }
12572 };
12573 }
12574 }
12575 }
12576 };
12577
12578 let validators = render_form_validators(name_lit, kind, nullable, attrs);
12579
12580 quote! {
12581 // `mut` so the per-field `clean` hook (#372) can rewrite the
12582 // parsed value in-place when it returns Ok with a normalized
12583 // form (e.g. trim / lowercase).
12584 let mut #ident = {
12585 #lookup
12586 #parsed_value
12587 #validators
12588 __v
12589 };
12590 }
12591}
12592
12593fn render_form_validators(
12594 name_lit: &str,
12595 kind: FormFieldKind,
12596 nullable: bool,
12597 attrs: &FormFieldAttrs,
12598) -> TokenStream2 {
12599 let mut checks: Vec<TokenStream2> = Vec::new();
12600
12601 let val_ref = if nullable {
12602 quote! { __v.as_ref() }
12603 } else {
12604 quote! { ::core::option::Option::Some(&__v) }
12605 };
12606
12607 let is_string = matches!(kind, FormFieldKind::String);
12608 let is_numeric = matches!(
12609 kind,
12610 FormFieldKind::I16
12611 | FormFieldKind::I32
12612 | FormFieldKind::I64
12613 | FormFieldKind::F32
12614 | FormFieldKind::F64
12615 );
12616
12617 if is_string {
12618 if let Some(min_len) = attrs.min_length {
12619 let min_len_usize = min_len as usize;
12620 checks.push(quote! {
12621 if let ::core::option::Option::Some(__s) = #val_ref {
12622 if __s.chars().count() < #min_len_usize {
12623 __errors.add(
12624 #name_lit,
12625 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12626 );
12627 }
12628 }
12629 });
12630 }
12631 if let Some(max_len) = attrs.max_length {
12632 let max_len_usize = max_len as usize;
12633 checks.push(quote! {
12634 if let ::core::option::Option::Some(__s) = #val_ref {
12635 if __s.chars().count() > #max_len_usize {
12636 __errors.add(
12637 #name_lit,
12638 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12639 );
12640 }
12641 }
12642 });
12643 }
12644 }
12645
12646 if is_numeric {
12647 if let Some(min) = attrs.min {
12648 checks.push(quote! {
12649 if let ::core::option::Option::Some(__n) = #val_ref {
12650 if (*__n as f64) < (#min as f64) {
12651 __errors.add(
12652 #name_lit,
12653 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12654 );
12655 }
12656 }
12657 });
12658 }
12659 if let Some(max) = attrs.max {
12660 checks.push(quote! {
12661 if let ::core::option::Option::Some(__n) = #val_ref {
12662 if (*__n as f64) > (#max as f64) {
12663 __errors.add(
12664 #name_lit,
12665 ::std::format!("Ensure this value is less than or equal to {}.", #max),
12666 );
12667 }
12668 }
12669 });
12670 }
12671 }
12672
12673 quote! { #( #checks )* }
12674}
12675
12676// ============================================================
12677// #[derive(ViewSet)]
12678// ============================================================
12679
12680struct ViewSetAttrs {
12681 model: syn::Path,
12682 fields: Option<Vec<String>>,
12683 filter_fields: Vec<String>,
12684 search_fields: Vec<String>,
12685 /// (field_name, desc)
12686 ordering: Vec<(String, bool)>,
12687 page_size: Option<usize>,
12688 read_only: bool,
12689 allow_anonymous: bool,
12690 perms: ViewSetPermsAttrs,
12691 /// `#[viewset(serializer = SomeSerializer)]` — render list /
12692 /// retrieve / create responses through this `#[derive(Serializer)]`
12693 /// type instead of the default field-level projection (requires the
12694 /// `serializer` feature). Tri-dialect.
12695 serializer: Option<syn::Path>,
12696}
12697
12698#[derive(Default)]
12699struct ViewSetPermsAttrs {
12700 list: Vec<String>,
12701 retrieve: Vec<String>,
12702 create: Vec<String>,
12703 update: Vec<String>,
12704 destroy: Vec<String>,
12705}
12706
12707fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12708 let root = rustango_root();
12709 let struct_name = &input.ident;
12710
12711 // Must be a unit struct or an empty named struct.
12712 match &input.data {
12713 Data::Struct(s) => match &s.fields {
12714 Fields::Unit | Fields::Named(_) => {}
12715 Fields::Unnamed(_) => {
12716 return Err(syn::Error::new_spanned(
12717 struct_name,
12718 "ViewSet can only be derived on a unit struct or an empty named struct",
12719 ));
12720 }
12721 },
12722 _ => {
12723 return Err(syn::Error::new_spanned(
12724 struct_name,
12725 "ViewSet can only be derived on a struct",
12726 ));
12727 }
12728 }
12729
12730 let attrs = parse_viewset_attrs(input)?;
12731 let model_path = &attrs.model;
12732
12733 // `.fields(&[...])` call — None means skip (use all scalar fields).
12734 let fields_call = if let Some(ref fields) = attrs.fields {
12735 let lits = fields.iter().map(|f| f.as_str());
12736 quote!(.fields(&[ #(#lits),* ]))
12737 } else {
12738 quote!()
12739 };
12740
12741 let filter_fields_call = if attrs.filter_fields.is_empty() {
12742 quote!()
12743 } else {
12744 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12745 quote!(.filter_fields(&[ #(#lits),* ]))
12746 };
12747
12748 let search_fields_call = if attrs.search_fields.is_empty() {
12749 quote!()
12750 } else {
12751 let lits = attrs.search_fields.iter().map(|f| f.as_str());
12752 quote!(.search_fields(&[ #(#lits),* ]))
12753 };
12754
12755 let ordering_call = if attrs.ordering.is_empty() {
12756 quote!()
12757 } else {
12758 let pairs = attrs.ordering.iter().map(|(f, desc)| {
12759 let f = f.as_str();
12760 quote!((#f, #desc))
12761 });
12762 quote!(.ordering(&[ #(#pairs),* ]))
12763 };
12764
12765 let page_size_call = if let Some(n) = attrs.page_size {
12766 quote!(.page_size(#n))
12767 } else {
12768 quote!()
12769 };
12770
12771 let read_only_call = if attrs.read_only {
12772 quote!(.read_only())
12773 } else {
12774 quote!()
12775 };
12776 let allow_anonymous_call = if attrs.allow_anonymous {
12777 quote!(.allow_anonymous())
12778 } else {
12779 quote!()
12780 };
12781
12782 // `.serializer::<S>()` — reshape responses through a derived
12783 // serializer. Requires the downstream crate to enable the
12784 // `serializer` feature (the method is gated on it).
12785 let serializer_call = if let Some(ref ser) = attrs.serializer {
12786 quote!(.serializer::<#ser>())
12787 } else {
12788 quote!()
12789 };
12790
12791 let perms = &attrs.perms;
12792 let perms_call = if perms.list.is_empty()
12793 && perms.retrieve.is_empty()
12794 && perms.create.is_empty()
12795 && perms.update.is_empty()
12796 && perms.destroy.is_empty()
12797 {
12798 quote!()
12799 } else {
12800 let list_lits = perms.list.iter().map(|s| s.as_str());
12801 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12802 let create_lits = perms.create.iter().map(|s| s.as_str());
12803 let update_lits = perms.update.iter().map(|s| s.as_str());
12804 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12805 quote! {
12806 .permissions(#root::viewset::ViewSetPerms {
12807 list: ::std::vec![ #(#list_lits.to_owned()),* ],
12808 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12809 create: ::std::vec![ #(#create_lits.to_owned()),* ],
12810 update: ::std::vec![ #(#update_lits.to_owned()),* ],
12811 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
12812 })
12813 }
12814 };
12815
12816 Ok(quote! {
12817 impl #struct_name {
12818 /// Build an `axum::Router` with the six standard REST endpoints
12819 /// for this ViewSet, mounted at `prefix`.
12820 ///
12821 /// Accepts any backend's pool — a `sqlx::PgPool`,
12822 /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
12823 /// enum — because they all `Into<Pool>` (#1273). This is why
12824 /// the derive no longer names `PgPool`, which would not
12825 /// exist on a project built without the `postgres` feature.
12826 pub fn router(
12827 prefix: &str,
12828 pool: impl ::core::convert::Into<#root::sql::Pool>,
12829 ) -> #root::__axum::Router {
12830 #root::viewset::ViewSet::for_model(
12831 <#model_path as #root::core::Model>::SCHEMA
12832 )
12833 #fields_call
12834 #filter_fields_call
12835 #search_fields_call
12836 #ordering_call
12837 #page_size_call
12838 #perms_call
12839 #read_only_call
12840 #allow_anonymous_call
12841 #serializer_call
12842 .router_pool(prefix, pool.into())
12843 }
12844 }
12845 })
12846}
12847
12848fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
12849 let mut model: Option<syn::Path> = None;
12850 let mut fields: Option<Vec<String>> = None;
12851 let mut filter_fields: Vec<String> = Vec::new();
12852 let mut search_fields: Vec<String> = Vec::new();
12853 let mut ordering: Vec<(String, bool)> = Vec::new();
12854 let mut page_size: Option<usize> = None;
12855 let mut read_only = false;
12856 let mut allow_anonymous = false;
12857 let mut perms = ViewSetPermsAttrs::default();
12858 let mut serializer: Option<syn::Path> = None;
12859
12860 for attr in &input.attrs {
12861 if !attr.path().is_ident("viewset") {
12862 continue;
12863 }
12864 attr.parse_nested_meta(|meta| {
12865 if meta.path.is_ident("model") {
12866 let path: syn::Path = meta.value()?.parse()?;
12867 model = Some(path);
12868 return Ok(());
12869 }
12870 if meta.path.is_ident("serializer") {
12871 let path: syn::Path = meta.value()?.parse()?;
12872 serializer = Some(path);
12873 return Ok(());
12874 }
12875 if meta.path.is_ident("fields") {
12876 let s: LitStr = meta.value()?.parse()?;
12877 fields = Some(split_field_list(&s.value()));
12878 return Ok(());
12879 }
12880 if meta.path.is_ident("filter_fields") {
12881 let s: LitStr = meta.value()?.parse()?;
12882 filter_fields = split_field_list(&s.value());
12883 return Ok(());
12884 }
12885 if meta.path.is_ident("search_fields") {
12886 let s: LitStr = meta.value()?.parse()?;
12887 search_fields = split_field_list(&s.value());
12888 return Ok(());
12889 }
12890 if meta.path.is_ident("ordering") {
12891 let s: LitStr = meta.value()?.parse()?;
12892 ordering = parse_ordering_list(&s.value());
12893 return Ok(());
12894 }
12895 if meta.path.is_ident("page_size") {
12896 let lit: syn::LitInt = meta.value()?.parse()?;
12897 page_size = Some(lit.base10_parse::<usize>()?);
12898 return Ok(());
12899 }
12900 if meta.path.is_ident("read_only") {
12901 read_only = true;
12902 return Ok(());
12903 }
12904 if meta.path.is_ident("allow_anonymous") {
12905 allow_anonymous = true;
12906 return Ok(());
12907 }
12908 if meta.path.is_ident("permissions") {
12909 meta.parse_nested_meta(|inner| {
12910 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
12911 let s: LitStr = inner.value()?.parse()?;
12912 Ok(split_field_list(&s.value()))
12913 };
12914 if inner.path.is_ident("list") {
12915 perms.list = parse_codenames(&inner)?;
12916 } else if inner.path.is_ident("retrieve") {
12917 perms.retrieve = parse_codenames(&inner)?;
12918 } else if inner.path.is_ident("create") {
12919 perms.create = parse_codenames(&inner)?;
12920 } else if inner.path.is_ident("update") {
12921 perms.update = parse_codenames(&inner)?;
12922 } else if inner.path.is_ident("destroy") {
12923 perms.destroy = parse_codenames(&inner)?;
12924 } else {
12925 return Err(inner.error(
12926 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
12927 ));
12928 }
12929 Ok(())
12930 })?;
12931 return Ok(());
12932 }
12933 Err(meta.error(
12934 "unknown viewset attribute (supported: model, fields, filter_fields, \
12935 search_fields, ordering, page_size, read_only, allow_anonymous, serializer, \
12936 permissions(...))",
12937 ))
12938 })?;
12939 }
12940
12941 let model = model.ok_or_else(|| {
12942 syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
12943 })?;
12944
12945 Ok(ViewSetAttrs {
12946 model,
12947 fields,
12948 filter_fields,
12949 search_fields,
12950 ordering,
12951 page_size,
12952 read_only,
12953 allow_anonymous,
12954 perms,
12955 serializer,
12956 })
12957}
12958
12959// ============================================================ #[derive(Serializer)]
12960
12961struct SerializerContainerAttrs {
12962 model: syn::Path,
12963 /// `#[serializer(validate = "fn_name")]` on the struct — the
12964 /// cross-field validation hook (#436). The named inherent method
12965 /// must take `&self` and return
12966 /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
12967 /// `validate()` runs every per-field validator first; then calls
12968 /// the cross-field method if declared; aggregates all errors into
12969 /// one `FormErrors`.
12970 cross_validate: Option<syn::Ident>,
12971}
12972
12973#[derive(Default)]
12974struct SerializerFieldAttrs {
12975 read_only: bool,
12976 write_only: bool,
12977 source: Option<String>,
12978 skip: bool,
12979 /// `#[serializer(method = "fn_name")]` — computed field. The macro
12980 /// emits a `from_model` initializer that calls
12981 /// `Self::fn_name(&model)` and stores the return value.
12982 method: Option<String>,
12983 /// `#[serializer(validate = "fn_name")]` — per-field validator
12984 /// callable run by `Self::validate(&self)`. Must return
12985 /// `Result<(), String>`. Errors land in `FormErrors` keyed by
12986 /// the field name.
12987 validate: Option<String>,
12988 /// `#[serializer(nested)]` on a field whose type is another
12989 /// `Serializer` — the macro emits `from_model` initializer that
12990 /// reads the parent via `model.<source>.value()` then calls the
12991 /// child serializer's `from_model(parent)`. When the FK is
12992 /// unloaded the field falls back to `Default::default()` (does
12993 /// NOT panic) so a missing prefetch in prod degrades gracefully.
12994 /// Source field on the model defaults to the field name; override
12995 /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
12996 /// panic-on-unloaded behavior for tests.
12997 nested: bool,
12998 /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
12999 /// strict behavior: panic when the FK isn't loaded. Useful in
13000 /// test code where forgetting select_related must trip a hard
13001 /// failure rather than render a blank nested object.
13002 nested_strict: bool,
13003 /// `#[serializer(many = TagSerializer)]` — declare the field as
13004 /// a list of nested serializers. Field type must be `Vec<S>`
13005 /// where `S` is the inner serializer. The macro initializes the
13006 /// field to `Vec::new()` in `from_model` and emits a typed
13007 /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
13008 /// maps each model row through `S::from_model`. Auto-load isn't
13009 /// possible (the M2M / one-to-many accessor is async); callers
13010 /// fetch the children + call the setter post-from_model.
13011 many: Option<syn::Type>,
13012 /// `#[serializer(slug = "name")]` — render a relation as one of
13013 /// its fields.
13014 /// Source field on the model must be a `ForeignKey<T>`; the
13015 /// macro emits `from_model` glue that walks
13016 /// `model.<source>.value()?.<slug>` and clones it. Field type on
13017 /// the serializer is typically `String` (whatever type the slug
13018 /// column has). When the FK is unloaded the field falls back to
13019 /// `Default::default()`, same graceful-degrade contract as
13020 /// `nested`. Source defaults to the field name; override with
13021 /// `source = "..."`. v0.44.
13022 slug: Option<String>,
13023 /// `#[serializer(max_length = N)]` — caps
13024 /// the character count of a string field on write. Overrides the
13025 /// model's `max_length`; when absent the model value is inherited.
13026 max_length: Option<u64>,
13027 /// `#[serializer(min_length = N)]` — minimum character count.
13028 /// Serializer-only (the model has no `min_length` column).
13029 min_length: Option<u64>,
13030 /// `#[serializer(min = N)]` — inclusive
13031 /// integer lower bound; overrides the model's `min` when given.
13032 min: Option<i64>,
13033 /// `#[serializer(max = N)]` — inclusive
13034 /// integer upper bound; overrides the model's `max` when given.
13035 max: Option<i64>,
13036}
13037
13038fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
13039 let mut model: Option<syn::Path> = None;
13040 let mut cross_validate: Option<syn::Ident> = None;
13041 for attr in &input.attrs {
13042 if !attr.path().is_ident("serializer") {
13043 continue;
13044 }
13045 attr.parse_nested_meta(|meta| {
13046 if meta.path.is_ident("model") {
13047 let _eq: syn::Token![=] = meta.input.parse()?;
13048 model = Some(meta.input.parse()?);
13049 return Ok(());
13050 }
13051 if meta.path.is_ident("validate") {
13052 // #436 — container-level `validate = "fn_name"` for
13053 // cross-field validation. Field-level
13054 // `#[serializer(validate = "...")]` on a field is
13055 // parsed separately in `parse_serializer_field_attrs`.
13056 let s: LitStr = meta.value()?.parse()?;
13057 cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
13058 return Ok(());
13059 }
13060 Err(meta.error(
13061 "unknown serializer container attribute \
13062 (supported: `model`, `validate`)",
13063 ))
13064 })?;
13065 }
13066 let model = model.ok_or_else(|| {
13067 syn::Error::new_spanned(
13068 &input.ident,
13069 "`#[serializer(model = SomeModel)]` is required",
13070 )
13071 })?;
13072 Ok(SerializerContainerAttrs {
13073 model,
13074 cross_validate,
13075 })
13076}
13077
13078fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
13079 let mut out = SerializerFieldAttrs::default();
13080 for attr in &field.attrs {
13081 if !attr.path().is_ident("serializer") {
13082 continue;
13083 }
13084 attr.parse_nested_meta(|meta| {
13085 if meta.path.is_ident("read_only") {
13086 out.read_only = true;
13087 return Ok(());
13088 }
13089 if meta.path.is_ident("write_only") {
13090 out.write_only = true;
13091 return Ok(());
13092 }
13093 if meta.path.is_ident("skip") {
13094 out.skip = true;
13095 return Ok(());
13096 }
13097 if meta.path.is_ident("source") {
13098 let s: LitStr = meta.value()?.parse()?;
13099 out.source = Some(s.value());
13100 return Ok(());
13101 }
13102 if meta.path.is_ident("method") {
13103 let s: LitStr = meta.value()?.parse()?;
13104 out.method = Some(s.value());
13105 return Ok(());
13106 }
13107 if meta.path.is_ident("validate") {
13108 let s: LitStr = meta.value()?.parse()?;
13109 out.validate = Some(s.value());
13110 return Ok(());
13111 }
13112 if meta.path.is_ident("many") {
13113 let _eq: syn::Token![=] = meta.input.parse()?;
13114 out.many = Some(meta.input.parse()?);
13115 return Ok(());
13116 }
13117 if meta.path.is_ident("nested") {
13118 out.nested = true;
13119 // Optional strict flag inside parentheses:
13120 // #[serializer(nested(strict))]
13121 if meta.input.peek(syn::token::Paren) {
13122 meta.parse_nested_meta(|inner| {
13123 if inner.path.is_ident("strict") {
13124 out.nested_strict = true;
13125 return Ok(());
13126 }
13127 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
13128 })?;
13129 }
13130 return Ok(());
13131 }
13132 if meta.path.is_ident("slug") {
13133 let s: LitStr = meta.value()?.parse()?;
13134 out.slug = Some(s.value());
13135 return Ok(());
13136 }
13137 if meta.path.is_ident("max_length") {
13138 let lit: syn::LitInt = meta.value()?.parse()?;
13139 out.max_length = Some(lit.base10_parse::<u64>()?);
13140 return Ok(());
13141 }
13142 if meta.path.is_ident("min_length") {
13143 let lit: syn::LitInt = meta.value()?.parse()?;
13144 out.min_length = Some(lit.base10_parse::<u64>()?);
13145 return Ok(());
13146 }
13147 if meta.path.is_ident("min") {
13148 let lit: syn::LitInt = meta.value()?.parse()?;
13149 out.min = Some(lit.base10_parse::<i64>()?);
13150 return Ok(());
13151 }
13152 if meta.path.is_ident("max") {
13153 let lit: syn::LitInt = meta.value()?.parse()?;
13154 out.max = Some(lit.base10_parse::<i64>()?);
13155 return Ok(());
13156 }
13157 Err(meta.error(
13158 "unknown serializer field attribute (supported: \
13159 `read_only`, `write_only`, `source`, `skip`, `method`, \
13160 `validate`, `nested`, `many`, `slug`, `max_length`, \
13161 `min_length`, `min`, `max`)",
13162 ))
13163 })?;
13164 }
13165 // Validate: read_only + write_only is nonsensical
13166 if out.read_only && out.write_only {
13167 return Err(syn::Error::new_spanned(
13168 field,
13169 "a field cannot be both `read_only` and `write_only`",
13170 ));
13171 }
13172 if out.method.is_some() && out.source.is_some() {
13173 return Err(syn::Error::new_spanned(
13174 field,
13175 "`method` and `source` are mutually exclusive — `method` computes \
13176 the value from a method, `source` reads it from a different model field",
13177 ));
13178 }
13179 if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
13180 return Err(syn::Error::new_spanned(
13181 field,
13182 "`slug` is mutually exclusive with `method`, `nested`, and `many` \
13183 — pick one strategy for populating the field",
13184 ));
13185 }
13186 Ok(out)
13187}
13188
13189fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
13190 let root = rustango_root();
13191 let struct_name = &input.ident;
13192 let struct_name_lit = struct_name.to_string();
13193
13194 let Data::Struct(data) = &input.data else {
13195 return Err(syn::Error::new_spanned(
13196 struct_name,
13197 "Serializer can only be derived on structs",
13198 ));
13199 };
13200 let Fields::Named(named) = &data.fields else {
13201 return Err(syn::Error::new_spanned(
13202 struct_name,
13203 "Serializer requires a struct with named fields",
13204 ));
13205 };
13206
13207 let container = parse_serializer_container_attrs(input)?;
13208 let model_path = &container.model;
13209
13210 // Classify each field. `ty` is only consumed by the
13211 // `#[cfg(feature = "openapi")]` block below, but we always
13212 // capture it to keep the field-info build a single pass.
13213 #[allow(dead_code)]
13214 struct FieldInfo {
13215 ident: syn::Ident,
13216 ty: syn::Type,
13217 attrs: SerializerFieldAttrs,
13218 }
13219 let mut fields_info: Vec<FieldInfo> = Vec::new();
13220 for field in &named.named {
13221 let ident = field.ident.clone().expect("named field has ident");
13222 let attrs = parse_serializer_field_attrs(field)?;
13223 fields_info.push(FieldInfo {
13224 ident,
13225 ty: field.ty.clone(),
13226 attrs,
13227 });
13228 }
13229
13230 // Generate from_model body: struct literal with each field assigned.
13231 let from_model_fields = fields_info.iter().map(|fi| {
13232 let ident = &fi.ident;
13233 if let Some(_inner) = &fi.attrs.many {
13234 // Many — collection field. Initialize empty; caller
13235 // populates via the macro-emitted set_<field> helper
13236 // after fetching the M2M children.
13237 quote! { #ident: ::std::vec::Vec::new() }
13238 } else if let Some(method) = &fi.attrs.method {
13239 // SerializerMethodField: call Self::<method>(&model) to
13240 // compute the value. Method signature must be
13241 // `fn <method>(model: &T) -> <field type>`.
13242 let method_ident = syn::Ident::new(method, ident.span());
13243 quote! { #ident: Self::#method_ident(model) }
13244 } else if let Some(slug_field) = &fi.attrs.slug {
13245 // v0.44 — SlugRelatedField. Source defaults to the field
13246 // name on this struct; override via `source = "..."`. The
13247 // source field on the model is expected to be a
13248 // `ForeignKey<T>`; the slug field on the parent is named
13249 // by the attribute value. When the FK is unloaded the
13250 // field falls back to `Default::default()` — same
13251 // graceful-degrade contract as `nested`; a NULL
13252 // `Option<ForeignKey>` renders `None`.
13253 let src_name = fi
13254 .attrs
13255 .source
13256 .as_deref()
13257 .unwrap_or(&ident_name(&fi.ident))
13258 .to_owned();
13259 let src_ident = ident_from_name(&src_name, ident.span());
13260 let slug_ident = syn::Ident::new(slug_field, ident.span());
13261 quote! {
13262 #ident: #root::serializer::LoadedRelation::render(
13263 &model.#src_ident,
13264 |__loaded| ::core::clone::Clone::clone(&__loaded.#slug_ident),
13265 ::core::default::Default::default,
13266 )
13267 }
13268 } else if fi.attrs.nested {
13269 // Nested serializer. Source defaults to the field name on
13270 // this struct; override via `source = "..."`. The source
13271 // field on the model is a `ForeignKey<T>` or
13272 // `Option<ForeignKey<T>>`, read through `LoadedRelation`.
13273 //
13274 // Behavior matrix (tweakable per-field):
13275 // * FK loaded → nested object materializes via
13276 // ChildSerializer::from_model(parent).
13277 // * FK unloaded → fall back to ChildSerializer::default()
13278 // (so prod doesn't crash on a missing
13279 // prefetch — just renders a blank nested
13280 // object). Add `#[serializer(nested(
13281 // strict))]` to keep the v0.18.1
13282 // panic-on-unloaded behavior for tests
13283 // that want hard guardrails.
13284 // * NULL `Option<ForeignKey>` → `None` in an `Option<Child>`
13285 // field (JSON null), strict or not.
13286 let src_name = fi.attrs.source.as_deref().unwrap_or(&ident_name(&fi.ident)).to_owned();
13287 let src_ident = ident_from_name(&src_name, ident.span());
13288 let unloaded = if fi.attrs.nested_strict {
13289 let panic_msg = format!(
13290 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
13291 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
13292 );
13293 quote! { || ::core::panic!("{}", #panic_msg) }
13294 } else {
13295 quote! { ::core::default::Default::default }
13296 };
13297 quote! {
13298 #ident: #root::serializer::LoadedRelation::render(
13299 &model.#src_ident,
13300 |__loaded| #root::serializer::ModelSerializer::from_model(__loaded),
13301 #unloaded,
13302 )
13303 }
13304 } else if fi.attrs.write_only || fi.attrs.skip {
13305 // Not read from model — use default
13306 quote! { #ident: ::core::default::Default::default() }
13307 } else if let Some(src) = &fi.attrs.source {
13308 let src_ident = ident_from_name(src, ident.span());
13309 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13310 } else {
13311 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13312 }
13313 });
13314
13315 // is_writable predicate (also used by writable_lits /
13316 // writable_source_lits below).
13317 let is_writable = |fi: &&FieldInfo| {
13318 !fi.attrs.read_only
13319 && !fi.attrs.skip
13320 && fi.attrs.method.is_none()
13321 && !fi.attrs.nested
13322 && fi.attrs.many.is_none()
13323 && fi.attrs.slug.is_none()
13324 };
13325
13326 // Declarative field constraints: one block
13327 // per writable field, run inside `validate()`. Each resolves its
13328 // bounds as the serializer attr when given (`#[serializer(max_length
13329 // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13330 // `max` / `choices`), then dispatches on the field's JSON value via
13331 // `forms::validators::check_value` (string → length/choices, integer
13332 // → min/max).
13333 let opt_usize = |v: Option<u64>| match v {
13334 Some(n) => {
13335 let n = n as usize;
13336 quote!(::core::option::Option::Some(#n))
13337 }
13338 None => quote!(::core::option::Option::None),
13339 };
13340 let opt_i64 = |v: Option<i64>| match v {
13341 Some(n) => quote!(::core::option::Option::Some(#n)),
13342 None => quote!(::core::option::Option::None),
13343 };
13344 let constraint_blocks: Vec<_> = fields_info
13345 .iter()
13346 .filter(is_writable)
13347 .map(|fi| {
13348 let ident = &fi.ident;
13349 let fname = ident_name(ident);
13350 let mname = fi
13351 .attrs
13352 .source
13353 .clone()
13354 .unwrap_or_else(|| ident_name(&fi.ident));
13355 let attr_max_len = opt_usize(fi.attrs.max_length);
13356 let attr_min_len = opt_usize(fi.attrs.min_length);
13357 let attr_min = opt_i64(fi.attrs.min);
13358 let attr_max = opt_i64(fi.attrs.max);
13359 // A bound the field's type cannot take is a compile error, not a no-op.
13360 let ty = &fi.ty;
13361 let len_check = (fi.attrs.max_length.is_some() || fi.attrs.min_length.is_some())
13362 .then(|| quote! { #root::forms::validators::assert_length_bounded::<#ty>(); });
13363 let num_check = (fi.attrs.min.is_some() || fi.attrs.max.is_some())
13364 .then(|| quote! { #root::forms::validators::assert_number_bounded::<#ty>(); });
13365 quote! {
13366 #len_check
13367 #num_check
13368 if __sent(#fname) {
13369 let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13370 let __max_length: ::core::option::Option<usize> = #attr_max_len
13371 .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13372 let __min_length: ::core::option::Option<usize> = #attr_min_len;
13373 let __min: ::core::option::Option<i64> =
13374 #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13375 let __max: ::core::option::Option<i64> =
13376 #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13377 let __choices = __sf.and_then(|__f| __f.choices);
13378 let __v = #root::__serde_json::to_value(&self.#ident)
13379 .unwrap_or(#root::__serde_json::Value::Null);
13380 #root::forms::validators::check_value(
13381 #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13382 &mut __errors,
13383 );
13384 }
13385 }
13386 })
13387 .collect();
13388 let has_constraints = !constraint_blocks.is_empty();
13389
13390 // Per-field validators. Emit a
13391 // `validate(&self)` method that runs each user-defined validator
13392 // and aggregates errors into `FormErrors`.
13393 let validator_calls: Vec<_> = fields_info
13394 .iter()
13395 .filter_map(|fi| {
13396 let ident = &fi.ident;
13397 let name_lit = ident_name(ident);
13398 let method = fi.attrs.validate.as_ref()?;
13399 let method_ident = syn::Ident::new(method, ident.span());
13400 Some(quote! {
13401 if __sent(#name_lit) {
13402 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13403 __errors.add(#name_lit.to_owned(), __e);
13404 }
13405 }
13406 })
13407 })
13408 .collect();
13409 // #436 — cross-field `validate(&self)`. If the
13410 // container declared `#[serializer(validate = "fn_name")]`,
13411 // the macro-generated `validate(&self)` runs every per-field
13412 // validator first, then calls the user's cross-field method,
13413 // merging its `FormErrors` into the per-field errors. Either
13414 // alone is enough to emit the wrapper.
13415 let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13416 quote! {
13417 // Merge cross-field errors into the per-field bucket so
13418 // a single .validate() call surfaces both layers.
13419 if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13420 __errors.merge(__cross);
13421 }
13422 }
13423 });
13424 let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13425 // Anything to run? Declarative constraints (for any writable field) +
13426 // per-field validators + cross-field hook.
13427 let has_run_validations = has_validators || has_constraints;
13428 // Shared body: declarative field constraints first (length / range /
13429 // choices, serializer-attr-or-model), then per-field validators, then
13430 // the cross-field hook. Field rules run only where `__sent(name)` (#1995).
13431 let checks = quote! {
13432 let mut __errors = #root::forms::FormErrors::default();
13433 #( #constraint_blocks )*
13434 #( #validator_calls )*
13435 #cross_validate_call
13436 if __errors.is_empty() {
13437 ::core::result::Result::Ok(())
13438 } else {
13439 ::core::result::Result::Err(__errors)
13440 }
13441 };
13442 let validate_body = quote! {
13443 let __sent = |_: &str| true;
13444 #checks
13445 };
13446 // PATCH: only the keys the body carries.
13447 let validate_patch_body = quote! {
13448 let __sent = |__n: &str| __obj.is_some_and(|__o| __o.contains_key(__n));
13449 #checks
13450 };
13451 // Inherent `validate(&self)` — kept for back-compat with direct
13452 // `serializer.validate()` calls that don't import `ModelSerializer`.
13453 // Emitted only when the serializer declares per-field/cross-field
13454 // validators (unchanged rule) so it never collides with a
13455 // hand-written inherent `validate`.
13456 let validate_method = if has_validators {
13457 quote! {
13458 impl #struct_name {
13459 /// Run the declarative field constraints, every
13460 /// `#[serializer(validate = "...")]` per-field validator,
13461 /// and (when declared) the container-level cross-field
13462 /// validator. Aggregates errors into `FormErrors` keyed by
13463 /// the field name. Returns `Ok(())` when all pass.
13464 pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13465 #validate_body
13466 }
13467 }
13468 }
13469 } else {
13470 quote! {}
13471 };
13472 // Trait-method override so the type-erased ViewSet write path can
13473 // dispatch `ModelSerializer::validate` generically. Emitted whenever
13474 // there's anything to run (declarative constraints inherited from the
13475 // model count); otherwise the trait's default no-op is used.
13476 let trait_validate_override = if has_run_validations {
13477 quote! {
13478 fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13479 #validate_body
13480 }
13481 }
13482 } else {
13483 quote! {}
13484 };
13485
13486 // For every `#[serializer(many = S)]` field, emit a
13487 // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13488 // helper that maps the parents through `S::from_model`.
13489 let many_setters: Vec<_> = fields_info
13490 .iter()
13491 .filter_map(|fi| {
13492 let many_ty = fi.attrs.many.as_ref()?;
13493 let ident = &fi.ident;
13494 let setter = syn::Ident::new(&format!("set_{}", ident_name(ident)), ident.span());
13495 Some(quote! {
13496 /// Populate this `many` field by mapping each parent model
13497 /// through the inner serializer's `from_model`. Call after
13498 /// fetching the M2M / one-to-many children since
13499 /// `from_model` itself can't await an SQL query.
13500 pub fn #setter(
13501 &mut self,
13502 models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13503 ) -> &mut Self {
13504 self.#ident = models.iter()
13505 .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13506 .collect();
13507 self
13508 }
13509 })
13510 })
13511 .collect();
13512 let many_setters_impl = if many_setters.is_empty() {
13513 quote! {}
13514 } else {
13515 quote! {
13516 impl #struct_name {
13517 #( #many_setters )*
13518 }
13519 }
13520 };
13521
13522 // Generate custom Serialize: skip write_only fields
13523 let output_fields: Vec<_> = fields_info
13524 .iter()
13525 .filter(|fi| !fi.attrs.write_only)
13526 .collect();
13527 let output_field_count = output_fields.len();
13528 let serialize_fields = output_fields.iter().map(|fi| {
13529 let ident = &fi.ident;
13530 let name_lit = ident_name(ident);
13531 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13532 });
13533
13534 // writable_fields: normal + write_only.
13535 // Exclude:
13536 // - `read_only` — server-computed.
13537 // - `skip` — caller sets manually post-from_model.
13538 // - `method` — computed from a Self::fn(&model) call; accepting
13539 // it on write is meaningless.
13540 // - `nested` / `many` — populated from related-model data, not
13541 // from a field on the wire body.
13542 // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13543 // `many` in `writable_fields()`, which made the ViewSet write
13544 // path accept those fields from the JSON body and try to bind
13545 // them to the SQL UPDATE — a silent no-op at best, a type
13546 // mismatch at worst.
13547 // (`is_writable` is defined above, near the constraint codegen.)
13548 let writable_lits: Vec<_> = fields_info
13549 .iter()
13550 .filter(is_writable)
13551 .map(|fi| ident_name(&fi.ident))
13552 .collect();
13553
13554 // `writable_source_fields`: the MODEL field names of writable
13555 // serializer fields (source-resolved). The ViewSet write path skips
13556 // every model column NOT in this set, so `read_only` / `method` /
13557 // computed fields a client posts are ignored instead of written.
13558 let writable_source_lits: Vec<String> = fields_info
13559 .iter()
13560 .filter(is_writable)
13561 .map(|fi| {
13562 fi.attrs
13563 .source
13564 .clone()
13565 .unwrap_or_else(|| ident_name(&fi.ident))
13566 })
13567 .collect();
13568
13569 // `readable_source_fields`: model columns the JSON output copies
13570 // straight from the model, so `?ordering=` cannot sort on a column
13571 // the API never shows (#1845).
13572 let readable_source_lits: Vec<String> = fields_info
13573 .iter()
13574 .filter(|fi| {
13575 !fi.attrs.write_only
13576 && !fi.attrs.skip
13577 && fi.attrs.method.is_none()
13578 && !fi.attrs.nested
13579 && fi.attrs.many.is_none()
13580 && fi.attrs.slug.is_none()
13581 })
13582 .map(|fi| {
13583 fi.attrs
13584 .source
13585 .clone()
13586 .unwrap_or_else(|| ident_name(&fi.ident))
13587 })
13588 .collect();
13589
13590 // `from_writable_json`: build a partial instance for input
13591 // validation. Writable fields are parsed from the JSON body (keyed
13592 // by serializer field name); every other field defaults. Per-field
13593 // type errors collect into `FormErrors` keyed by the field name.
13594 // Construction is field-by-field (not `Self::default()`) so it works
13595 // for serializers regardless of a struct-level `Default`.
13596 let from_writable_json_inits: Vec<_> = fields_info
13597 .iter()
13598 .map(|fi| {
13599 let ident = &fi.ident;
13600 let fname = ident_name(ident);
13601 let ty = &fi.ty;
13602 if is_writable(&fi) {
13603 quote! {
13604 #ident: match __obj.and_then(|__o| __o.get(#fname)) {
13605 ::core::option::Option::Some(__v) => {
13606 match #root::__serde_json::from_value::<#ty>(
13607 ::core::clone::Clone::clone(__v),
13608 ) {
13609 ::core::result::Result::Ok(__x) => __x,
13610 ::core::result::Result::Err(__e) => {
13611 __errors.add(#fname.to_owned(), __e.to_string());
13612 ::core::default::Default::default()
13613 }
13614 }
13615 }
13616 ::core::option::Option::None => ::core::default::Default::default(),
13617 }
13618 }
13619 } else {
13620 quote! { #ident: ::core::default::Default::default() }
13621 }
13622 })
13623 .collect();
13624 // `validate_patch`: overlay each sent writable field on the stored row.
13625 let patch_overlays: Vec<_> = fields_info
13626 .iter()
13627 .filter(is_writable)
13628 .map(|fi| {
13629 let ident = &fi.ident;
13630 let fname = ident_name(ident);
13631 let ty = &fi.ty;
13632 quote! {
13633 if let ::core::option::Option::Some(__v) = __obj.and_then(|__o| __o.get(#fname)) {
13634 match #root::__serde_json::from_value::<#ty>(::core::clone::Clone::clone(__v)) {
13635 ::core::result::Result::Ok(__x) => __merged.#ident = __x,
13636 ::core::result::Result::Err(__e) => {
13637 __errors.add(#fname.to_owned(), __e.to_string());
13638 }
13639 }
13640 }
13641 }
13642 })
13643 .collect();
13644 let patch_validate_call = if has_run_validations {
13645 quote! { __merged.__rustango_validate_patch(__obj) }
13646 } else {
13647 quote! { let _ = &__merged; ::core::result::Result::Ok(()) }
13648 };
13649 let patch_validate_helper = if has_run_validations {
13650 quote! {
13651 impl #struct_name {
13652 #[doc(hidden)]
13653 fn __rustango_validate_patch(
13654 &self,
13655 __obj: ::core::option::Option<&#root::__serde_json::Map<::std::string::String, #root::__serde_json::Value>>,
13656 ) -> ::core::result::Result<(), #root::forms::FormErrors> {
13657 #validate_patch_body
13658 }
13659 }
13660 }
13661 } else {
13662 quote! {}
13663 };
13664
13665 // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
13666 // Only includes fields shown in JSON output (skips write_only). For each
13667 // `Option<T>` field, omit from `required` and add `.nullable()`.
13668 let openapi_impl = {
13669 #[cfg(feature = "openapi")]
13670 {
13671 let property_calls = output_fields.iter().map(|fi| {
13672 let ident = &fi.ident;
13673 let name_lit = ident_name(ident);
13674 let ty = &fi.ty;
13675 let nullable_call = if is_option(ty) {
13676 quote! { .nullable() }
13677 } else {
13678 quote! {}
13679 };
13680 quote! {
13681 .property(
13682 #name_lit,
13683 <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13684 #nullable_call,
13685 )
13686 }
13687 });
13688 let required_lits: Vec<_> = output_fields
13689 .iter()
13690 .filter(|fi| !is_option(&fi.ty))
13691 .map(|fi| ident_name(&fi.ident))
13692 .collect();
13693 quote! {
13694 impl #root::openapi::OpenApiSchema for #struct_name {
13695 fn openapi_schema() -> #root::openapi::Schema {
13696 #root::openapi::Schema::object()
13697 #( #property_calls )*
13698 .required([ #( #required_lits ),* ])
13699 }
13700 }
13701 }
13702 }
13703 #[cfg(not(feature = "openapi"))]
13704 {
13705 quote! {}
13706 }
13707 };
13708
13709 Ok(quote! {
13710 impl #root::serializer::ModelSerializer for #struct_name {
13711 type Model = #model_path;
13712
13713 fn from_model(model: &Self::Model) -> Self {
13714 Self {
13715 #( #from_model_fields ),*
13716 }
13717 }
13718
13719 fn writable_fields() -> &'static [&'static str] {
13720 &[ #( #writable_lits ),* ]
13721 }
13722
13723 fn writable_source_fields() -> &'static [&'static str] {
13724 &[ #( #writable_source_lits ),* ]
13725 }
13726
13727 fn readable_source_fields() -> &'static [&'static str] {
13728 &[ #( #readable_source_lits ),* ]
13729 }
13730
13731 fn from_writable_json(
13732 __body: &#root::__serde_json::Value,
13733 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
13734 let mut __errors = #root::forms::FormErrors::default();
13735 let __obj = __body.as_object();
13736 let __out = Self {
13737 #( #from_writable_json_inits ),*
13738 };
13739 if __errors.is_empty() {
13740 ::core::result::Result::Ok(__out)
13741 } else {
13742 ::core::result::Result::Err(__errors)
13743 }
13744 }
13745
13746 fn validate_patch(
13747 __stored: &Self::Model,
13748 __body: &#root::__serde_json::Value,
13749 ) -> ::core::result::Result<(), #root::forms::FormErrors> {
13750 let mut __errors = #root::forms::FormErrors::default();
13751 let __obj = __body.as_object();
13752 #[allow(unused_mut)]
13753 let mut __merged = <Self as #root::serializer::ModelSerializer>::from_model(__stored);
13754 #( #patch_overlays )*
13755 if !__errors.is_empty() {
13756 return ::core::result::Result::Err(__errors);
13757 }
13758 #patch_validate_call
13759 }
13760
13761 #trait_validate_override
13762 }
13763
13764 #patch_validate_helper
13765
13766 impl #root::__serde::Serialize for #struct_name {
13767 fn serialize<S>(&self, serializer: S)
13768 -> ::core::result::Result<S::Ok, S::Error>
13769 where
13770 S: #root::__serde::Serializer,
13771 {
13772 use #root::__serde::ser::SerializeStruct;
13773 let mut __state = serializer.serialize_struct(
13774 #struct_name_lit,
13775 #output_field_count,
13776 )?;
13777 #( #serialize_fields )*
13778 __state.end()
13779 }
13780 }
13781
13782 #openapi_impl
13783
13784 #validate_method
13785
13786 #many_setters_impl
13787 })
13788}
13789
13790/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
13791/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
13792/// when the feature is off.
13793#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
13794fn is_option(ty: &syn::Type) -> bool {
13795 if let syn::Type::Path(p) = ty {
13796 if let Some(last) = p.path.segments.last() {
13797 return last.ident == "Option";
13798 }
13799 }
13800 false
13801}
13802
13803#[cfg(test)]
13804mod main_attr_tests {
13805 use super::*;
13806
13807 fn expand(args: &str) -> String {
13808 let body = quote! { async fn main() { run().await } };
13809 expand_main(args.parse().unwrap(), body)
13810 .expect("expansion failed")
13811 .to_string()
13812 }
13813
13814 /// Asserts on the expansion rather than on `parse_logging`,
13815 /// because the defect was a subscriber reaching the output, not
13816 /// an argument being misread (#1465).
13817 #[test]
13818 fn logging_false_emits_no_subscriber() {
13819 let out = expand("logging = false");
13820 assert!(
13821 !out.contains("logging :: Setup"),
13822 "`logging = false` must not install a subscriber:\n{out}",
13823 );
13824 assert!(
13825 out.contains("block_on"),
13826 "the runtime must still be built:\n{out}",
13827 );
13828 }
13829
13830 #[test]
13831 fn default_still_installs_a_subscriber() {
13832 // The control. Without it, an `expand_main` that dropped the
13833 // prologue unconditionally would pass the test above while
13834 // leaving every default project with no logging at all.
13835 let out = expand("");
13836 assert!(
13837 out.contains("logging :: Setup"),
13838 "the default entrypoint must still install one:\n{out}",
13839 );
13840 }
13841
13842 #[test]
13843 fn logging_false_composes_with_flavor() {
13844 let out = expand("flavor = \"current_thread\", logging = false");
13845 assert!(!out.contains("logging :: Setup"), "{out}");
13846 assert!(out.contains("new_current_thread"), "{out}");
13847 }
13848
13849 #[test]
13850 fn logging_true_is_the_default_shape() {
13851 // `logging = true` is accepted and means the default, so the
13852 // match must not fire on the substring `logging`.
13853 assert!(parse_logging(&"logging = true".parse().unwrap()));
13854 assert!(!parse_logging(&"logging=false".parse().unwrap()));
13855 }
13856}
13857
13858#[cfg(test)]
13859mod embed_migrations_tests {
13860 use super::*;
13861
13862 fn expand(path: &str) -> String {
13863 expand_embed_migrations(quote! { #path })
13864 .expect("expansion failed")
13865 .to_string()
13866 }
13867
13868 #[test]
13869 fn missing_dir_warns() {
13870 let out = expand("./no_such_migrations_dir");
13871 assert!(out.contains("deprecated"), "missing dir must warn:\n{out}");
13872 assert!(out.contains("no_such_migrations_dir"), "{out}");
13873 }
13874
13875 #[test]
13876 fn existing_dir_does_not_warn() {
13877 let out = expand("./src");
13878 assert!(!out.contains("deprecated"), "{out}");
13879 }
13880}
13881
13882#[cfg(test)]
13883mod field_attr_tests {
13884 use super::*;
13885
13886 fn error_for(field: syn::Field) -> Option<String> {
13887 let attrs = parse_field_attrs(&field).unwrap();
13888 let kind = detect_type(&field.ty).unwrap().kind;
13889 check_bound_compatibility(&field, &attrs, kind)
13890 .err()
13891 .map(|e| e.to_string())
13892 }
13893
13894 /// `citext`, `vector` and `geometry` on the wrong type are errors, not no-ops (#1937).
13895 #[test]
13896 fn type_specific_attrs_reject_other_types() {
13897 let f: syn::FieldsNamed = syn::parse_quote!({
13898 #[rustango(citext)] a: i64,
13899 #[rustango(vector(dims = 3))] b: String,
13900 #[rustango(geometry(srid = 4326))] c: String,
13901 #[rustango(citext)] d: String,
13902 });
13903 let errs: Vec<_> = f.named.into_iter().map(error_for).collect();
13904 assert!(errs[0].as_deref().unwrap().contains("citext"), "{errs:?}");
13905 assert!(errs[1].as_deref().unwrap().contains("vector"), "{errs:?}");
13906 assert!(errs[2].as_deref().unwrap().contains("geometry"), "{errs:?}");
13907 assert_eq!(errs[3], None);
13908 }
13909
13910 /// `manager(ext)` emitted a trait nobody could add methods to; it is refused (#2132).
13911 #[test]
13912 fn manager_ext_is_refused() {
13913 let input: DeriveInput = syn::parse_quote! {
13914 #[rustango(table = "p", manager(ext = "PostManagerExt"))]
13915 struct Post { id: i64 }
13916 };
13917 let err = parse_container_attrs(&input).err().expect("refused");
13918 assert!(err.to_string().contains("#2132"), "{err}");
13919 }
13920
13921 /// `set_null` on a NOT NULL `ForeignKey<T>` would fail every parent delete.
13922 #[test]
13923 fn set_null_needs_an_optional_foreign_key() {
13924 let input: DeriveInput = syn::parse_quote! {
13925 #[rustango(table = "c")]
13926 struct Child {
13927 #[rustango(primary_key)] id: i64,
13928 #[rustango(on_delete = "set_null")] parent: ForeignKey<Parent>,
13929 }
13930 };
13931 let err = expand(&input).err().expect("refused");
13932 assert!(err.to_string().contains("set_null"), "{err}");
13933 let input: DeriveInput = syn::parse_quote! {
13934 #[rustango(table = "c")]
13935 struct Child {
13936 #[rustango(primary_key)] id: i64,
13937 #[rustango(on_delete = "set_null")] parent: Option<ForeignKey<Parent>>,
13938 }
13939 };
13940 assert!(expand(&input).is_ok());
13941 }
13942
13943 /// The same for `fk = "…"` / `o2o = "…"` on a plain column.
13944 #[test]
13945 fn set_null_needs_a_nullable_fk_column() {
13946 let input: DeriveInput = syn::parse_quote! {
13947 #[rustango(table = "c")]
13948 struct Child {
13949 #[rustango(primary_key)] id: i64,
13950 #[rustango(fk = "p", on_delete = "set_null")] parent_id: i64,
13951 #[rustango(o2o = "q", on_delete = "set_null")] other_id: Option<i64>,
13952 }
13953 };
13954 let err = expand(&input).err().expect("refused");
13955 assert!(err.to_string().contains("set_null"), "{err}");
13956 let input: DeriveInput = syn::parse_quote! {
13957 #[rustango(table = "c")]
13958 struct Child {
13959 #[rustango(primary_key)] id: i64,
13960 #[rustango(fk = "p", on_delete = "set_null")] parent_id: std::option::Option<i64>,
13961 #[rustango(o2o = "q", on_delete = "set_null")] other_id: ::core::option::Option<i64>,
13962 #[rustango(on_delete = "set_null")] parent: core::option::Option<ForeignKey<Parent>>,
13963 }
13964 };
13965 assert!(expand(&input).is_ok());
13966 }
13967
13968 /// Two FKs to one parent would emit the same reverse accessor twice.
13969 #[test]
13970 fn reverse_accessor_clash_names_related_name() {
13971 let input: DeriveInput = syn::parse_quote! {
13972 #[rustango(table = "m")]
13973 struct Message {
13974 #[rustango(primary_key)] id: i64,
13975 sender: ForeignKey<User>,
13976 recipient: ForeignKey<User>,
13977 }
13978 };
13979 let err = expand(&input).err().expect("refused");
13980 assert!(err.to_string().contains("related_name"), "{err}");
13981 }
13982
13983 /// `related_name = "+"` emits no reverse accessor, so a parent from
13984 /// another crate, which cannot take an inherent impl, still derives.
13985 #[test]
13986 fn related_name_plus_skips_the_reverse_accessor() {
13987 let input: DeriveInput = syn::parse_quote! {
13988 #[rustango(table = "m")]
13989 struct Message {
13990 #[rustango(primary_key)] id: i64,
13991 sender: ForeignKey<User>,
13992 #[rustango(related_name = "+")] recipient: ForeignKey<User>,
13993 #[rustango(related_name = "+")] owner: ForeignKey<other::Account>,
13994 }
13995 };
13996 let out = expand(&input).expect("derives").to_string();
13997 assert_eq!(out.matches("message_set_pool").count(), 1, "{out}");
13998 assert!(!out.contains("impl other :: Account {"), "{out}");
13999 }
14000
14001 /// A raw ident derives names without `r#`; this used to panic (#1937).
14002 #[test]
14003 fn raw_idents_derive_plain_names() {
14004 let ident: syn::Ident = syn::parse_quote!(r#type);
14005 assert_eq!(column_type_ident(&ident).to_string(), "type_col");
14006 assert_eq!(ident_from_name("type", ident.span()).to_string(), "r#type");
14007 }
14008}