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 as one of its fields: clones `model.<source>.value()?.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. **Caveat:** cargo
200/// doesn't watch directory listings, so adding or removing a
201/// migration file inside the dir won't auto-trigger a rebuild — run
202/// `cargo clean` (or just bump any other source file) when you add
203/// new migrations during embedded development.
204#[proc_macro]
205pub fn embed_migrations(input: TokenStream) -> TokenStream {
206 expand_embed_migrations(input.into())
207 .unwrap_or_else(syn::Error::into_compile_error)
208 .into()
209}
210
211/// `Q!()` — `Model.field__lookup = value` filter syntax, resolved at
212/// compile time against typed columns. Issue #269 / T1.7.
213///
214/// Each invocation lowers to the equivalent typed-column method call:
215///
216/// ```ignore
217/// // These expand identically:
218/// Q!(User.email__icontains = "alice")
219/// User::email.ilike("%alice%")
220/// ```
221///
222/// Field-name typos fail the build (the macro emits `User::no_such_field`
223/// which doesn't exist) — the headline ergonomic win of this slice over
224/// filters keyed by a plain string.
225///
226/// # Supported lookup suffixes
227///
228/// * bare `=` / `__exact` → `.eq(value)`
229/// * `__iexact` → `.ilike(value)` (case-insensitive equality, no wildcards)
230/// * `__ne` → `.ne(value)`
231/// * `__gt` / `__gte` / `__lt` / `__lte` → corresponding comparison
232/// * `__contains` / `__icontains` → `.like("%v%")` / `.ilike("%v%")`
233/// * `__startswith` / `__istartswith` → `.like("v%")` / `.ilike("v%")`
234/// * `__endswith` / `__iendswith` → `.like("%v")` / `.ilike("%v")`
235/// * `__in` → `.is_in(iterable)`
236/// * `__not_in` → `.not_in(iterable)`
237/// * `__isnull = true` → `.is_null()`; `__isnull = false` → `.is_not_null()`
238/// * `__between` accepts a tuple literal `(lo, hi)` → `.between(lo, hi)`
239/// * `__regex` / `__iregex` → `.regex(pattern)` / `.iregex(pattern)`
240///
241/// Unknown suffixes fail the build with a `compile_error!` pointing at
242/// the lookup token.
243///
244/// # Combine
245///
246/// Each `Q!()` returns a `TypedFilter<Model>` — chain via the existing
247/// `.and()` / `.or()` / `.not()` methods:
248///
249/// ```ignore
250/// User::objects()
251/// .where_(
252/// Q!(User.active = true)
253/// .and(Q!(User.email__icontains = "alice"))
254/// )
255/// .fetch(&pool).await?;
256/// ```
257///
258/// All emitted code routes through existing per-dialect writers — no new
259/// SQL emission machinery. Tri-dialect support is inherent.
260#[allow(non_snake_case)]
261#[proc_macro]
262pub fn Q(input: TokenStream) -> TokenStream {
263 expand_q(input.into())
264 .unwrap_or_else(syn::Error::into_compile_error)
265 .into()
266}
267
268/// `#[rustango::main]` — the runserver entrypoint. Wraps
269/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
270/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
271/// functions are zero-boilerplate:
272///
273/// ```ignore
274/// #[rustango::main]
275/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
276/// rustango::server::Builder::from_env().await?
277/// .migrate("migrations").await?
278/// .api(my_app::urls::api())
279/// .seed_with(my_app::seed::run).await?
280/// .serve("0.0.0.0:8080").await
281/// }
282/// ```
283///
284/// Optional `flavor = "current_thread"` passes through to
285/// `#[tokio::main]`; default is the multi-threaded runtime.
286///
287/// `logging = false` suppresses the default subscriber, for apps
288/// that install their own. Needed whenever the body reaches
289/// `Cli::with_logging()` or `logging::Setup`: the subscriber this
290/// macro installs gets there first, and `tracing` keeps the first
291/// one, so the `[logging]` section would otherwise be read and
292/// discarded (#1465).
293///
294/// ```ignore
295/// #[rustango::main(logging = false)]
296/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
297/// rustango::manage::Cli::new()
298/// .with_settings_from_env()
299/// .with_logging() // now the first installer
300/// .run().await
301/// }
302/// ```
303///
304/// Pulls `tracing-subscriber` into the rustango crate behind the
305/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
306/// out get plain `#[tokio::main]` ergonomics without the dependency.
307#[proc_macro_attribute]
308pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
309 expand_main(args.into(), item.into())
310 .unwrap_or_else(syn::Error::into_compile_error)
311 .into()
312}
313
314fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
315 let mut input: syn::ItemFn = syn::parse2(item)?;
316 if input.sig.asyncness.is_none() {
317 return Err(syn::Error::new(
318 input.sig.ident.span(),
319 "`#[rustango::main]` must wrap an `async fn`",
320 ));
321 }
322
323 // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
324 // to `#[tokio::main]`. Tokio's proc-macro internally emits
325 // `::tokio::*` paths that resolve against the user crate's deps,
326 // so calling it through the rustango re-export still requires the
327 // user to add tokio to their own Cargo.toml. Building the
328 // runtime ourselves keeps the dep transitive through the
329 // `runtime` feature on rustango.
330 //
331 // Parse optional `flavor = "current_thread"` / `flavor =
332 // "multi_thread"` from the attribute args. Unknown args are
333 // tolerated (forward-compat with tokio's own arg surface).
334 let root = rustango_root();
335 let flavor = parse_flavor(&args);
336 let builder_call = match flavor {
337 Flavor::CurrentThread => quote! {
338 #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
339 },
340 Flavor::MultiThread => quote! {
341 #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
342 },
343 };
344
345 // Detach the user body and rewrite `main` as a sync fn that
346 // builds the runtime and blocks on the async body.
347 let user_body = input.block.clone();
348 input.sig.asyncness = None;
349
350 // `logging = false` drops the block entirely, so a later
351 // `Cli::with_logging()` is the first installer and its
352 // `[logging]` settings actually take effect (#1465).
353 let logging_prologue = if parse_logging(&args) {
354 quote! {
355 use #root::__private_runtime::tracing_subscriber::{self, EnvFilter};
356 // Colour only when stdout is a terminal, and never under
357 // `NO_COLOR` — `Color::Auto`'s rule, called rather than
358 // copied.
359 //
360 // This was inlined at first, on a comment claiming
361 // `rustango::logging` is gated on `admin` + `tenancy`. It is
362 // not: `pub mod logging` is ungated and its contents are
363 // `runtime`-gated — the same feature that gates this macro
364 // (`pub use rustango_macros::main` is `#[cfg(feature =
365 // "runtime")]`). So wherever this expands, `Color` is
366 // reachable, and the copy was justified by a gate that does
367 // not exist. `should_colour` carries a deliberate
368 // `NO_COLOR`-set-but-empty subtlety that the inline happened
369 // to match today and nothing kept matching tomorrow.
370 //
371 // Without any of it the subscriber inherits
372 // tracing-subscriber's default, `cfg!(feature = "ansi")` —
373 // true since the framework turned that feature on — so
374 // `./app > app.log` and every container redirecting stdout
375 // collected escape codes. `Setup::install` has its own
376 // check; this is the default entrypoint and had none.
377 let __ansi = #root::logging::Color::Auto.should_colour();
378 // `try_init` so duplicate installers (e.g. tests already
379 // holding a subscriber) don't panic.
380 let _ = tracing_subscriber::fmt()
381 .with_env_filter(
382 EnvFilter::try_from_default_env()
383 .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
384 )
385 .with_ansi(__ansi)
386 .try_init();
387 }
388 } else {
389 quote! {}
390 };
391
392 input.block = syn::parse2(quote! {{
393 { #logging_prologue }
394 let __rt = #builder_call
395 .enable_all()
396 .build()
397 .expect("failed to build tokio runtime");
398 __rt.block_on(async move #user_body)
399 }})?;
400
401 Ok(quote! {
402 #input
403 })
404}
405
406enum Flavor {
407 MultiThread,
408 CurrentThread,
409}
410
411fn parse_flavor(args: &TokenStream2) -> Flavor {
412 // Cheap parser: look for the literal token sequence
413 // `flavor = "current_thread"`. Everything else (including
414 // bare `multi_thread` or no args) defaults to multi-thread.
415 let s = args.to_string();
416 if s.contains("current_thread") {
417 Flavor::CurrentThread
418 } else {
419 Flavor::MultiThread
420 }
421}
422
423/// `false` when the attribute carries `logging = false` — the caller
424/// installs their own subscriber, usually via `Cli::with_logging`
425/// (#1465). Same cheap token match as `parse_flavor`.
426fn parse_logging(args: &TokenStream2) -> bool {
427 !args.to_string().replace(' ', "").contains("logging=false")
428}
429
430/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
431struct QInput {
432 base_path: syn::Path,
433 field: syn::Ident,
434 value: syn::Expr,
435}
436
437impl syn::parse::Parse for QInput {
438 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
439 let base_path: syn::Path = input.parse()?;
440 input.parse::<syn::Token![.]>()?;
441 let field: syn::Ident = input.parse()?;
442 input.parse::<syn::Token![=]>()?;
443 let value: syn::Expr = input.parse()?;
444 Ok(QInput {
445 base_path,
446 field,
447 value,
448 })
449 }
450}
451
452fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
453 let q: QInput = syn::parse2(input)?;
454 let root = rustango_root();
455 let field_str = q.field.to_string();
456 let field_span = q.field.span();
457 let (base, suffix) = match field_str.find("__") {
458 Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
459 None => (field_str.as_str(), ""),
460 };
461 if base.is_empty() {
462 return Err(syn::Error::new(
463 field_span,
464 "Q!(): field name is empty before `__` suffix",
465 ));
466 }
467 let base_ident = syn::Ident::new(base, field_span);
468 let value = &q.value;
469 let path = &q.base_path;
470
471 // Most suffixes map directly to a Column method with the value
472 // forwarded unchanged. Some need value-shape massaging (wildcards
473 // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
474 // ISNULL). Unknown suffixes fail the build.
475 let expanded = match suffix {
476 "" | "exact" => quote! {
477 #root::core::Column::eq(#path::#base_ident, #value)
478 },
479 "ne" => quote! {
480 #root::core::Column::ne(#path::#base_ident, #value)
481 },
482 "gt" => quote! {
483 #root::core::Column::gt(#path::#base_ident, #value)
484 },
485 "gte" => quote! {
486 #root::core::Column::gte(#path::#base_ident, #value)
487 },
488 "lt" => quote! {
489 #root::core::Column::lt(#path::#base_ident, #value)
490 },
491 "lte" => quote! {
492 #root::core::Column::lte(#path::#base_ident, #value)
493 },
494 // The escaped-LIKE lookups delegate to the typed Column
495 // methods, which escape the value and pair it with the
496 // *Escaped ops so `%` / `_` match literally on every dialect
497 // (#1257) — the macro no longer owns any pattern wrapping.
498 "iexact" => quote! {
499 #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
500 },
501 "contains" => quote! {
502 #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
503 },
504 "icontains" => quote! {
505 #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
506 },
507 "startswith" => quote! {
508 #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
509 },
510 "istartswith" => quote! {
511 #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
512 },
513 "endswith" => quote! {
514 #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
515 },
516 "iendswith" => quote! {
517 #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
518 },
519 "in" => quote! {
520 #root::core::Column::is_in(#path::#base_ident, #value)
521 },
522 "not_in" => quote! {
523 #root::core::Column::not_in(#path::#base_ident, #value)
524 },
525 "isnull" => {
526 // Must be a bool literal at macro time so we can route to
527 // is_null vs is_not_null without a runtime branch.
528 let b = match value {
529 syn::Expr::Lit(syn::ExprLit {
530 lit: syn::Lit::Bool(b),
531 ..
532 }) => b.value(),
533 _ => {
534 return Err(syn::Error::new_spanned(
535 value,
536 "Q!(): `__isnull` requires a `true` or `false` literal",
537 ));
538 }
539 };
540 if b {
541 quote! { #root::core::Column::is_null(#path::#base_ident) }
542 } else {
543 quote! { #root::core::Column::is_not_null(#path::#base_ident) }
544 }
545 }
546 "between" => {
547 // Accept a tuple literal `(lo, hi)`.
548 let tuple = match value {
549 syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
550 _ => {
551 return Err(syn::Error::new_spanned(
552 value,
553 "Q!(): `__between` requires a tuple literal `(lo, hi)`",
554 ));
555 }
556 };
557 let lo = &tuple.elems[0];
558 let hi = &tuple.elems[1];
559 quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
560 }
561 "regex" => quote! {
562 #root::core::Column::regex(#path::#base_ident, #value)
563 },
564 "iregex" => quote! {
565 #root::core::Column::iregex(#path::#base_ident, #value)
566 },
567 _ => {
568 return Err(syn::Error::new(
569 field_span,
570 format!(
571 "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",
572 suffix
573 ),
574 ));
575 }
576 };
577 Ok(expanded)
578}
579
580fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
581 // Default to "./migrations" if invoked without args.
582 let path_str = if input.is_empty() {
583 "./migrations".to_string()
584 } else {
585 let lit: LitStr = syn::parse2(input)?;
586 lit.value()
587 };
588
589 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
590 syn::Error::new(
591 proc_macro2::Span::call_site(),
592 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
593 )
594 })?;
595 let abs = std::path::Path::new(&manifest).join(&path_str);
596
597 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
598 if abs.is_dir() {
599 let read = std::fs::read_dir(&abs).map_err(|e| {
600 syn::Error::new(
601 proc_macro2::Span::call_site(),
602 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
603 )
604 })?;
605 for entry in read.flatten() {
606 let path = entry.path();
607 if !path.is_file() {
608 continue;
609 }
610 if path.extension().and_then(|s| s.to_str()) != Some("json") {
611 continue;
612 }
613 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
614 continue;
615 };
616 entries.push((stem.to_owned(), path));
617 }
618 }
619 entries.sort_by(|a, b| a.0.cmp(&b.0));
620
621 // Compile-time chain validation: read each migration's JSON,
622 // pull `name` and `prev` (file-stem-keyed for the chain check),
623 // and verify every `prev` points to another migration in the
624 // slice. Mismatches between the file stem and the embedded
625 // `name` field — and broken `prev` chains — fail at MACRO
626 // EXPANSION time so a misshapen migration set never compiles.
627 //
628 // This is the v0.4 Slice 5 distinguisher: rustango's JSON
629 // migrations + a Rust proc-macro that reads them is a combo no
630 // other Rust web framework matches (Cot's are imperative Rust
631 // code, Loco's are SeaORM up/down, Rwf's are raw SQL — none have
632 // a static chain to validate).
633 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
634 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
635 for (stem, path) in &entries {
636 let raw = std::fs::read_to_string(path).map_err(|e| {
637 syn::Error::new(
638 proc_macro2::Span::call_site(),
639 format!(
640 "embed_migrations!: cannot read {} for chain validation: {e}",
641 path.display()
642 ),
643 )
644 })?;
645 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
646 syn::Error::new(
647 proc_macro2::Span::call_site(),
648 format!(
649 "embed_migrations!: {} is not valid JSON: {e}",
650 path.display()
651 ),
652 )
653 })?;
654 let name = json
655 .get("name")
656 .and_then(|v| v.as_str())
657 .ok_or_else(|| {
658 syn::Error::new(
659 proc_macro2::Span::call_site(),
660 format!(
661 "embed_migrations!: {} is missing the `name` field",
662 path.display()
663 ),
664 )
665 })?
666 .to_owned();
667 if name != *stem {
668 return Err(syn::Error::new(
669 proc_macro2::Span::call_site(),
670 format!(
671 "embed_migrations!: file stem `{stem}` does not match the migration's \
672 `name` field `{name}` — rename the file or fix the JSON",
673 ),
674 ));
675 }
676 let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
677 chain_names.push(name.clone());
678 prev_refs.push((name, prev));
679 }
680
681 let name_set: std::collections::HashSet<&str> =
682 chain_names.iter().map(String::as_str).collect();
683 for (name, prev) in &prev_refs {
684 if let Some(p) = prev {
685 if !name_set.contains(p.as_str()) {
686 return Err(syn::Error::new(
687 proc_macro2::Span::call_site(),
688 format!(
689 "embed_migrations!: broken migration chain — `{name}` declares \
690 prev=`{p}` but no migration with that name exists in {}",
691 abs.display()
692 ),
693 ));
694 }
695 }
696 }
697
698 let pairs: Vec<TokenStream2> = entries
699 .iter()
700 .map(|(name, path)| {
701 let path_lit = path.display().to_string();
702 quote! { (#name, ::core::include_str!(#path_lit)) }
703 })
704 .collect();
705
706 Ok(quote! {
707 {
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 = 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
880 let model_impl = model_impl_tokens(
881 struct_name,
882 &model_name,
883 &table,
884 display.as_deref(),
885 app_label.as_deref(),
886 container.admin.as_ref(),
887 &container.default_order,
888 &collected.field_schemas,
889 collected.soft_delete_column.as_deref(),
890 container.permissions,
891 audit_track_names.as_deref(),
892 &container.m2m,
893 &all_indexes,
894 &container.checks,
895 &container.excludes,
896 &container.composite_fks,
897 &container.generic_fks,
898 container.scope.as_deref(),
899 container.is_view,
900 container.verbose_name.as_deref(),
901 container.verbose_name_plural.as_deref(),
902 container.managed,
903 container.base_manager_name.as_deref(),
904 container.order_with_respect_to.as_deref(),
905 container.proxy,
906 &container.required_db_features,
907 container.required_db_vendor.as_deref(),
908 container.default_related_name.as_deref(),
909 container.db_table_comment.as_deref(),
910 container
911 .get_latest_by
912 .as_ref()
913 .map(|(c, d)| (c.as_str(), *d)),
914 &container.extra_permissions,
915 &container.default_permissions,
916 &container.global_scopes,
917 &container.reverse_has_relations,
918 &container.generic_has_relations,
919 );
920 let module_ident = column_module_ident(struct_name);
921 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
922 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
923 let track_set: Option<std::collections::HashSet<&str>> = audit
924 .track
925 .as_ref()
926 .map(|(names, _)| names.iter().map(String::as_str).collect());
927 collected
928 .column_entries
929 .iter()
930 .filter(|c| {
931 track_set
932 .as_ref()
933 .map_or(true, |s| s.contains(c.name.as_str()))
934 })
935 .collect()
936 });
937 let inherent_impl = inherent_impl_tokens(
938 struct_name,
939 &collected,
940 collected.primary_key.as_ref(),
941 &column_consts,
942 audited_fields.as_deref(),
943 upsert_unique.as_deref(),
944 &container.manager_fns,
945 );
946 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
947 let from_row_impl = from_row_impl_tokens(
948 struct_name,
949 &collected.from_row_inits,
950 &collected.from_row_inits_my,
951 );
952 let reverse_helpers = reverse_helper_tokens(
953 struct_name,
954 &collected.fk_relations,
955 container.default_related_name.as_deref(),
956 );
957 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
958 let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
959 // Issue #817 — `#[rustango(through(...))]` accessors.
960 let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
961 // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
962 let reverse_has_accessors =
963 reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
964 let generic_fk_accessors = generic_fk_accessor_tokens(
965 struct_name,
966 &container.generic_fks,
967 &collected.column_entries,
968 );
969
970 // Issue #271 / T1.9 — `#[rustango(manager(ext = "FooManagerExt"))]`
971 // emits an empty extension trait so users can add methods via
972 // `impl FooManagerExt for QuerySet<Foo>` without hand-writing the
973 // trait declaration. See `crates/rustango/src/manager.rs` for the
974 // pattern this replaces.
975 let manager_trait = container.manager_ext.as_ref().map(|name| {
976 let model_name_str = struct_name.to_string();
977 let doc = format!(
978 "Empty marker trait for [`{model_name_str}`], from \
979 `#[rustango(manager(ext = ...))]`. Put query shortcuts on your \
980 own extension trait over `QuerySet<{model_name_str}>`."
981 );
982 quote! {
983 #[doc = #doc]
984 pub trait #name: ::core::marker::Sized {}
985 }
986 });
987
988 Ok(quote! {
989 #model_impl
990 #inherent_impl
991 #from_row_impl
992 #column_module
993 #reverse_helpers
994 #m2m_accessors
995 #generic_m2m_accessors
996 #through_accessors
997 #reverse_has_accessors
998 #generic_fk_accessors
999 #manager_trait
1000
1001 #root::core::inventory::submit! {
1002 // `module_path!()` evaluates at the registration site,
1003 // so a Model declared in `crate::blog::models` records
1004 // `"<crate>::blog::models"` and `resolved_app_label()`
1005 // can infer "blog" without an explicit attribute.
1006 #root::core::ModelEntry::new(
1007 <#struct_name as #root::core::Model>::SCHEMA,
1008 ::core::module_path!(),
1009 )
1010 .with_audited(
1011 <#struct_name as #root::core::Model>::__rustango_audited_update,
1012 <#struct_name as #root::core::Model>::__rustango_audited_delete,
1013 )
1014 .with_audited_create(<#struct_name as #root::core::Model>::__rustango_audited_create)
1015 }
1016 })
1017}
1018
1019/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
1020/// matches `field_name` against the model's FK fields and, for a
1021/// match, decodes the FK target via the parent's macro-generated
1022/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
1023/// `ForeignKey::Loaded` on `self`.
1024///
1025/// Always emitted (with empty arms for FK-less models, which
1026/// return `Ok(false)` for any field name) so the `T: LoadRelated`
1027/// trait bound on `fetch_on` is universally satisfied — users
1028/// never have to think about implementing it.
1029fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1030 let root = rustango_root();
1031 let arms = fk_relations.iter().map(|rel| {
1032 let parent_ty = &rel.parent_type;
1033 let field_ident = &rel.field_ident;
1034 let fk_name = ident_name(field_ident);
1035 let fk_col = fk_name.as_str();
1036 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1037 let assign = if rel.nullable {
1038 quote! {
1039 self.#field_ident = ::core::option::Option::Some(
1040 #root::sql::ForeignKey::loaded(_pk, _parent),
1041 );
1042 }
1043 } else {
1044 quote! {
1045 self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1046 }
1047 };
1048 quote! {
1049 #fk_col => {
1050 let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
1051 // Audit #451 — multi-hop `select_related("a__b__c")`:
1052 // stitch the deeper relation onto this parent first,
1053 // decoding it at the accumulated `__next_alias`. The
1054 // parent type also impls `LoadRelated`, so this recurses
1055 // the FK chain to arbitrary depth.
1056 if let ::core::option::Option::Some(__r) = __rest {
1057 let _ = #root::sql::LoadRelated::__rustango_load_related(
1058 &mut _parent, row, __r, &__next_alias,
1059 )?;
1060 }
1061 // Loud-in-debug, default-in-release: a divergence
1062 // between the FK field's declared `K` (drives the
1063 // expected `SqlValue::<Variant>`) and the parent's
1064 // `__rustango_pk_value` output is a macro-internal
1065 // invariant break — surfacing the panic in dev
1066 // catches it before users hit silent PK=0 corruption.
1067 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1068 #root::core::SqlValue::#variant_ident(v) => v,
1069 _other => {
1070 ::core::debug_assert!(
1071 false,
1072 "rustango macro bug: load_related on FK `{}` expected \
1073 SqlValue::{} from parent's __rustango_pk_value but got \
1074 {:?} — file a bug at https://github.com/ujeenet/rustango",
1075 #fk_col,
1076 ::core::stringify!(#variant_ident),
1077 _other,
1078 );
1079 #default_expr
1080 }
1081 };
1082 #assign
1083 ::core::result::Result::Ok(true)
1084 }
1085 }
1086 });
1087 quote! {
1088 #[cfg(feature = "postgres")]
1089 impl #root::sql::LoadRelated for #struct_name {
1090 #[allow(unused_variables)]
1091 fn __rustango_load_related(
1092 &mut self,
1093 row: &#root::sql::sqlx::postgres::PgRow,
1094 field_name: &str,
1095 alias: &str,
1096 ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1097 // Audit #451 — split the multi-hop path: `base` is the FK
1098 // on THIS model, `__rest` (if any) is the remaining chain
1099 // to stitch onto the loaded parent. `__next_alias` is the
1100 // accumulated join alias the parent's columns live under
1101 // (`{alias}__{next-hop}`), matching `lower_select_related`.
1102 let (__base, __rest): (&str, ::core::option::Option<&str>) =
1103 match field_name.split_once("__") {
1104 ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1105 ::core::option::Option::None => (field_name, ::core::option::Option::None),
1106 };
1107 let __next_alias: ::std::string::String = match __rest {
1108 ::core::option::Option::Some(__r) => {
1109 let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1110 ::std::format!("{}__{}", alias, __rb)
1111 }
1112 ::core::option::Option::None => ::std::string::String::new(),
1113 };
1114 match __base {
1115 #( #arms )*
1116 _ => ::core::result::Result::Ok(false),
1117 }
1118 }
1119 }
1120 }
1121}
1122
1123/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1124/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1125/// which expands to a `LoadRelatedMy` impl when rustango is built with
1126/// the `mysql` feature, and to nothing otherwise. The decoded parent
1127/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1128/// decoder, also batch8) so the dual emission is symmetric across
1129/// backends.
1130fn load_related_impl_my_tokens(
1131 struct_name: &syn::Ident,
1132 fk_relations: &[FkRelation],
1133) -> TokenStream2 {
1134 let root = rustango_root();
1135 let arms = fk_relations.iter().map(|rel| {
1136 let parent_ty = &rel.parent_type;
1137 let field_ident = &rel.field_ident;
1138 let fk_name = ident_name(field_ident);
1139 let fk_col = fk_name.as_str();
1140 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1141 let assign = if rel.nullable {
1142 quote! {
1143 __self.#field_ident = ::core::option::Option::Some(
1144 #root::sql::ForeignKey::loaded(_pk, _parent),
1145 );
1146 }
1147 } else {
1148 quote! {
1149 __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1150 }
1151 };
1152 // `self` IS hygiene-tracked through macro_rules — emitted from
1153 // a different context than the `&mut self` parameter inside
1154 // the macro_rules-expanded fn. Pass it through as `__self`
1155 // and let the macro_rules rebind it to the receiver.
1156 quote! {
1157 #fk_col => {
1158 let mut _parent: #parent_ty =
1159 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1160 // Audit #451 — multi-hop: stitch the deeper relation onto
1161 // the parent at the accumulated alias (see PG twin).
1162 if let ::core::option::Option::Some(__r) = __rest {
1163 let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1164 &mut _parent, row, __r, &__next_alias,
1165 )?;
1166 }
1167 // See note in `load_related_impl_tokens` (PG twin) —
1168 // the same loud-in-debug invariant guard.
1169 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1170 #root::core::SqlValue::#variant_ident(v) => v,
1171 _other => {
1172 ::core::debug_assert!(
1173 false,
1174 "rustango macro bug: load_related on FK `{}` expected \
1175 SqlValue::{} from parent's __rustango_pk_value but got \
1176 {:?} — file a bug at https://github.com/ujeenet/rustango",
1177 #fk_col,
1178 ::core::stringify!(#variant_ident),
1179 _other,
1180 );
1181 #default_expr
1182 }
1183 };
1184 #assign
1185 ::core::result::Result::Ok(true)
1186 }
1187 }
1188 });
1189 quote! {
1190 #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1191 #( #arms )*
1192 });
1193 }
1194}
1195
1196/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1197/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1198/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1199fn load_related_impl_sqlite_tokens(
1200 struct_name: &syn::Ident,
1201 fk_relations: &[FkRelation],
1202) -> TokenStream2 {
1203 let root = rustango_root();
1204 let arms = fk_relations.iter().map(|rel| {
1205 let parent_ty = &rel.parent_type;
1206 let field_ident = &rel.field_ident;
1207 let fk_name = ident_name(field_ident);
1208 let fk_col = fk_name.as_str();
1209 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1210 let assign = if rel.nullable {
1211 quote! {
1212 __self.#field_ident = ::core::option::Option::Some(
1213 #root::sql::ForeignKey::loaded(_pk, _parent),
1214 );
1215 }
1216 } else {
1217 quote! {
1218 __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1219 }
1220 };
1221 quote! {
1222 #fk_col => {
1223 let mut _parent: #parent_ty =
1224 <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1225 // Audit #451 — multi-hop: stitch the deeper relation onto
1226 // the parent at the accumulated alias (see PG twin).
1227 if let ::core::option::Option::Some(__r) = __rest {
1228 let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1229 &mut _parent, row, __r, &__next_alias,
1230 )?;
1231 }
1232 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1233 #root::core::SqlValue::#variant_ident(v) => v,
1234 _other => {
1235 ::core::debug_assert!(
1236 false,
1237 "rustango macro bug: load_related on FK `{}` expected \
1238 SqlValue::{} from parent's __rustango_pk_value but got \
1239 {:?} — file a bug at https://github.com/ujeenet/rustango",
1240 #fk_col,
1241 ::core::stringify!(#variant_ident),
1242 _other,
1243 );
1244 #default_expr
1245 }
1246 };
1247 #assign
1248 ::core::result::Result::Ok(true)
1249 }
1250 }
1251 });
1252 quote! {
1253 #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1254 #( #arms )*
1255 });
1256 }
1257}
1258
1259/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1260/// matches `field_name` against the model's FK fields and returns
1261/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1262/// group children by parent PK.
1263///
1264/// Always emitted (with `_ => None` for FK-less models) so the
1265/// trait bound on `fetch_with_prefetch` is universally satisfied.
1266fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1267 let root = rustango_root();
1268 let arms = fk_relations.iter().map(|rel| {
1269 let field_ident = &rel.field_ident;
1270 let fk_name = ident_name(field_ident);
1271 let fk_col = fk_name.as_str();
1272 if rel.pk_kind == DetectedKind::I64 {
1273 // i64 FK — return the stored PK so prefetch_related can
1274 // group children by it. Nullable variant unwraps via
1275 // `as_ref().map(...)`: an unset (NULL) FK column yields
1276 // `None` and that child sits out of the grouping (correct
1277 // semantics — it has no parent to attach to).
1278 if rel.nullable {
1279 quote! {
1280 #fk_col => self.#field_ident
1281 .as_ref()
1282 .map(|fk| #root::sql::ForeignKey::pk(fk)),
1283 }
1284 } else {
1285 quote! {
1286 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1287 }
1288 }
1289 } else {
1290 // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1291 // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1292 // i64-keyed grouping path — the trait signature is
1293 // `Option<i64>` and a non-i64 PK can't lower into it.
1294 // The FK still works for everything else (CRUD, lazy
1295 // load via `.get()`, select_related JOINs); only the
1296 // bulk prefetch grouper needs the integer key.
1297 quote! {
1298 #fk_col => ::core::option::Option::None,
1299 }
1300 }
1301 });
1302 // PK-type-agnostic version: every FK arm emits an
1303 // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1304 // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1305 // opt OUT of the legacy i64 method (it returns None) but opt IN
1306 // here.
1307 let value_arms = fk_relations.iter().map(|rel| {
1308 let field_ident = &rel.field_ident;
1309 let fk_name = ident_name(field_ident);
1310 let fk_col = fk_name.as_str();
1311 if rel.nullable {
1312 quote! {
1313 #fk_col => self.#field_ident
1314 .as_ref()
1315 .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1316 #root::sql::ForeignKey::pk(fk)
1317 )),
1318 }
1319 } else {
1320 quote! {
1321 #fk_col => ::core::option::Option::Some(
1322 ::core::convert::Into::<#root::core::SqlValue>::into(
1323 self.#field_ident.pk()
1324 )
1325 ),
1326 }
1327 }
1328 });
1329 quote! {
1330 impl #root::sql::FkPkAccess for #struct_name {
1331 #[allow(unused_variables)]
1332 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1333 match field_name {
1334 #( #arms )*
1335 _ => ::core::option::Option::None,
1336 }
1337 }
1338 #[allow(unused_variables)]
1339 fn __rustango_fk_pk_value(
1340 &self,
1341 field_name: &str,
1342 ) -> ::core::option::Option<#root::core::SqlValue> {
1343 match field_name {
1344 #( #value_arms )*
1345 _ => ::core::option::Option::None,
1346 }
1347 }
1348 }
1349 }
1350}
1351
1352/// For every `ForeignKey<Parent>` field on `Child`, emit
1353/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1354/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
1355/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
1356/// — the canonical reverse-FK fetch. One round trip, no N+1.
1357///
1358/// **PG-only emission**: the accessor is bounded on
1359/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1360/// both of which are gated behind the `postgres` cargo feature. The
1361/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1362/// model derive itself compiles on tri-dialect / sqlite-only
1363/// downstream builds — the accessor just isn't materialised. A tri-
1364/// dialect `_set_pool` variant is a separate follow-up.
1365fn reverse_helper_tokens(
1366 child_ident: &syn::Ident,
1367 fk_relations: &[FkRelation],
1368 default_related_name: Option<&str>,
1369) -> TokenStream2 {
1370 let root = rustango_root();
1371 if fk_relations.is_empty() {
1372 return TokenStream2::new();
1373 }
1374 // Method-name resolution per FK (issue #816 + follow-up):
1375 // 1. Field-level `#[rustango(related_name = "...")]` on the FK
1376 // itself — wins over everything else.
1377 // 2. Container-level `default_related_name = "..."` on the
1378 // child. Applies to every FK on this model that didn't
1379 // override.
1380 // 3. Fallback: `<child_snake>_set`. `Post` → `post_set`,
1381 // `BlogComment` → `blog_comment_set`. Avoids English-plural
1382 // edge cases.
1383 //
1384 // The PG-on-executor variant keeps the resolved name; the
1385 // tri-dialect `_pool` variant appends `_pool` to it (matches the
1386 // framework's convention for the `&Pool` flavor of every helper).
1387 let default_pg_suffix = default_related_name
1388 .map(str::to_owned)
1389 .unwrap_or_else(|| format!("{}_set", to_snake_case(&ident_name(child_ident))));
1390 let impls = fk_relations.iter().map(|rel| {
1391 let pg_suffix = rel
1392 .related_name
1393 .clone()
1394 .unwrap_or_else(|| default_pg_suffix.clone());
1395 let pool_suffix = format!("{}_pool", pg_suffix);
1396 let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1397 let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1398 let parent_ty = &rel.parent_type;
1399 let fk_name = ident_name(&rel.field_ident);
1400 let fk_col = fk_name.as_str();
1401 let doc = format!(
1402 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1403 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1404 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1405 further `{child_ident}::objects()` filters via direct queryset use."
1406 );
1407 let pool_doc = format!(
1408 "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1409 [`#root::sql::Pool`] and dispatches per backend so the \
1410 reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1411 Use this from framework code that holds a `&Pool` (admin, \
1412 tenancy resolver, viewset handlers); reach for the executor- \
1413 bound variant when you already have a typed `sqlx::Executor`."
1414 );
1415 quote! {
1416 #[cfg(feature = "postgres")]
1417 impl #parent_ty {
1418 #[doc = #doc]
1419 ///
1420 /// # Errors
1421 /// Returns [`#root::sql::ExecError`] for SQL-writing
1422 /// or driver failures.
1423 pub async fn #pg_method_ident<'_c, _E>(
1424 &self,
1425 _executor: _E,
1426 ) -> ::core::result::Result<
1427 ::std::vec::Vec<#child_ident>,
1428 #root::sql::ExecError,
1429 >
1430 where
1431 _E: #root::sql::sqlx::Executor<
1432 '_c,
1433 Database = #root::sql::sqlx::Postgres,
1434 >,
1435 {
1436 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1437 #root::query::QuerySet::<#child_ident>::new()
1438 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1439 .fetch_on(_executor)
1440 .await
1441 }
1442 }
1443
1444 impl #parent_ty {
1445 #[doc = #pool_doc]
1446 ///
1447 /// # Errors
1448 /// Returns [`#root::sql::ExecError`] for SQL-writing
1449 /// or driver failures.
1450 pub async fn #pool_method_ident(
1451 &self,
1452 pool: &#root::sql::Pool,
1453 ) -> ::core::result::Result<
1454 ::std::vec::Vec<#child_ident>,
1455 #root::sql::ExecError,
1456 > {
1457 use #root::sql::FetcherPool as _;
1458 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1459 #root::query::QuerySet::<#child_ident>::new()
1460 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1461 .fetch(pool)
1462 .await
1463 }
1464 }
1465 }
1466 });
1467 quote! { #( #impls )* }
1468}
1469
1470/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1471/// relation declared on the model.
1472/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1473/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1474///
1475/// Closes #239 + #240 — `comment.content_object` /
1476/// `comment.content_object = post` ergonomics on top of the existing
1477/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1478///
1479/// `column_entries` is passed so we can resolve each `ct_column` /
1480/// `pk_column` SQL name back to its Rust field ident — the macro
1481/// only sees the column-side strings in the attribute, but the
1482/// emitted accessor needs to read the actual struct field.
1483fn generic_fk_accessor_tokens(
1484 struct_name: &syn::Ident,
1485 generic_fks: &[GenericFkAttr],
1486 column_entries: &[ColumnEntry],
1487) -> TokenStream2 {
1488 let root = rustango_root();
1489 if generic_fks.is_empty() {
1490 return TokenStream2::new();
1491 }
1492 let methods = generic_fks.iter().filter_map(|gfk| {
1493 // Resolve `ct_column` + `pk_column` to the struct's Rust
1494 // field idents. A typo (column name doesn't match any field)
1495 // emits no method for that registration — the user will see
1496 // the compiler reject the SCHEMA literal anyway, so there's
1497 // a clear error path without us double-reporting.
1498 let ct_ident = column_entries
1499 .iter()
1500 .find(|c| c.column == gfk.ct_column)
1501 .map(|c| c.ident.clone())?;
1502 let pk_ident = column_entries
1503 .iter()
1504 .find(|c| c.column == gfk.pk_column)
1505 .map(|c| c.ident.clone())?;
1506
1507 let accessor_ident =
1508 syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1509 let setter_ident =
1510 syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1511 let name_literal = gfk.name.as_str();
1512
1513 Some(quote! {
1514 #[doc = concat!(
1515 "Resolve the polymorphic `",
1516 #name_literal,
1517 "` relation. Reads `self.",
1518 stringify!(#ct_ident),
1519 "` + `self.",
1520 stringify!(#pk_ident),
1521 "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1522 "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1523 #name_literal,
1524 "\", ...))]`."
1525 )]
1526 pub async fn #accessor_ident(
1527 &self,
1528 pool: &#root::sql::Pool,
1529 ) -> ::core::result::Result<
1530 ::core::option::Option<#root::__serde_json::Value>,
1531 #root::sql::ExecError,
1532 > {
1533 let gfk = #root::contenttypes::GenericForeignKey::new(
1534 self.#ct_ident as i64,
1535 self.#pk_ident as i64,
1536 );
1537 gfk.get_object(pool).await
1538 }
1539
1540 #[doc = concat!(
1541 "Set the polymorphic `",
1542 #name_literal,
1543 "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1544 stringify!(#ct_ident),
1545 "` and `self.",
1546 stringify!(#pk_ident),
1547 "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1548 #name_literal,
1549 "\", ...))]`."
1550 )]
1551 pub async fn #setter_ident<T: #root::core::Model>(
1552 &mut self,
1553 pool: &#root::sql::Pool,
1554 target_pk: i64,
1555 ) -> ::core::result::Result<(), #root::sql::ExecError> {
1556 let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1557 pool,
1558 target_pk,
1559 ).await?;
1560 self.#ct_ident = gfk.content_type_id as _;
1561 self.#pk_ident = gfk.object_pk as _;
1562 ::core::result::Result::Ok(())
1563 }
1564 })
1565 });
1566 quote! {
1567 impl #struct_name {
1568 #( #methods )*
1569 }
1570 }
1571}
1572
1573fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1574 let root = rustango_root();
1575 if m2m_relations.is_empty() {
1576 return TokenStream2::new();
1577 }
1578 let methods = m2m_relations.iter().map(|rel| {
1579 let method_name = format!("{}_m2m", rel.name);
1580 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1581 let through = rel.through.as_str();
1582 let src_col = rel.src.as_str();
1583 let dst_col = rel.dst.as_str();
1584 quote! {
1585 pub fn #method_ident(&self) -> #root::sql::M2MManager {
1586 #root::sql::M2MManager {
1587 src_pk: self.__rustango_pk_value(),
1588 through: #through,
1589 src_col: #src_col,
1590 dst_col: #dst_col,
1591 }
1592 }
1593 }
1594 });
1595 quote! {
1596 impl #struct_name {
1597 #( #methods )*
1598 }
1599 }
1600}
1601
1602/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1603/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1604fn generic_m2m_accessor_tokens(
1605 struct_name: &syn::Ident,
1606 relations: &[GenericM2MAttr],
1607) -> TokenStream2 {
1608 let root = rustango_root();
1609 if relations.is_empty() {
1610 return TokenStream2::new();
1611 }
1612 let methods = relations.iter().map(|rel| {
1613 let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1614 let through = rel.through.as_str();
1615 let pk_col = rel.pk_column.as_str();
1616 let ct_col = rel.ct_column.as_str();
1617 let related_col = rel.related_column.as_str();
1618 quote! {
1619 pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1620 #root::sql::GenericM2MManager {
1621 src_pk: self.__rustango_pk_value(),
1622 src_schema: <Self as #root::core::Model>::SCHEMA,
1623 through: #through,
1624 pk_col: #pk_col,
1625 ct_col: #ct_col,
1626 dst_col: #related_col,
1627 }
1628 }
1629 }
1630 });
1631 quote! {
1632 impl #struct_name {
1633 #( #methods )*
1634 }
1635 }
1636}
1637
1638/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1639/// associated functions for each `#[rustango(reverse_has(...))]`
1640/// attribute. Issue #830.
1641///
1642/// The two emitted functions return ready-to-use `WhereExpr` nodes
1643/// that downstream callers drop into
1644/// `QuerySet::<Self>::where_raw(...)`:
1645///
1646/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1647/// <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1648/// `whereHas` parity (without the closure-style sub-predicate
1649/// refinement — that's a follow-up; users can layer additional
1650/// predicates by constructing the SelectQuery themselves and
1651/// calling `where_raw(exists(query))` from `crate::core::subquery`).
1652/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1653/// `whereDoesntHave` parity.
1654///
1655/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1656/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1657/// machinery threads the outer-table reference through automatically
1658/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1659fn reverse_has_accessor_tokens(
1660 struct_name: &syn::Ident,
1661 reverse_has_relations: &[ReverseHasAttr],
1662) -> TokenStream2 {
1663 let root = rustango_root();
1664 if reverse_has_relations.is_empty() {
1665 return TokenStream2::new();
1666 }
1667 let methods = reverse_has_relations.iter().map(|rel| {
1668 let exists_name = format!("{}_exists_expr", rel.name);
1669 let not_exists_name = format!("{}_not_exists_expr", rel.name);
1670 let count_name = format!("{}_count", rel.name);
1671 let fetch_name = format!("{}_fetch", rel.name);
1672 let first_name = format!("{}_first", rel.name);
1673 let pluck_name = format!("{}_pluck", rel.name);
1674 let accessor_name = rel.name.as_str();
1675 let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1676 let not_exists_ident = syn::Ident::new(¬_exists_name, struct_name.span());
1677 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1678 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1679 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1680 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1681 let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1682 let child = &rel.child;
1683 let child_fk_column = rel.child_fk_column.as_str();
1684 let self_pk_column = rel.self_pk_column.as_str();
1685 let exists_doc = format!(
1686 "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1687 FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1688 Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1689 filter to {struct_name}s with at least one matching child.",
1690 );
1691 let not_exists_doc = format!(
1692 "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1693 (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1694 <outer>.{self_pk_column})`. Drop into \
1695 `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1696 {struct_name}s with **no** matching child.",
1697 );
1698 let count_doc = format!(
1699 "Eloquent `$model->{name}->count()` analog — returns \
1700 the number of `{child}` rows whose `{child_fk_column}` \
1701 matches this `{struct_name}` instance's primary key. \
1702 Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1703 {child_fk_column} = <self.pk>`.",
1704 name = rel.name,
1705 );
1706 let accessor_doc = format!(
1707 "Eloquent `$model->{name}` accessor — returns a \
1708 `QuerySet<{child}>` filtered to rows whose \
1709 `{child_fk_column}` matches this `{struct_name}` \
1710 instance's primary key. **Chainable**: compose `.filter()` \
1711 / `.order_by()` / `.limit()` etc. on top, then call \
1712 `.fetch(&pool)` (the QuerySet trait method) when \
1713 done. For the simple \"fetch all\" hot path with no \
1714 further composition, prefer the bare-name \
1715 `{name}_fetch(&pool)` companion.",
1716 name = rel.name,
1717 );
1718 let fetch_doc = format!(
1719 "Eloquent `$model->{name}->get()` — bare-name hot-path \
1720 over `{name}(&self).fetch(&pool)`. Use this when \
1721 you don't need further `.filter()` / `.order_by()` \
1722 composition; falls back to the chainable accessor when \
1723 you do. Avoids the `_pool` suffix on the most common \
1724 call-site shape.",
1725 name = rel.name,
1726 );
1727 quote! {
1728 #[doc = #accessor_doc]
1729 pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1730 #root::query::QuerySet::<#child>::new()
1731 .filter(#child_fk_column, self.__rustango_pk_value())
1732 }
1733
1734 #[doc = #fetch_doc]
1735 pub async fn #fetch_ident(
1736 &self,
1737 pool: &#root::sql::Pool,
1738 ) -> ::core::result::Result<
1739 ::std::vec::Vec<#child>,
1740 #root::sql::ExecError,
1741 > {
1742 use #root::sql::FetcherPool as _;
1743 self.#accessor_ident().fetch(pool).await
1744 }
1745
1746 /// Eloquent `$model->relation->first()` / `hasOne`
1747 /// semantics — bare-name shortcut over
1748 /// `self.<name>().first(&pool)`. Returns `None` when no
1749 /// child rows match. Useful when the relation is
1750 /// nominally many-to-one in shape but at most one row is
1751 /// expected (latest comment, primary tag, etc.).
1752 pub async fn #first_ident(
1753 &self,
1754 pool: &#root::sql::Pool,
1755 ) -> ::core::result::Result<
1756 ::core::option::Option<#child>,
1757 #root::sql::ExecError,
1758 > {
1759 self.#accessor_ident().first(pool).await
1760 }
1761
1762 /// Eloquent `$model->relation->pluck($col)` — project a
1763 /// single column from the child rows into a `Vec<U>`.
1764 /// Skips the typed `Child` decode, which is cheaper when
1765 /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1766 pub async fn #pluck_ident<U>(
1767 &self,
1768 col: &'static str,
1769 pool: &#root::sql::Pool,
1770 ) -> ::core::result::Result<
1771 ::std::vec::Vec<U>,
1772 #root::sql::ExecError,
1773 >
1774 where
1775 U: #root::sql::FlatScalar,
1776 {
1777 self.#accessor_ident()
1778 .values_list_flat(col)
1779 .fetch::<U>(pool)
1780 .await
1781 }
1782
1783 #[doc = #exists_doc]
1784 pub fn #exists_ident() -> #root::core::WhereExpr {
1785 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1786 let child_schema =
1787 <#child as #root::core::Model>::SCHEMA;
1788 let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1789 lhs: Expr::Column(#child_fk_column),
1790 op: Op::Eq,
1791 rhs: Expr::OuterRef(#self_pk_column),
1792 });
1793 WhereExpr::Exists(::std::boxed::Box::new(inner))
1794 }
1795
1796 #[doc = #not_exists_doc]
1797 pub fn #not_exists_ident() -> #root::core::WhereExpr {
1798 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1799 let child_schema =
1800 <#child as #root::core::Model>::SCHEMA;
1801 let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1802 lhs: Expr::Column(#child_fk_column),
1803 op: Op::Eq,
1804 rhs: Expr::OuterRef(#self_pk_column),
1805 });
1806 WhereExpr::NotExists(::std::boxed::Box::new(inner))
1807 }
1808
1809 #[doc = #count_doc]
1810 pub async fn #count_ident(
1811 &self,
1812 pool: &#root::sql::Pool,
1813 ) -> ::core::result::Result<
1814 i64,
1815 #root::sql::ExecError,
1816 > {
1817 use #root::sql::CounterPool as _;
1818 #root::query::QuerySet::<#child>::new()
1819 .filter(#child_fk_column, self.__rustango_pk_value())
1820 .count(pool)
1821 .await
1822 }
1823 }
1824 });
1825 quote! {
1826 impl #struct_name {
1827 #( #methods )*
1828 }
1829 }
1830}
1831
1832/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1833/// `#[rustango(through(...))]` attribute. Issue #817.
1834///
1835/// Each accessor builds a correlated subquery via
1836/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1837/// intermediate table, filters on the FK column pointing at this
1838/// model, and projects the intermediate PK. The outer queryset
1839/// filters the far model by its FK-to-intermediate column being in
1840/// that set.
1841///
1842/// The returned `QuerySet<Far>` is **chainable** — the subquery
1843/// lives inside a `where_raw` clause so the user's later
1844/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1845/// disturb it.
1846///
1847/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1848/// SQLite — no LATERAL or backend-specific syntax involved.
1849fn through_accessor_tokens(
1850 struct_name: &syn::Ident,
1851 through_relations: &[ThroughAttr],
1852) -> TokenStream2 {
1853 let root = rustango_root();
1854 if through_relations.is_empty() {
1855 return TokenStream2::new();
1856 }
1857 let methods = through_relations.iter().map(|rel| {
1858 let method_name = format!("{}_through", rel.name);
1859 let count_name = format!("{}_through_count", rel.name);
1860 let fetch_name = format!("{}_through_fetch", rel.name);
1861 let first_name = format!("{}_through_first", rel.name);
1862 let pluck_name = format!("{}_through_pluck", rel.name);
1863 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1864 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1865 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1866 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1867 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1868 let far = &rel.far;
1869 let intermediate = &rel.intermediate;
1870 let far_fk_column = rel.far_fk_column.as_str();
1871 let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1872 let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1873 let doc = format!(
1874 "Eloquent `hasManyThrough` accessor — returns a \
1875 `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1876 instance through the intermediate `{intermediate}` table. \
1877 Generated SQL shape: \
1878 `… WHERE {far_fk_column} IN (SELECT \
1879 {intermediate_pk_column} FROM <{intermediate}> WHERE \
1880 {intermediate_fk_column} = self.pk)`. Chainable like any \
1881 other QuerySet — compose `.filter()` / `.order_by()` / \
1882 `.limit()` etc. on top.",
1883 );
1884 let count_doc = format!(
1885 "Eloquent `$model->{name}->count()` analog for the \
1886 through-relation — returns the number of `{far}` rows \
1887 reachable through `{intermediate}`. Equivalent to \
1888 `self.{name}_through().count(pool)` but spelled \
1889 as a bare instance method for parity with the \
1890 `reverse_has` `<name>_count` shape.",
1891 name = rel.name,
1892 );
1893 quote! {
1894 #[doc = #doc]
1895 pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1896 use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1897 let intermediate_schema =
1898 <#intermediate as #root::core::Model>::SCHEMA;
1899 let sub = SelectQuery::new(intermediate_schema)
1900 .where_clause(WhereExpr::Predicate(Filter::new(
1901 #intermediate_fk_column,
1902 Op::Eq,
1903 self.__rustango_pk_value(),
1904 )))
1905 .projection(::std::vec![#intermediate_pk_column]);
1906 #root::query::QuerySet::<#far>::new().where_raw(
1907 WhereExpr::InSubquery {
1908 column: #far_fk_column,
1909 negated: false,
1910 subquery: ::std::boxed::Box::new(sub),
1911 },
1912 )
1913 }
1914
1915 #[doc = #count_doc]
1916 pub async fn #count_ident(
1917 &self,
1918 pool: &#root::sql::Pool,
1919 ) -> ::core::result::Result<
1920 i64,
1921 #root::sql::ExecError,
1922 > {
1923 use #root::sql::CounterPool as _;
1924 self.#method_ident().count(pool).await
1925 }
1926
1927 /// Eloquent `$model->relation->get()` for the
1928 /// through-relation — bare-name hot-path over
1929 /// `self.<name>_through().fetch(&pool)`. Use this
1930 /// when you don't need further `.filter()` /
1931 /// `.order_by()` composition; falls back to the
1932 /// chainable accessor when you do. Avoids the `_pool`
1933 /// suffix on the most common call-site shape.
1934 pub async fn #fetch_ident(
1935 &self,
1936 pool: &#root::sql::Pool,
1937 ) -> ::core::result::Result<
1938 ::std::vec::Vec<#far>,
1939 #root::sql::ExecError,
1940 > {
1941 use #root::sql::FetcherPool as _;
1942 self.#method_ident().fetch(pool).await
1943 }
1944
1945 /// Eloquent `hasOneThrough` analog — bare-name shortcut
1946 /// over `self.<name>_through().first(&pool)`. Returns
1947 /// `None` when no far rows are reachable through the
1948 /// intermediate. Useful when at most one row is
1949 /// expected (latest comment by country, primary tag,
1950 /// etc.).
1951 pub async fn #first_ident(
1952 &self,
1953 pool: &#root::sql::Pool,
1954 ) -> ::core::result::Result<
1955 ::core::option::Option<#far>,
1956 #root::sql::ExecError,
1957 > {
1958 self.#method_ident().first(pool).await
1959 }
1960
1961 /// Pluck a single column from the far rows into `Vec<U>`
1962 /// — cheaper than the typed `<Far>` decode when only one
1963 /// scalar column is needed.
1964 pub async fn #pluck_ident<U>(
1965 &self,
1966 col: &'static str,
1967 pool: &#root::sql::Pool,
1968 ) -> ::core::result::Result<
1969 ::std::vec::Vec<U>,
1970 #root::sql::ExecError,
1971 >
1972 where
1973 U: #root::sql::FlatScalar,
1974 {
1975 self.#method_ident()
1976 .values_list_flat(col)
1977 .fetch::<U>(pool)
1978 .await
1979 }
1980 }
1981 });
1982 quote! {
1983 impl #struct_name {
1984 #( #methods )*
1985 }
1986 }
1987}
1988
1989struct ColumnEntry {
1990 /// The struct field ident, used both for the inherent const name on
1991 /// the model and for the inner column type's name.
1992 ident: syn::Ident,
1993 /// The struct's field type, used as `Column::Value`.
1994 value_ty: Type,
1995 /// Rust-side field name (e.g. `"id"`).
1996 name: String,
1997 /// SQL-side column name (e.g. `"user_id"`).
1998 column: String,
1999 /// `#root::core::FieldType::I64` etc.
2000 field_type_tokens: TokenStream2,
2001 /// A bare or optional `Uuid` field; MySQL decodes it from text.
2002 uuid_column: bool,
2003}
2004
2005struct CollectedFields {
2006 field_schemas: Vec<TokenStream2>,
2007 from_row_inits: Vec<TokenStream2>,
2008 /// Aliased counterparts of `from_row_inits` — read columns via
2009 /// `format!("{prefix}__{col}")` aliases so a Model can be
2010 /// decoded from a JOINed row's projected target columns.
2011 from_aliased_row_inits: Vec<TokenStream2>,
2012 /// MySQL twins of `from_row_inits` / `from_aliased_row_inits`.
2013 from_row_inits_my: Vec<TokenStream2>,
2014 from_aliased_row_inits_my: Vec<TokenStream2>,
2015 /// Static column-name list — used by the simple insert path
2016 /// (no `Auto<T>` fields). Aligned with `insert_values`.
2017 insert_columns: Vec<TokenStream2>,
2018 /// Static `Into<SqlValue>` expressions, one per field. Aligned
2019 /// with `insert_columns`. Used by the simple insert path only.
2020 insert_values: Vec<TokenStream2>,
2021 /// Per-field push expressions for the dynamic (Auto-aware)
2022 /// insert path. Each statement either unconditionally pushes
2023 /// `(column, value)` or, for an `Auto<T>` field, conditionally
2024 /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
2025 insert_pushes: Vec<TokenStream2>,
2026 /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
2027 /// when `has_auto == false`.
2028 returning_cols: Vec<TokenStream2>,
2029 /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
2030 /// field. Run after `insert_returning` to populate the model.
2031 auto_assigns: Vec<TokenStream2>,
2032 /// `(ident, column_literal)` pairs for every Auto field. Used by
2033 /// the bulk_insert codegen to rebuild assigns against `_row_mut`
2034 /// instead of `self`.
2035 auto_field_idents: Vec<(syn::Ident, String)>,
2036 /// #1028 — `(ident, column)` for each `generated_as` field. Drives
2037 /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
2038 /// back into the struct after insert (MySQL has no RETURNING → the
2039 /// field stays at its placeholder until the next read).
2040 generated_field_idents: Vec<(syn::Ident, String)>,
2041 /// First `Auto<T>` field the database fills, with its inner `T`: the
2042 /// MySQL `LAST_INSERT_ID()` target. Rust-filled ones never are (#1934).
2043 first_db_auto: Option<(syn::Ident, Type)>,
2044 /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2045 /// by the all-Auto-Unset bulk path (Auto cols dropped from
2046 /// `columns`).
2047 bulk_pushes_no_auto: Vec<TokenStream2>,
2048 /// Bulk-insert per-row pushes for **all fields including Auto**.
2049 /// Used by the all-Auto-Set bulk path (Auto col included with the
2050 /// caller-supplied value).
2051 bulk_pushes_all: Vec<TokenStream2>,
2052 /// Column-name literals for non-Auto fields only (paired with
2053 /// `bulk_pushes_no_auto`).
2054 bulk_columns_no_auto: Vec<TokenStream2>,
2055 /// Column-name literals for every field including Auto (paired
2056 /// with `bulk_pushes_all`).
2057 bulk_columns_all: Vec<TokenStream2>,
2058 /// Fills each `default_uuid_v7` field of `_row` in place, so a
2059 /// mutable bulk insert hands the PKs back (#1934).
2060 bulk_fill_in_place: Vec<TokenStream2>,
2061 /// Rust-filled `Auto` fields (`default_uuid_v7`, `auto_now(_add)`), reset when `insert_or_ignore` skips (#1937).
2062 rust_filled_idents: Vec<syn::Ident>,
2063 /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2064 /// + the loop that asserts every row matches. One pair per Auto
2065 /// field. Empty when `has_auto == false`.
2066 bulk_auto_uniformity: Vec<TokenStream2>,
2067 /// Identifier of the first Auto field, used as the witness for
2068 /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2069 first_auto_ident: Option<syn::Ident>,
2070 /// `true` if any field on the struct is `Auto<T>`.
2071 has_auto: bool,
2072 /// `true` when the primary-key field's Rust type is `Auto<T>`.
2073 /// Gates `save()` codegen — only Auto PKs let us infer
2074 /// insert-vs-update from the in-memory value.
2075 pk_is_auto: bool,
2076 /// `Assignment` constructors for every non-PK column. Drives the
2077 /// UPDATE branch of `save()`.
2078 update_assignments: Vec<TokenStream2>,
2079 /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2080 /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2081 upsert_update_columns: Vec<TokenStream2>,
2082 primary_key: Option<(syn::Ident, String)>,
2083 column_entries: Vec<ColumnEntry>,
2084 /// Rust-side field names, in declaration order. Used to validate
2085 /// container attributes like `display = "…"`.
2086 field_names: Vec<String>,
2087 /// FK fields on this child model. Drives the reverse-relation
2088 /// helper emit — for each FK, the macro adds an inherent
2089 /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2090 /// method on the parent type.
2091 fk_relations: Vec<FkRelation>,
2092 /// SQL column name of the `#[rustango(soft_delete)]` field, if
2093 /// the model has one. Drives emission of the `soft_delete_on` /
2094 /// `restore_on` inherent methods. At most one such column per
2095 /// model is allowed; collect_fields rejects duplicates.
2096 soft_delete_column: Option<String>,
2097 /// Rust field ident of the `#[rustango(soft_delete)]` field —
2098 /// companion to `soft_delete_column` for emitting predicates
2099 /// that need to read the field off `&self` (e.g. `trashed()`).
2100 soft_delete_field_ident: Option<syn::Ident>,
2101}
2102
2103#[derive(Clone)]
2104struct FkRelation {
2105 /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2106 /// helper is emitted as `impl <ParentType> { … }`.
2107 parent_type: Type,
2108 /// Rust field ident of the FK. Its name is the key for select_related,
2109 /// prefetch and filters; the SQL column may differ (#1936).
2110 field_ident: syn::Ident,
2111 /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2112 /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2113 /// default `ForeignKey<T>` (no explicit K); other kinds when the
2114 /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2115 pk_kind: DetectedKind,
2116 /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2117 /// FK column). Drives the `Some(...)` wrapping in load_related
2118 /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2119 /// the codegen matches the field's declared shape.
2120 nullable: bool,
2121 /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2122 /// override. When set, the reverse helper picks this name instead
2123 /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2124 /// #816 (issue's "Related" note re: per-FK override).
2125 related_name: Option<String>,
2126}
2127
2128fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2129 let root = rustango_root();
2130 let cap = named.named.len();
2131 let mut out = CollectedFields {
2132 field_schemas: Vec::with_capacity(cap),
2133 from_row_inits: Vec::with_capacity(cap),
2134 from_aliased_row_inits: Vec::with_capacity(cap),
2135 from_row_inits_my: Vec::with_capacity(cap),
2136 from_aliased_row_inits_my: Vec::with_capacity(cap),
2137 insert_columns: Vec::with_capacity(cap),
2138 insert_values: Vec::with_capacity(cap),
2139 insert_pushes: Vec::with_capacity(cap),
2140 returning_cols: Vec::new(),
2141 auto_assigns: Vec::new(),
2142 auto_field_idents: Vec::new(),
2143 generated_field_idents: Vec::new(),
2144 first_db_auto: None,
2145 bulk_pushes_no_auto: Vec::with_capacity(cap),
2146 bulk_pushes_all: Vec::with_capacity(cap),
2147 bulk_columns_no_auto: Vec::with_capacity(cap),
2148 bulk_columns_all: Vec::with_capacity(cap),
2149 bulk_fill_in_place: Vec::new(),
2150 rust_filled_idents: Vec::new(),
2151 bulk_auto_uniformity: Vec::new(),
2152 first_auto_ident: None,
2153 has_auto: false,
2154 pk_is_auto: false,
2155 update_assignments: Vec::with_capacity(cap),
2156 upsert_update_columns: Vec::with_capacity(cap),
2157 primary_key: None,
2158 column_entries: Vec::with_capacity(cap),
2159 field_names: Vec::with_capacity(cap),
2160 fk_relations: Vec::new(),
2161 soft_delete_column: None,
2162 soft_delete_field_ident: None,
2163 };
2164
2165 for field in &named.named {
2166 let info = process_field(field, table)?;
2167 out.field_names.push(ident_name(info.ident));
2168 out.field_schemas.push(info.schema);
2169 out.from_row_inits.push(info.from_row_init);
2170 out.from_aliased_row_inits.push(info.from_aliased_row_init);
2171 out.from_row_inits_my.push(info.from_row_init_my);
2172 out.from_aliased_row_inits_my
2173 .push(info.from_aliased_row_init_my);
2174 if let Some(parent_ty) = info.fk_inner.clone() {
2175 out.fk_relations.push(FkRelation {
2176 parent_type: parent_ty,
2177 field_ident: info.ident.clone(),
2178 pk_kind: info.fk_pk_kind,
2179 nullable: info.nullable,
2180 related_name: info.related_name.clone(),
2181 });
2182 }
2183 if info.soft_delete {
2184 if out.soft_delete_column.is_some() {
2185 return Err(syn::Error::new_spanned(
2186 field,
2187 "only one field may be marked `#[rustango(soft_delete)]`",
2188 ));
2189 }
2190 out.soft_delete_column = Some(info.column.clone());
2191 out.soft_delete_field_ident = Some(info.ident.clone());
2192 }
2193 let column = info.column.as_str();
2194 let ident = info.ident;
2195 // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2196 // skip every write path — Postgres recomputes the value
2197 // from EXPR. Push only the column-entry record (so typed
2198 // column constants still exist for filtering / projection)
2199 // and the schema literal (already pushed above) and move
2200 // on. No insert_columns/values, no insert_pushes, no
2201 // bulk_*, no update_assignments, no upsert_update_columns,
2202 // no returning_cols.
2203 if info.generated_as.is_some() {
2204 out.column_entries.push(ColumnEntry {
2205 ident: ident.clone(),
2206 value_ty: info.value_ty.clone(),
2207 name: ident_name(ident),
2208 column: info.column.clone(),
2209 field_type_tokens: info.field_type_tokens,
2210 uuid_column: info.uuid_column,
2211 });
2212 // #1028 — refresh the DB-computed value after insert on
2213 // RETURNING-capable backends. The column joins `returning_cols`
2214 // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2215 // ident is recorded so the `AssignAutoPkPool` impl decodes it
2216 // back into the struct. MySQL has no `INSERT … RETURNING`, so
2217 // it keeps the placeholder until the next read.
2218 out.returning_cols.push(quote!(#column));
2219 out.generated_field_idents
2220 .push((ident.clone(), info.column.clone()));
2221 continue;
2222 }
2223 out.insert_columns.push(quote!(#column));
2224 out.insert_values.push(quote! {
2225 ::core::convert::Into::<#root::core::SqlValue>::into(
2226 ::core::clone::Clone::clone(&self.#ident)
2227 )
2228 });
2229 if info.auto {
2230 out.has_auto = true;
2231 if out.first_auto_ident.is_none() {
2232 out.first_auto_ident = Some(ident.clone());
2233 }
2234 // `default_uuid_v7` (issue #823) generates the PK Rust-side
2235 // before binding, so the value is already in
2236 // `self.#ident` after the insert_push — RETURNING is
2237 // redundant. Skip adding this column to returning_cols /
2238 // auto_assigns / auto_field_idents to avoid (a) an
2239 // unnecessary RETURNING column on every dialect, and (b)
2240 // the MySQL `LAST_INSERT_ID()` path that can only fill an
2241 // integer PK.
2242 // `auto_now_add` / `auto_now` join `default_uuid_v7` here
2243 // for the same reason: they are filled Rust-side below and
2244 // bound, so the value is already in `self` and RETURNING
2245 // would be a redundant column on every dialect.
2246 if !info.default_uuid_v7 && !info.auto_now_add && !info.auto_now {
2247 if out.first_db_auto.is_none() {
2248 out.first_db_auto = auto_inner_type(info.value_ty)
2249 .cloned()
2250 .map(|ty| (ident.clone(), ty));
2251 }
2252 out.returning_cols.push(quote!(#column));
2253 out.auto_field_idents
2254 .push((ident.clone(), info.column.clone()));
2255 out.auto_assigns.push(quote! {
2256 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2257 });
2258 }
2259 if info.default_uuid_v7 {
2260 // Rust-side UUIDv7 generation (issue #823, Eloquent
2261 // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2262 // and bind; Auto::Set → bind the user's value. The
2263 // column is ALWAYS present in the INSERT statement —
2264 // no RETURNING / no DB DEFAULT needed.
2265 out.insert_pushes.push(quote! {
2266 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2267 self.#ident = #root::sql::Auto::Set(
2268 #root::__uuid::Uuid::now_v7(),
2269 );
2270 }
2271 if let #root::sql::Auto::Set(_v) = &self.#ident {
2272 _columns.push(#column);
2273 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2274 ::core::clone::Clone::clone(_v)
2275 ));
2276 }
2277 });
2278 } else if info.auto_now_add || info.auto_now {
2279 // Rust-side write timestamp (#1464), mirroring
2280 // `default_uuid_v7` above. `auto_now` has always bound
2281 // `Utc::now()` on UPDATE but not on INSERT, so the
2282 // first write of an `updated_at` took the DB default
2283 // while every later one took the clock — the same
2284 // column, filled two different ways.
2285 //
2286 // The default is the problem. `CREATE TABLE IF NOT
2287 // EXISTS` leaves an upgraded table's
2288 // `DEFAULT CURRENT_TIMESTAMP` in place, and SQLite's
2289 // ALTER TABLE grammar (RENAME/ADD/DROP) has no
2290 // statement that changes it — so every insert relying
2291 // on that default wrote the legacy shape forever, while
2292 // the migrate sweep converted the rows around it. The
2293 // column went permanently mixed and `ORDER BY` inverted
2294 // the admin audit log (#1616 rework review,
2295 // correctness-001 / dialects-005 / security-002).
2296 //
2297 // Binding it here makes the stale default unreachable
2298 // rather than trying to migrate it: the timestamp comes
2299 // from the application, not the database.
2300 out.insert_pushes.push(quote! {
2301 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2302 self.#ident = #root::sql::Auto::Set(
2303 #root::__chrono::Utc::now(),
2304 );
2305 }
2306 if let #root::sql::Auto::Set(_v) = &self.#ident {
2307 _columns.push(#column);
2308 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2309 ::core::clone::Clone::clone(_v)
2310 ));
2311 }
2312 });
2313 } else {
2314 out.insert_pushes.push(quote! {
2315 if let #root::sql::Auto::Set(_v) = &self.#ident {
2316 _columns.push(#column);
2317 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2318 ::core::clone::Clone::clone(_v)
2319 ));
2320 }
2321 });
2322 }
2323 // …except the timestamp columns, which must appear in BOTH
2324 // paths (#1464). The path is chosen by the *first* Auto
2325 // field — in practice the PK — so a bulk insert of rows
2326 // with an unset PK took the no-auto branch and dropped the
2327 // timestamp column, falling back to the DB default that
2328 // this change exists to stop depending on.
2329 //
2330 // `rows` is borrowed immutably here, so this fills per row
2331 // at push time rather than writing back into the struct. An
2332 // explicitly-Set value is still honoured; only `Unset`
2333 // takes the clock.
2334 // `default_uuid_v7` likewise: its column has no DB default (#1934).
2335 if info.default_uuid_v7 || info.auto_now_add || info.auto_now {
2336 out.rust_filled_idents.push(ident.clone());
2337 }
2338 let rust_fill = if info.default_uuid_v7 {
2339 Some(quote!(#root::__uuid::Uuid::now_v7()))
2340 } else if info.auto_now_add || info.auto_now {
2341 Some(quote!(#root::__chrono::Utc::now()))
2342 } else {
2343 None
2344 };
2345 // Bulk: Auto fields appear in the all-Set path. An unset filled
2346 // field takes its fill there too, not NULL (#1950).
2347 out.bulk_columns_all.push(quote!(#column));
2348 if let Some(fill) = &rust_fill {
2349 let push = quote! {
2350 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2351 match &_row.#ident {
2352 #root::sql::Auto::Set(_v) => ::core::clone::Clone::clone(_v),
2353 #root::sql::Auto::Unset => #fill,
2354 }
2355 ));
2356 };
2357 out.bulk_columns_no_auto.push(quote!(#column));
2358 out.bulk_pushes_no_auto.push(push.clone());
2359 out.bulk_pushes_all.push(push);
2360 } else {
2361 out.bulk_pushes_all.push(quote! {
2362 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2363 ::core::clone::Clone::clone(&_row.#ident)
2364 ));
2365 });
2366 }
2367 if info.default_uuid_v7 {
2368 out.bulk_fill_in_place.push(quote! {
2369 if matches!(&_row.#ident, #root::sql::Auto::Unset) {
2370 _row.#ident = #root::sql::Auto::Set(#root::__uuid::Uuid::now_v7());
2371 }
2372 });
2373 }
2374 // Uniformity check: every row's Auto state must match the
2375 // first row's. Mixed Set/Unset within one bulk_insert is
2376 // rejected here so the column list stays consistent.
2377 // A filled field may be unset on any row, so it is exempt.
2378 let ident_clone = ident.clone();
2379 if rust_fill.is_none() {
2380 out.bulk_auto_uniformity.push(quote! {
2381 for _r in rows.iter().skip(1) {
2382 if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2383 return ::core::result::Result::Err(
2384 #root::sql::ExecError::Sql(
2385 #root::sql::SqlError::BulkAutoMixed
2386 )
2387 );
2388 }
2389 }
2390 });
2391 }
2392 } else {
2393 out.insert_pushes.push(quote! {
2394 _columns.push(#column);
2395 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2396 ::core::clone::Clone::clone(&self.#ident)
2397 ));
2398 });
2399 // Bulk: non-Auto fields appear in BOTH paths.
2400 out.bulk_columns_no_auto.push(quote!(#column));
2401 out.bulk_columns_all.push(quote!(#column));
2402 let push_expr = quote! {
2403 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2404 ::core::clone::Clone::clone(&_row.#ident)
2405 ));
2406 };
2407 out.bulk_pushes_no_auto.push(push_expr.clone());
2408 out.bulk_pushes_all.push(push_expr);
2409 }
2410 if info.primary_key {
2411 if out.primary_key.is_some() {
2412 return Err(syn::Error::new_spanned(
2413 field,
2414 "only one field may be marked `#[rustango(primary_key)]`",
2415 ));
2416 }
2417 out.primary_key = Some((ident.clone(), info.column.clone()));
2418 if info.auto {
2419 out.pk_is_auto = true;
2420 }
2421 } else if info.auto_now_add {
2422 // Immutable post-insert: skip from UPDATE entirely.
2423 } else if info.auto_now {
2424 // `auto_now` columns: bind `chrono::Utc::now()` on every
2425 // UPDATE so the column is always overridden with the
2426 // wall-clock at write time, regardless of what value the
2427 // user left in the struct field.
2428 out.update_assignments.push(quote! {
2429 #root::core::Assignment::new(
2430 #column,
2431 ::core::convert::Into::<#root::core::SqlValue>::into(
2432 #root::__chrono::Utc::now()
2433 ),
2434 )
2435 });
2436 out.upsert_update_columns.push(quote!(#column));
2437 } else {
2438 out.update_assignments.push(quote! {
2439 #root::core::Assignment::new(
2440 #column,
2441 ::core::convert::Into::<#root::core::SqlValue>::into(
2442 ::core::clone::Clone::clone(&self.#ident)
2443 ),
2444 )
2445 });
2446 out.upsert_update_columns.push(quote!(#column));
2447 }
2448 out.column_entries.push(ColumnEntry {
2449 ident: ident.clone(),
2450 value_ty: info.value_ty.clone(),
2451 name: ident_name(ident),
2452 column: info.column.clone(),
2453 field_type_tokens: info.field_type_tokens,
2454 uuid_column: info.uuid_column,
2455 });
2456 }
2457 Ok(out)
2458}
2459
2460fn model_impl_tokens(
2461 struct_name: &syn::Ident,
2462 model_name: &str,
2463 table: &str,
2464 display: Option<&str>,
2465 app_label: Option<&str>,
2466 admin: Option<&AdminAttrs>,
2467 default_order: &[(String, bool, proc_macro2::Span)],
2468 field_schemas: &[TokenStream2],
2469 soft_delete_column: Option<&str>,
2470 permissions: bool,
2471 audit_track: Option<&[String]>,
2472 m2m_relations: &[M2MAttr],
2473 indexes: &[IndexAttr],
2474 checks: &[CheckAttr],
2475 excludes: &[ExcludeAttr],
2476 composite_fks: &[CompositeFkAttr],
2477 generic_fks: &[GenericFkAttr],
2478 scope: Option<&str>,
2479 is_view: bool,
2480 verbose_name: Option<&str>,
2481 verbose_name_plural: Option<&str>,
2482 managed: bool,
2483 base_manager_name: Option<&str>,
2484 order_with_respect_to: Option<&str>,
2485 proxy: bool,
2486 required_db_features: &[String],
2487 required_db_vendor: Option<&str>,
2488 default_related_name: Option<&str>,
2489 db_table_comment: Option<&str>,
2490 get_latest_by: Option<(&str, bool)>,
2491 extra_permissions: &[(String, String)],
2492 default_permissions: &[String],
2493 global_scopes: &[GlobalScopeAttr],
2494 reverse_has_relations: &[ReverseHasAttr],
2495 generic_has_relations: &[GenericHasAttr],
2496) -> TokenStream2 {
2497 let root = rustango_root();
2498 let display_tokens = if let Some(name) = display {
2499 quote!(::core::option::Option::Some(#name))
2500 } else {
2501 quote!(::core::option::Option::None)
2502 };
2503 let app_label_tokens = if let Some(name) = app_label {
2504 quote!(::core::option::Option::Some(#name))
2505 } else {
2506 quote!(::core::option::Option::None)
2507 };
2508 let soft_delete_tokens = if let Some(col) = soft_delete_column {
2509 quote!(::core::option::Option::Some(#col))
2510 } else {
2511 quote!(::core::option::Option::None)
2512 };
2513 let audit_track_tokens = match audit_track {
2514 None => quote!(::core::option::Option::None),
2515 Some(names) => {
2516 let lits = names.iter().map(|n| n.as_str());
2517 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2518 }
2519 };
2520 let admin_tokens = admin_config_tokens(admin);
2521 // Default `tenant` so single-tenant projects (no `scope` attr
2522 // anywhere) keep the v0.24.x behavior. Container-attr parser
2523 // already validated the value is "registry" or "tenant".
2524 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2525 Some("registry") => quote!(#root::core::ModelScope::Registry),
2526 _ => quote!(#root::core::ModelScope::Tenant),
2527 };
2528 let verbose_name_tokens = optional_str(verbose_name);
2529 let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2530 let base_manager_name_tokens = optional_str(base_manager_name);
2531 let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2532 let required_db_features_lits: Vec<&str> =
2533 required_db_features.iter().map(String::as_str).collect();
2534 let required_db_vendor_tokens = optional_str(required_db_vendor);
2535 let default_related_name_tokens = optional_str(default_related_name);
2536 let db_table_comment_tokens = optional_str(db_table_comment);
2537 let get_latest_by_tokens = match get_latest_by {
2538 Some((col, desc)) => {
2539 quote!(::core::option::Option::Some((#col, #desc)))
2540 }
2541 None => quote!(::core::option::Option::None),
2542 };
2543 let extra_permission_tokens: Vec<_> = extra_permissions
2544 .iter()
2545 .map(|(c, l)| quote!((#c, #l)))
2546 .collect();
2547 let default_permission_tokens: Vec<_> = default_permissions
2548 .iter()
2549 .map(|action| quote!(#action))
2550 .collect();
2551 let indexes_tokens = indexes.iter().map(|idx| {
2552 // When no explicit `name = "..."` was given, derive a stable,
2553 // collision-free name from the table + columns
2554 // (`<table>_<col>_<col>_idx`) instead of a shared literal that
2555 // would clash the moment a model declares two unnamed indexes.
2556 // Capped at Postgres's 63-char identifier limit.
2557 let derived_name = idx.name.clone().unwrap_or_else(|| {
2558 let mut n = format!("{table}_{}_idx", idx.columns.join("_"));
2559 if n.len() > 63 {
2560 n.truncate(63);
2561 }
2562 n
2563 });
2564 let name = derived_name.as_str();
2565 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2566 let unique = idx.unique;
2567 // Map the parsed method string onto the IndexMethod enum
2568 // variant — kept at the codegen layer so the IR doesn't
2569 // carry the string form.
2570 let method_variant = match idx.method.as_str() {
2571 "gin" => quote!(#root::core::IndexMethod::Gin),
2572 "gist" => quote!(#root::core::IndexMethod::Gist),
2573 "brin" => quote!(#root::core::IndexMethod::Brin),
2574 "spgist" => quote!(#root::core::IndexMethod::SpGist),
2575 "hash" => quote!(#root::core::IndexMethod::Hash),
2576 "bloom" => quote!(#root::core::IndexMethod::Bloom),
2577 _ => quote!(#root::core::IndexMethod::BTree),
2578 };
2579 let where_clause = match &idx.where_clause {
2580 Some(s) => quote!(::core::option::Option::Some(#s)),
2581 None => quote!(::core::option::Option::None),
2582 };
2583 let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2584 quote! {{
2585 let mut i = #root::core::IndexSchema::new(#name, &[ #(#cols),* ]);
2586 i.unique = #unique;
2587 i.method = #method_variant;
2588 i.where_clause = #where_clause;
2589 i.include = &[ #(#include_lits),* ];
2590 i
2591 }}
2592 });
2593 let checks_tokens = checks.iter().map(|c| {
2594 let name = c.name.as_str();
2595 let expr = c.expr.as_str();
2596 quote!(#root::core::CheckConstraint::new(#name, #expr))
2597 });
2598 let excludes_tokens = excludes.iter().map(|e| {
2599 let name = e.name.as_str();
2600 let using = e.using.as_str();
2601 let element_tokens = e.elements.iter().map(|(col, op)| {
2602 let col_s = col.as_str();
2603 let op_s = op.as_str();
2604 quote!((#col_s, #op_s))
2605 });
2606 let where_tokens = match e.where_clause.as_deref() {
2607 Some(w) => quote!(::core::option::Option::Some(#w)),
2608 None => quote!(::core::option::Option::None),
2609 };
2610 quote! {{
2611 let mut e = #root::core::ExclusionConstraint::new(
2612 #name,
2613 #using,
2614 &[ #(#element_tokens),* ],
2615 );
2616 e.where_clause = #where_tokens;
2617 e
2618 }}
2619 });
2620 let composite_fk_tokens = composite_fks.iter().map(|rel| {
2621 let name = rel.name.as_str();
2622 let to = rel.to.as_str();
2623 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2624 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2625 quote! {
2626 #root::core::CompositeFkRelation::new(
2627 #name,
2628 #to,
2629 &[ #(#from_cols),* ],
2630 &[ #(#on_cols),* ],
2631 )
2632 }
2633 });
2634 let generic_fk_tokens = generic_fks.iter().map(|rel| {
2635 let name = rel.name.as_str();
2636 let ct_col = rel.ct_column.as_str();
2637 let pk_col = rel.pk_column.as_str();
2638 quote!(#root::core::GenericRelation::new(#name, #ct_col, #pk_col))
2639 });
2640 // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2641 // no `#[rustango(default_order = "...")]` attribute was supplied.
2642 let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2643 let col_lit = col.as_str();
2644 quote! { (#col_lit, #desc) }
2645 });
2646
2647 // Issue #820 — `global_scopes` slice literal. Empty when no
2648 // `#[rustango(global_scope(...))]` attribute was supplied. The
2649 // `apply` path is re-emitted verbatim so it resolves in the
2650 // consumer's scope; the name is stored as a string literal.
2651 let global_scope_tokens = global_scopes.iter().map(|s| {
2652 let name = s.name.as_str();
2653 // The slice sits in a nested `const`, where `Self` does not resolve.
2654 let mut apply = s.apply.clone();
2655 if let Some(first) = apply.segments.first_mut() {
2656 if first.ident == "Self" {
2657 first.ident = struct_name.clone();
2658 }
2659 }
2660 quote!(#root::core::GlobalScope::new(#name, #apply))
2661 });
2662
2663 let m2m_tokens = m2m_relations.iter().map(|rel| {
2664 let name = rel.name.as_str();
2665 let to = rel.to.as_str();
2666 let through = rel.through.as_str();
2667 let src = rel.src.as_str();
2668 let dst = rel.dst.as_str();
2669 let auto_create = rel.auto_create;
2670 quote! {{
2671 let mut m = #root::core::M2MRelation::new(#name, #to, #through, #src, #dst);
2672 m.auto_create = #auto_create;
2673 m
2674 }}
2675 });
2676 // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2677 // when the model declares `#[rustango(reverse_has(...))]`. Each
2678 // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2679 // a const expression. Models without reverse_has fall through to
2680 // the trait's empty default — no override emitted.
2681 let reverse_relations_override = if reverse_has_relations.is_empty() {
2682 quote!()
2683 } else {
2684 let entries = reverse_has_relations.iter().map(|rel| {
2685 let name = rel.name.as_str();
2686 let child = &rel.child;
2687 let child_fk_column = rel.child_fk_column.as_str();
2688 let self_pk_column = rel.self_pk_column.as_str();
2689 quote! {
2690 #root::core::ReverseRelation::new(
2691 #name,
2692 <#child as #root::core::Model>::SCHEMA,
2693 #child_fk_column,
2694 #self_pk_column,
2695 )
2696 }
2697 });
2698 quote! {
2699 fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2700 const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2701 RELS
2702 }
2703 }
2704 };
2705 // Issue #830 — `Model::generic_reverse_relations()` override for
2706 // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2707 let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2708 quote!()
2709 } else {
2710 let entries = generic_has_relations.iter().map(|rel| {
2711 let name = rel.name.as_str();
2712 let child = &rel.child;
2713 let ct_column = rel.ct_column.as_str();
2714 let pk_column = rel.pk_column.as_str();
2715 let self_pk_column = rel.self_pk_column.as_str();
2716 quote! {
2717 #root::core::GenericReverseRelation::new(
2718 #name,
2719 <#child as #root::core::Model>::SCHEMA,
2720 #ct_column,
2721 #pk_column,
2722 #self_pk_column,
2723 )
2724 }
2725 });
2726 quote! {
2727 fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2728 const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2729 RELS
2730 }
2731 }
2732 };
2733 let fields_slice = const_slice(
2734 "e!(#root::core::FieldSchema),
2735 field_schemas.iter().cloned(),
2736 );
2737 let m2m_slice = const_slice("e!(#root::core::M2MRelation), m2m_tokens);
2738 let indexes_slice = const_slice("e!(#root::core::IndexSchema), indexes_tokens);
2739 let checks_slice = const_slice("e!(#root::core::CheckConstraint), checks_tokens);
2740 let excludes_slice = const_slice("e!(#root::core::ExclusionConstraint), excludes_tokens);
2741 let composite_fk_slice = const_slice(
2742 "e!(#root::core::CompositeFkRelation),
2743 composite_fk_tokens,
2744 );
2745 let generic_fk_slice = const_slice("e!(#root::core::GenericRelation), generic_fk_tokens);
2746 let audited_update_override = if audit_track.is_some() {
2747 quote! {
2748 fn __rustango_audited_update() -> ::core::option::Option<#root::audit::AuditedUpdate> {
2749 ::core::option::Option::Some(Self::__rustango_update_audited)
2750 }
2751 fn __rustango_audited_delete() -> ::core::option::Option<#root::audit::AuditedDelete> {
2752 ::core::option::Option::Some(Self::__rustango_delete_audited)
2753 }
2754 fn __rustango_audited_create() -> ::core::option::Option<#root::audit::AuditedCreate> {
2755 ::core::option::Option::Some(Self::__rustango_create_audited)
2756 }
2757 }
2758 } else {
2759 quote!()
2760 };
2761 let global_scope_slice = const_slice("e!(#root::core::GlobalScope), global_scope_tokens);
2762 quote! {
2763 impl #root::core::Model for #struct_name {
2764 const SCHEMA: &'static #root::core::ModelSchema = &{
2765 #root::core::ModelSchema::from_parts(#root::core::ModelSchemaParts {
2766 name: #model_name,
2767 table: #table,
2768 fields: #fields_slice,
2769 display: #display_tokens,
2770 app_label: #app_label_tokens,
2771 admin: #admin_tokens,
2772 soft_delete_column: #soft_delete_tokens,
2773 permissions: #permissions,
2774 audit_track: #audit_track_tokens,
2775 m2m: #m2m_slice,
2776 indexes: #indexes_slice,
2777 check_constraints: #checks_slice,
2778 exclusion_constraints: #excludes_slice,
2779 composite_relations: #composite_fk_slice,
2780 generic_relations: #generic_fk_slice,
2781 scope: #scope_tokens,
2782 default_order: &[ #(#default_order_tokens),* ],
2783 is_view: #is_view,
2784 verbose_name: #verbose_name_tokens,
2785 verbose_name_plural: #verbose_name_plural_tokens,
2786 managed: #managed,
2787 base_manager_name: #base_manager_name_tokens,
2788 order_with_respect_to: #order_with_respect_to_tokens,
2789 proxy: #proxy,
2790 required_db_features: &[ #(#required_db_features_lits),* ],
2791 required_db_vendor: #required_db_vendor_tokens,
2792 default_related_name: #default_related_name_tokens,
2793 db_table_comment: #db_table_comment_tokens,
2794 get_latest_by: #get_latest_by_tokens,
2795 extra_permissions: &[ #(#extra_permission_tokens),* ],
2796 default_permissions: &[ #(#default_permission_tokens),* ],
2797 global_scopes: #global_scope_slice,
2798 })
2799 };
2800
2801 #reverse_relations_override
2802 #generic_reverse_relations_override
2803 #audited_update_override
2804 }
2805 }
2806}
2807
2808/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2809/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2810/// otherwise a static reference to a populated `AdminConfig`.
2811fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2812 let root = rustango_root();
2813 let Some(admin) = admin else {
2814 return quote!(::core::option::Option::None);
2815 };
2816
2817 let list_display = admin
2818 .list_display
2819 .as_ref()
2820 .map(|(v, _)| v.as_slice())
2821 .unwrap_or(&[]);
2822 let list_display_lits = list_display.iter().map(|s| s.as_str());
2823
2824 let search_fields = admin
2825 .search_fields
2826 .as_ref()
2827 .map(|(v, _)| v.as_slice())
2828 .unwrap_or(&[]);
2829 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2830
2831 let readonly_fields = admin
2832 .readonly_fields
2833 .as_ref()
2834 .map(|(v, _)| v.as_slice())
2835 .unwrap_or(&[]);
2836 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2837
2838 let list_filter = admin
2839 .list_filter
2840 .as_ref()
2841 .map(|(v, _)| v.as_slice())
2842 .unwrap_or(&[]);
2843 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2844
2845 let actions = admin
2846 .actions
2847 .as_ref()
2848 .map(|(v, _)| v.as_slice())
2849 .unwrap_or(&[]);
2850 let actions_lits = actions.iter().map(|s| s.as_str());
2851
2852 let fieldsets = admin
2853 .fieldsets
2854 .as_ref()
2855 .map(|(v, _)| v.as_slice())
2856 .unwrap_or(&[]);
2857 let fieldset_tokens = const_slice(
2858 "e!(#root::core::Fieldset),
2859 fieldsets.iter().map(|(title, fields)| {
2860 let title = title.as_str();
2861 let field_lits = fields.iter().map(String::as_str);
2862 quote!(#root::core::Fieldset::new(#title, &[ #( #field_lits ),* ]))
2863 }),
2864 );
2865
2866 let list_display_links = admin
2867 .list_display_links
2868 .as_ref()
2869 .map(|(v, _)| v.as_slice())
2870 .unwrap_or(&[]);
2871 let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2872
2873 let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2874 let actions_on_top = admin.actions_on_top.unwrap_or(true);
2875 let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2876 let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2877
2878 let prepopulated = admin
2879 .prepopulated_fields
2880 .as_ref()
2881 .map(|(v, _)| v.as_slice())
2882 .unwrap_or(&[]);
2883 let prepopulated_tokens = const_slice(
2884 "e!(#root::core::PrepopulatedField),
2885 prepopulated.iter().map(|(target, sources)| {
2886 let target = target.as_str();
2887 let source_lits = sources.iter().map(String::as_str);
2888 quote!(#root::core::PrepopulatedField::new(#target, &[ #( #source_lits ),* ]))
2889 }),
2890 );
2891
2892 let raw_id_fields = admin
2893 .raw_id_fields
2894 .as_ref()
2895 .map(|(v, _)| v.as_slice())
2896 .unwrap_or(&[]);
2897 let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2898
2899 let autocomplete_fields = admin
2900 .autocomplete_fields
2901 .as_ref()
2902 .map(|(v, _)| v.as_slice())
2903 .unwrap_or(&[]);
2904 let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
2905
2906 // #352 — list_select_related accepts "all" | "none" | "field, field, …".
2907 let list_select_related_tokens = match admin.list_select_related.as_deref() {
2908 None | Some("all") => quote!(#root::core::ListSelectRelated::All),
2909 Some("none") => quote!(#root::core::ListSelectRelated::None),
2910 Some(raw) => {
2911 let names: Vec<&str> = raw
2912 .split(',')
2913 .map(str::trim)
2914 .filter(|s| !s.is_empty())
2915 .collect();
2916 quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
2917 }
2918 };
2919
2920 // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
2921 // a Vec<(String, String)>. Empty / unset → no overrides.
2922 let formfield_pairs: Vec<(&str, &str)> = admin
2923 .formfield_overrides
2924 .as_ref()
2925 .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
2926 .unwrap_or_default();
2927 let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
2928 let field = *field;
2929 let widget = *widget;
2930 quote!((#field, #widget))
2931 });
2932
2933 let list_per_page = admin.list_per_page.unwrap_or(0);
2934
2935 let ordering_pairs = admin
2936 .ordering
2937 .as_ref()
2938 .map(|(v, _)| v.as_slice())
2939 .unwrap_or(&[]);
2940 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
2941 let name = name.as_str();
2942 let desc = *desc;
2943 quote!((#name, #desc))
2944 });
2945
2946 quote! {
2947 ::core::option::Option::Some({
2948 const ADMIN: &#root::core::AdminConfig = &{
2949 #root::core::AdminConfig::from_parts(#root::core::AdminConfigParts {
2950 list_display: &[ #( #list_display_lits ),* ],
2951 search_fields: &[ #( #search_fields_lits ),* ],
2952 list_per_page: #list_per_page,
2953 ordering: &[ #( #ordering_tokens ),* ],
2954 readonly_fields: &[ #( #readonly_fields_lits ),* ],
2955 list_filter: &[ #( #list_filter_lits ),* ],
2956 actions: &[ #( #actions_lits ),* ],
2957 fieldsets: #fieldset_tokens,
2958 list_display_links: &[ #( #list_display_links_lits ),* ],
2959 search_help_text: #search_help_text,
2960 actions_on_top: #actions_on_top,
2961 actions_on_bottom: #actions_on_bottom,
2962 date_hierarchy: #date_hierarchy,
2963 prepopulated_fields: #prepopulated_tokens,
2964 raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
2965 autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
2966 list_select_related: #list_select_related_tokens,
2967 formfield_overrides: &[ #( #formfield_tokens ),* ],
2968 })
2969 };
2970 ADMIN
2971 })
2972 }
2973}
2974
2975fn inherent_impl_tokens(
2976 struct_name: &syn::Ident,
2977 fields: &CollectedFields,
2978 primary_key: Option<&(syn::Ident, String)>,
2979 column_consts: &TokenStream2,
2980 audited_fields: Option<&[&ColumnEntry]>,
2981 upsert_unique: Option<&[String]>,
2982 manager_fns: &[syn::Ident],
2983) -> TokenStream2 {
2984 let root = rustango_root();
2985 // Audit-emit fragments threaded into write paths. Non-empty only
2986 // when the model carries `#[rustango(audit(...))]`. They reborrow
2987 // `_executor` (a `&mut PgConnection` for audited models — the
2988 // macro switches the signature below) so the data write and the
2989 // audit INSERT both run on the same caller-supplied connection.
2990 let executor_passes_to_data_write = if audited_fields.is_some() {
2991 quote!(&mut *_executor)
2992 } else {
2993 quote!(_executor)
2994 };
2995 let executor_param = if audited_fields.is_some() {
2996 quote!(_executor: &mut #root::sql::sqlx::PgConnection)
2997 } else {
2998 quote!(_executor: _E)
2999 };
3000 let executor_generics = if audited_fields.is_some() {
3001 quote!()
3002 } else {
3003 quote!(<'_c, _E>)
3004 };
3005 let executor_where = if audited_fields.is_some() {
3006 quote!()
3007 } else {
3008 quote! {
3009 where
3010 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
3011 }
3012 };
3013 // For audited models the `_on` methods take `&mut PgConnection`, so
3014 // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
3015 // must acquire a connection first. Non-audited models keep the
3016 // direct delegation since `&PgPool` IS an Executor.
3017 let pool_to_save_on = if audited_fields.is_some() {
3018 quote! {
3019 let mut _conn = pool.acquire().await?;
3020 self.save_on(&mut *_conn).await
3021 }
3022 } else {
3023 quote!(self.save_on(pool).await)
3024 };
3025 let pool_to_insert_on = if audited_fields.is_some() {
3026 quote! {
3027 let mut _conn = pool.acquire().await?;
3028 self.insert_on(&mut *_conn).await
3029 }
3030 } else {
3031 quote!(self.insert_on(pool).await)
3032 };
3033 let pool_to_delete_on = if audited_fields.is_some() {
3034 quote! {
3035 let mut _conn = pool.acquire().await?;
3036 self.delete_on(&mut *_conn).await
3037 }
3038 } else {
3039 quote!(self.delete_on(pool).await)
3040 };
3041 let pool_to_bulk_insert_on = if audited_fields.is_some() {
3042 quote! {
3043 let mut _tx = pool.begin().await?;
3044 Self::bulk_insert_on(rows, &mut *_tx).await?;
3045 _tx.commit().await?;
3046 ::core::result::Result::Ok(())
3047 }
3048 } else {
3049 quote!(Self::bulk_insert_on(rows, pool).await)
3050 };
3051 // Pre-existing bug surfaced by batch 22's first audited Auto<T>
3052 // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
3053 // directly, but `upsert_on` for audited models takes
3054 // `&mut PgConnection` (the audit emit needs a real connection).
3055 // Add the missing acquire shim to keep audited Auto-PK upsert
3056 // compiling.
3057 let pool_to_upsert_on = if audited_fields.is_some() {
3058 quote! {
3059 let mut _tx = pool.begin().await?;
3060 self.upsert_on(&mut *_tx).await?;
3061 _tx.commit().await?;
3062 ::core::result::Result::Ok(())
3063 }
3064 } else {
3065 quote!(self.upsert_on(pool).await)
3066 };
3067
3068 // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
3069 // (audit-on-connection over &Pool needs a bi-dialect transaction
3070 // helper, deferred). Two body shapes:
3071 // - has_auto: build InsertQuery skipping Auto::Unset columns,
3072 // request Auto cols in `returning`, dispatch via
3073 // `insert_returning_pool`, then on the returned `PgRow` /
3074 // `MySqlAutoId(id)` enum — pull each Auto field from the PG
3075 // row OR drop the single i64 into the first Auto field on MySQL
3076 // (multi-Auto models on MySQL error at runtime since
3077 // `LAST_INSERT_ID()` only reports one)
3078 // - non-Auto: build InsertQuery with explicit columns/values and
3079 // call `insert_pool` (no returning needed)
3080 // pool_insert_method body for the audited Auto-PK case is moved
3081 // to after audit_pair_tokens / audit_pk_to_string (they live
3082 // ~150 lines below). This block keeps the non-audited and
3083 // non-Auto branches in place — the audited Auto-PK arm is
3084 // computed below and merged via the dispatch helper variable.
3085 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
3086 // Audited models with explicit (non-Auto) PKs go through
3087 // the non-Auto insert path below — the audit emit is one
3088 // round-trip after the INSERT inside the same tx via
3089 // audit::save_one_with_audit? No, INSERT semantics
3090 // differ. For non-Auto PK + audited, route through a
3091 // dedicated insert + audit emit on the same tx, but defer
3092 // the macro emission to the audit-bundle-aware block below
3093 // — this `quote!()` placeholder gets overwritten there.
3094 quote!()
3095 } else if audited_fields.is_some() && fields.has_auto {
3096 // Audited Auto-PK insert_pool — assembled after the audit
3097 // bundles. Placeholder; real emission below.
3098 quote!()
3099 } else if fields.has_auto {
3100 let pushes = &fields.insert_pushes;
3101 let returning_cols = &fields.returning_cols;
3102 // A row not inserted was never saved, so a Rust-filled value must not stay (#1937).
3103 let filled = &fields.rust_filled_idents;
3104 let was_unset: Vec<syn::Ident> = filled
3105 .iter()
3106 .map(|i| quote::format_ident!("__{}_was_unset", ident_name(i)))
3107 .collect();
3108 let insert_or_ignore_call = quote! {
3109 #( let #was_unset = matches!(&self.#filled, #root::sql::Auto::Unset); )*
3110 let mut _columns: ::std::vec::Vec<&'static str> =
3111 ::std::vec::Vec::new();
3112 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3113 ::std::vec::Vec::new();
3114 #( #pushes )*
3115 let _query = #root::core::InsertQuery::new(
3116 <Self as #root::core::Model>::SCHEMA,
3117 _columns,
3118 _values,
3119 );
3120 let _result = #root::sql::insert_or_ignore(pool, &_query).await;
3121 if !matches!(_result, ::core::result::Result::Ok(true)) {
3122 #( if #was_unset { self.#filled = #root::sql::Auto::Unset; } )*
3123 }
3124 _result
3125 };
3126 // When every `Auto<T>` field is filled Rust-side
3127 // (`default_uuid_v7`, issue #823), there is no column to read
3128 // back from the database — `returning_cols` is empty. Route
3129 // through plain `insert_pool` instead of
3130 // `insert_returning_pool` to skip the redundant RETURNING /
3131 // LAST_INSERT_ID round-trip.
3132 if fields.returning_cols.is_empty() {
3133 quote! {
3134 /// Insert this row against either backend. Every
3135 /// `Auto<T>` PK on this model is filled Rust-side
3136 /// (e.g. `default_uuid_v7`) before binding, so no
3137 /// RETURNING round-trip is needed.
3138 ///
3139 /// # Errors
3140 /// As [`Self::insert`].
3141 pub async fn insert_pool(
3142 &mut self,
3143 pool: &#root::sql::Pool,
3144 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3145 let mut _columns: ::std::vec::Vec<&'static str> =
3146 ::std::vec::Vec::new();
3147 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3148 ::std::vec::Vec::new();
3149 #( #pushes )*
3150 let _query = #root::core::InsertQuery::new(
3151 <Self as #root::core::Model>::SCHEMA,
3152 _columns,
3153 _values,
3154 );
3155 #root::sql::insert_pool(pool, &_query).await
3156 }
3157
3158 /// Eloquent `Model::insertOrIgnore()` — same shape
3159 /// as the auto-PK branch above. Returns `Ok(true)`
3160 /// when inserted, `Ok(false)` when a conflict caused
3161 /// the INSERT to silently skip.
3162 ///
3163 /// # Errors
3164 /// As [`Self::insert`].
3165 pub async fn insert_or_ignore(
3166 &mut self,
3167 pool: &#root::sql::Pool,
3168 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3169 #insert_or_ignore_call
3170 }
3171 }
3172 } else {
3173 quote! {
3174 /// Insert this row against either backend, populating any
3175 /// `Auto<T>` PK from the auto-assigned value.
3176 ///
3177 /// # Errors
3178 /// As [`Self::insert`].
3179 pub async fn insert_pool(
3180 &mut self,
3181 pool: &#root::sql::Pool,
3182 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3183 let mut _columns: ::std::vec::Vec<&'static str> =
3184 ::std::vec::Vec::new();
3185 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3186 ::std::vec::Vec::new();
3187 #( #pushes )*
3188 let _query = #root::core::InsertQuery::new(
3189 <Self as #root::core::Model>::SCHEMA,
3190 _columns,
3191 _values,
3192 )
3193 .returning(::std::vec![ #( #returning_cols ),* ]);
3194 let _result = #root::sql::insert_returning_pool(
3195 pool, &_query,
3196 ).await?;
3197 #root::sql::apply_auto_pk(_result, self)
3198 }
3199
3200 /// Eloquent `Model::insertOrIgnore()` — INSERT this
3201 /// row or silently skip on unique-constraint
3202 /// violation. Returns `Ok(true)` when a row was
3203 /// inserted, `Ok(false)` when a conflict caused the
3204 /// INSERT to silently skip.
3205 ///
3206 /// **Caveat on auto-PK models**: when the row is
3207 /// skipped (conflict), this instance's `Auto<T>`
3208 /// fields stay `Unset` — no PK is back-populated
3209 /// because the server didn't auto-assign one. For
3210 /// "insert then read back the PK or the existing
3211 /// row's PK", use the `upsert` family or
3212 /// `get_or_create`.
3213 ///
3214 /// # Errors
3215 /// As [`Self::insert`].
3216 pub async fn insert_or_ignore(
3217 &mut self,
3218 pool: &#root::sql::Pool,
3219 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3220 #insert_or_ignore_call
3221 }
3222 }
3223 }
3224 } else {
3225 let insert_columns = &fields.insert_columns;
3226 let insert_values = &fields.insert_values;
3227 quote! {
3228 /// Insert this row into its table against either backend.
3229 /// Equivalent to [`Self::insert`] but takes
3230 /// [`#root::sql::Pool`].
3231 ///
3232 /// # Errors
3233 /// As [`Self::insert`].
3234 pub async fn insert_pool(
3235 &self,
3236 pool: &#root::sql::Pool,
3237 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3238 let _query = #root::core::InsertQuery::new(
3239 <Self as #root::core::Model>::SCHEMA,
3240 ::std::vec![ #( #insert_columns ),* ],
3241 ::std::vec![ #( #insert_values ),* ],
3242 );
3243 #root::sql::insert_pool(pool, &_query).await
3244 }
3245
3246 /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3247 /// or silently skip on unique-constraint violation. Maps
3248 /// to per-dialect "INSERT ... DO NOTHING on conflict":
3249 /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3250 /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL an
3251 /// `ON DUPLICATE KEY UPDATE` that leaves the row as is.
3252 ///
3253 /// Returns `Ok(true)` when a row was inserted,
3254 /// `Ok(false)` when a conflict caused the INSERT to
3255 /// silently skip.
3256 ///
3257 /// Use for "create-if-absent" patterns where you don't
3258 /// need the row back. For "find-or-create with the row
3259 /// returned", use the queryset-level
3260 /// `crate::sql::get_or_create` free function.
3261 ///
3262 /// # Errors
3263 /// As [`Self::insert`], plus any dialect-specific
3264 /// translation error from the ConflictClause writer.
3265 pub async fn insert_or_ignore(
3266 &self,
3267 pool: &#root::sql::Pool,
3268 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3269 let _query = #root::core::InsertQuery::new(
3270 <Self as #root::core::Model>::SCHEMA,
3271 ::std::vec![ #( #insert_columns ),* ],
3272 ::std::vec![ #( #insert_values ),* ],
3273 );
3274 #root::sql::insert_or_ignore(pool, &_query).await
3275 }
3276 }
3277 };
3278
3279 // pool_save_method moved to after audit_pair_tokens /
3280 // audit_pk_to_string (they live ~70 lines below) — needed for
3281 // the audited branch which builds an UpdateQuery + PendingEntry
3282 // and dispatches via audit::save_one_with_audit.
3283
3284 // pool_delete_method moved to after audit_pair_tokens / audit_pk_to_string
3285 // are computed (they live ~80 lines below).
3286
3287 // Build the (column, JSON value) pair list used by every
3288 // snapshot-style audit emission. Reused across delete_on,
3289 // soft_delete_on, restore_on, and (later) bulk paths. Empty
3290 // when the model isn't audited.
3291 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3292 .map(|tracked| {
3293 tracked
3294 .iter()
3295 .map(|c| {
3296 let column_lit = c.column.as_str();
3297 let ident = &c.ident;
3298 quote! {
3299 (
3300 #column_lit,
3301 #root::__serde_json::to_value(&self.#ident)
3302 .unwrap_or(#root::__serde_json::Value::Null),
3303 )
3304 }
3305 })
3306 .collect()
3307 })
3308 .unwrap_or_default();
3309 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3310 if fields.pk_is_auto {
3311 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3312 } else {
3313 quote!(::std::format!("{}", &self.#pk_ident))
3314 }
3315 } else {
3316 quote!(::std::string::String::new())
3317 };
3318 // `__rustango_audit_entry`: the one place a snapshot `PendingEntry`
3319 // is built, so every audited write path records the same shape.
3320 let delete_audited_body = if primary_key.is_some() {
3321 quote! {
3322 ::std::boxed::Box::pin(#root::audit::delete_many_with_audit::<Self>(
3323 pool,
3324 query,
3325 |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3326 ))
3327 }
3328 } else {
3329 quote! {
3330 let _ = (pool, query);
3331 ::std::boxed::Box::pin(async {
3332 ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3333 table: <Self as #root::core::Model>::SCHEMA.table,
3334 })
3335 })
3336 }
3337 };
3338 let update_audited_body = if primary_key.is_some() {
3339 quote! {
3340 ::std::boxed::Box::pin(#root::audit::update_many_with_audit::<Self>(
3341 pool,
3342 query,
3343 move |_r: &Self| _r.__rustango_audit_entry(op),
3344 ))
3345 }
3346 } else {
3347 quote! {
3348 let _ = (pool, query, op);
3349 ::std::boxed::Box::pin(async {
3350 ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3351 table: <Self as #root::core::Model>::SCHEMA.table,
3352 })
3353 })
3354 }
3355 };
3356 let audit_entry_method = if audited_fields.is_some() {
3357 let pairs = audit_pair_tokens.iter();
3358 let pk_str = audit_pk_to_string.clone();
3359 quote! {
3360 /// Snapshot audit entry for `operation` on this row.
3361 #[doc(hidden)]
3362 pub fn __rustango_audit_entry(
3363 &self,
3364 operation: #root::audit::AuditOp,
3365 ) -> #root::audit::PendingEntry {
3366 #root::audit::PendingEntry {
3367 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3368 entity_pk: #pk_str,
3369 operation,
3370 source: #root::audit::current_source(),
3371 changes: #root::audit::snapshot_changes(&[
3372 #( #pairs ),*
3373 ]),
3374 }
3375 }
3376
3377 /// Audited bulk `UPDATE`, behind `Model::__rustango_audited_update`.
3378 #[doc(hidden)]
3379 pub fn __rustango_update_audited<'a>(
3380 pool: &'a #root::sql::Pool,
3381 query: &'a #root::core::UpdateQuery,
3382 op: #root::audit::AuditOp,
3383 ) -> ::std::pin::Pin<::std::boxed::Box<
3384 dyn ::core::future::Future<
3385 Output = ::core::result::Result<u64, #root::sql::ExecError>,
3386 > + ::core::marker::Send + 'a,
3387 >> {
3388 #update_audited_body
3389 }
3390
3391 /// Audited `create` entry for a new row, behind `Model::__rustango_audited_create`.
3392 #[doc(hidden)]
3393 pub fn __rustango_create_audited<'a, 't>(
3394 tx: &'a mut #root::sql::PoolTx<'t>,
3395 pk: #root::core::SqlValue,
3396 ) -> ::std::pin::Pin<::std::boxed::Box<
3397 dyn ::core::future::Future<
3398 Output = ::core::result::Result<(), #root::sql::ExecError>,
3399 > + ::core::marker::Send + 'a,
3400 >> {
3401 ::std::boxed::Box::pin(#root::audit::record_create::<Self>(
3402 tx,
3403 <Self as #root::core::Model>::SCHEMA,
3404 pk,
3405 |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Create),
3406 ))
3407 }
3408
3409 /// Audited bulk `DELETE`, behind `Model::__rustango_audited_delete`.
3410 #[doc(hidden)]
3411 pub fn __rustango_delete_audited<'a>(
3412 pool: &'a #root::sql::Pool,
3413 query: &'a #root::core::DeleteQuery,
3414 ) -> ::std::pin::Pin<::std::boxed::Box<
3415 dyn ::core::future::Future<
3416 Output = ::core::result::Result<u64, #root::sql::ExecError>,
3417 > + ::core::marker::Send + 'a,
3418 >> {
3419 #delete_audited_body
3420 }
3421 }
3422 } else {
3423 quote!()
3424 };
3425 // Builds `_audit_entry`; `written` is the soft-delete column's new
3426 // value, recorded in place of the stale `self` field.
3427 let make_entry = |op_path: TokenStream2, written: Option<TokenStream2>| -> TokenStream2 {
3428 if let (Some(v), Some(col)) = (written, fields.soft_delete_column.as_deref()) {
3429 quote! {
3430 let mut _audit_entry = self.__rustango_audit_entry(#op_path);
3431 _audit_entry.set_tracked(#col, #v);
3432 }
3433 } else {
3434 quote!(let _audit_entry = self.__rustango_audit_entry(#op_path);)
3435 }
3436 };
3437 let soft_deleted_json = quote!(#root::__serde_json::to_value(&_now)
3438 .unwrap_or(#root::__serde_json::Value::Null));
3439 let restored_json = quote!(#root::__serde_json::Value::Null);
3440 // `_on` emit; `affected` paths skip the row when nothing changed.
3441 let make_op_emit =
3442 |op_path: TokenStream2, written: Option<TokenStream2>, affected: bool| -> TokenStream2 {
3443 if audited_fields.is_none() {
3444 return quote!();
3445 }
3446 let entry = make_entry(op_path, written);
3447 let emit = quote! {
3448 #entry
3449 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3450 };
3451 if affected {
3452 quote!(if _affected > 0 { #emit })
3453 } else {
3454 emit
3455 }
3456 };
3457 let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create), None, false);
3458 let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete), None, true);
3459 let audit_softdelete_emit = make_op_emit(
3460 quote!(#root::audit::AuditOp::SoftDelete),
3461 Some(soft_deleted_json.clone()),
3462 true,
3463 );
3464 let audit_restore_emit = make_op_emit(
3465 quote!(#root::audit::AuditOp::Restore),
3466 Some(restored_json.clone()),
3467 true,
3468 );
3469 // `&Pool` update runner for soft_delete / restore: one tx with the
3470 // audit row when audited, a plain UPDATE otherwise.
3471 let make_pool_update = |op_path: TokenStream2, written: TokenStream2| -> TokenStream2 {
3472 if audited_fields.is_some() {
3473 let entry = make_entry(op_path, Some(written));
3474 quote! {
3475 #entry
3476 #root::audit::save_one_with_audit(pool, &_query, &_audit_entry).await
3477 }
3478 } else {
3479 quote!(#root::sql::update_pool(pool, &_query).await)
3480 }
3481 };
3482 let pool_softdelete_run =
3483 make_pool_update(quote!(#root::audit::AuditOp::SoftDelete), soft_deleted_json);
3484 let pool_restore_run = make_pool_update(quote!(#root::audit::AuditOp::Restore), restored_json);
3485
3486 // `save_pool(&Pool)` — emitted for every model with a PK.
3487 // Audited Auto-PK models are deferred (the Auto::Unset →
3488 // insert_pool path needs the audited-insert flow from a future
3489 // batch). Three body shapes:
3490 // - non-audited, plain PK: build UpdateQuery + dispatch through
3491 // sql::update_pool
3492 // - non-audited, Auto-PK: same, but Auto::Unset routes to
3493 // self.insert_pool which already handles RETURNING / LAST_INSERT_ID
3494 // - audited, plain PK: build UpdateQuery + PendingEntry, dispatch
3495 // through audit::save_one_with_audit (per-backend tx wraps
3496 // UPDATE + audit emit atomically). Snapshot-style audit (post-
3497 // write field values) — diff-style audit (with pre-UPDATE
3498 // SELECT for `before` values) needs per-tracked-column codegen
3499 // that doesn't fit the runtime-helper pattern; legacy &PgPool
3500 // `save` keeps the diff for now.
3501 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3502 let pk_column_lit = pk_col.as_str();
3503 let assignments = &fields.update_assignments;
3504 if audited_fields.is_some() {
3505 if fields.pk_is_auto {
3506 // Auto-PK + audited: defer. The Auto::Unset insert
3507 // path needs a transactional INSERT + LAST_INSERT_ID
3508 // + audit emit flow — that's a follow-up batch.
3509 quote!()
3510 } else {
3511 let pairs = audit_pair_tokens.iter();
3512 let pairs2 = audit_pair_tokens.iter();
3513 let pk_str = audit_pk_to_string.clone();
3514 let pk_str2 = audit_pk_to_string.clone();
3515 quote! {
3516 /// Save (UPDATE) this row against either backend
3517 /// with audit emission inside the same transaction.
3518 /// Bi-dialect counterpart of [`Self::save`] for
3519 /// audited models with non-`Auto<T>` PKs.
3520 ///
3521 /// Captures **post-write** field state (snapshot
3522 /// audit). The legacy &PgPool [`Self::save`]
3523 /// captures BEFORE+AFTER for true diff audit;
3524 /// porting that to the &Pool path needs runtime
3525 /// per-tracked-column decoding and is deferred.
3526 ///
3527 /// # Errors
3528 /// As [`Self::save`].
3529 pub async fn save_pool(
3530 &mut self,
3531 pool: &#root::sql::Pool,
3532 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3533 let _query = #root::core::UpdateQuery::new(
3534 <Self as #root::core::Model>::SCHEMA,
3535 ::std::vec![ #( #assignments ),* ],
3536 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3537 #pk_column_lit,
3538 #root::core::Op::Eq,
3539 ::core::convert::Into::<#root::core::SqlValue>::into(
3540 ::core::clone::Clone::clone(&self.#pk_ident)
3541 ),
3542 )),
3543 );
3544 let _audit_entry = #root::audit::PendingEntry {
3545 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3546 entity_pk: #pk_str,
3547 operation: #root::audit::AuditOp::Update,
3548 source: #root::audit::current_source(),
3549 changes: #root::audit::snapshot_changes(&[
3550 #( #pairs ),*
3551 ]),
3552 };
3553 let _affected = #root::audit::save_one_with_audit(
3554 pool, &_query, &_audit_entry,
3555 ).await?;
3556 ::core::result::Result::Ok(_affected)
3557 }
3558
3559 /// `save_pool` narrowed to a Rust-field allowlist — issue #66.
3560 /// Audit emission shrinks to the same column set so
3561 /// the audit log reflects exactly what was written.
3562 ///
3563 /// # Errors
3564 /// As [`Self::save_pool`], plus
3565 /// [`#root::core::QueryError::UnknownField`] wrapped
3566 /// in `ExecError::Query` for unknown field names.
3567 pub async fn save_partial(
3568 &mut self,
3569 fields: &[&str],
3570 pool: &#root::sql::Pool,
3571 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3572 if fields.is_empty() {
3573 #root::__tracing::warn!(
3574 target: "rustango::save_partial",
3575 model = <Self as #root::core::Model>::SCHEMA.name,
3576 "save_partial called with empty field list — no-op"
3577 );
3578 return ::core::result::Result::Ok(0);
3579 }
3580 let _schema = <Self as #root::core::Model>::SCHEMA;
3581 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3582 ::std::collections::HashSet::with_capacity(fields.len());
3583 for f in fields {
3584 match _schema.field(f) {
3585 ::core::option::Option::Some(fs) => {
3586 _wanted_cols.insert(fs.column);
3587 }
3588 ::core::option::Option::None => {
3589 return ::core::result::Result::Err(
3590 #root::sql::ExecError::Query(
3591 #root::core::QueryError::UnknownField {
3592 model: _schema.name,
3593 field: (*f).to_owned(),
3594 }
3595 )
3596 );
3597 }
3598 }
3599 }
3600 let _full: ::std::vec::Vec<#root::core::Assignment> =
3601 ::std::vec![ #( #assignments ),* ];
3602 let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3603 .into_iter()
3604 .filter(|a| _wanted_cols.contains(a.column))
3605 .collect();
3606 if _filtered.is_empty() {
3607 #root::__tracing::warn!(
3608 target: "rustango::save_partial",
3609 model = _schema.name,
3610 "save_partial: every named field maps to a non-assignable column — no-op"
3611 );
3612 return ::core::result::Result::Ok(0);
3613 }
3614 let _query = #root::core::UpdateQuery::new(
3615 _schema,
3616 _filtered,
3617 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3618 #pk_column_lit,
3619 #root::core::Op::Eq,
3620 ::core::convert::Into::<#root::core::SqlValue>::into(
3621 ::core::clone::Clone::clone(&self.#pk_ident)
3622 ),
3623 )),
3624 );
3625 // Narrow the audit snapshot to the same column set.
3626 let _all_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3627 ::std::vec![ #( #pairs2 ),* ];
3628 let _narrowed: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3629 _all_pairs
3630 .into_iter()
3631 .filter(|(col, _)| _wanted_cols.contains(col))
3632 .collect();
3633 let _audit_entry = #root::audit::PendingEntry {
3634 entity_table: _schema.table,
3635 entity_pk: #pk_str2,
3636 operation: #root::audit::AuditOp::Update,
3637 source: #root::audit::current_source(),
3638 changes: #root::audit::snapshot_changes(&_narrowed),
3639 };
3640 let _affected = #root::audit::save_one_with_audit(
3641 pool, &_query, &_audit_entry,
3642 ).await?;
3643 ::core::result::Result::Ok(_affected)
3644 }
3645
3646 /// Typed-column counterpart of [`Self::save_partial`] —
3647 /// issue #67. `fields` is a tuple of [`Column`]
3648 /// constants whose `Model` matches `Self`; typos and
3649 /// model mismatches surface at *compile time*
3650 /// (`Author::name` inside a `Post::save_partial_typed`
3651 /// call is a type error, no runtime check).
3652 ///
3653 /// ```ignore
3654 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3655 /// ```
3656 ///
3657 /// Lowers to [`Self::save_partial`] under the hood;
3658 /// audit narrowing + every other semantic is identical.
3659 ///
3660 /// [`Column`]: #root::core::Column
3661 ///
3662 /// # Errors
3663 /// As [`Self::save_partial`].
3664 pub async fn save_partial_typed<
3665 L: #root::core::TypedFieldList<Self>,
3666 >(
3667 &mut self,
3668 fields: L,
3669 pool: &#root::sql::Pool,
3670 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3671 let _names = fields.rust_field_names();
3672 let _refs: ::std::vec::Vec<&str> =
3673 _names.iter().copied().collect();
3674 self.save_partial(&_refs, pool).await
3675 }
3676 }
3677 }
3678 } else {
3679 let dispatch_unset = if fields.pk_is_auto {
3680 quote! {
3681 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3682 return self.insert_pool(pool).await.map(|()| 1u64);
3683 }
3684 }
3685 } else {
3686 quote!()
3687 };
3688 quote! {
3689 /// Save this row to its table against either backend.
3690 /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3691 /// `UPDATE` keyed on the PK.
3692 ///
3693 /// # Errors
3694 /// As [`Self::save`].
3695 pub async fn save_pool(
3696 &mut self,
3697 pool: &#root::sql::Pool,
3698 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3699 #dispatch_unset
3700 let _query = #root::core::UpdateQuery::new(
3701 <Self as #root::core::Model>::SCHEMA,
3702 ::std::vec![ #( #assignments ),* ],
3703 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3704 #pk_column_lit,
3705 #root::core::Op::Eq,
3706 ::core::convert::Into::<#root::core::SqlValue>::into(
3707 ::core::clone::Clone::clone(&self.#pk_ident)
3708 ),
3709 )),
3710 );
3711 let _affected = #root::sql::update_pool(pool, &_query).await?;
3712 ::core::result::Result::Ok(_affected)
3713 }
3714
3715 /// Save (UPDATE) only the listed Rust-side fields,
3716 /// leaving every other column untouched. Issue #66.
3717 ///
3718 /// `fields` are Rust-side struct field names; the macro
3719 /// resolves each to its SQL column. Unknown field
3720 /// names return [`#root::core::QueryError::UnknownField`]
3721 /// wrapped in `ExecError::Query`. An empty list is a
3722 /// no-op (returns `Ok(())` and logs a `tracing::warn!`).
3723 ///
3724 /// Use this when:
3725 /// * you only mutated a couple of fields on a wide row
3726 /// (avoid re-writing every column on every save), or
3727 /// * two writers diverged after their initial read and
3728 /// you want to preserve the other writer's changes to
3729 /// columns you didn't touch.
3730 ///
3731 /// Auto-PK models with an unset PK return
3732 /// [`#root::core::QueryError::UnknownField`] with
3733 /// field name `<pk>` — `save_partial` is an
3734 /// UPDATE-only path. Call [`Self::insert_pool`]
3735 /// (or [`Self::save_pool`] which dispatches based on
3736 /// PK state) for the INSERT case.
3737 ///
3738 /// # Errors
3739 /// As [`Self::save_pool`], plus `UnknownField` for
3740 /// unknown / empty / Auto-Unset cases.
3741 pub async fn save_partial(
3742 &mut self,
3743 fields: &[&str],
3744 pool: &#root::sql::Pool,
3745 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3746 if fields.is_empty() {
3747 #root::__tracing::warn!(
3748 target: "rustango::save_partial",
3749 model = <Self as #root::core::Model>::SCHEMA.name,
3750 "save_partial called with empty field list — no-op"
3751 );
3752 return ::core::result::Result::Ok(0);
3753 }
3754 let _schema = <Self as #root::core::Model>::SCHEMA;
3755 // Validate field names against the schema.
3756 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3757 ::std::collections::HashSet::with_capacity(fields.len());
3758 for f in fields {
3759 match _schema.field(f) {
3760 ::core::option::Option::Some(fs) => {
3761 _wanted_cols.insert(fs.column);
3762 }
3763 ::core::option::Option::None => {
3764 return ::core::result::Result::Err(
3765 #root::sql::ExecError::Query(
3766 #root::core::QueryError::UnknownField {
3767 model: _schema.name,
3768 field: (*f).to_owned(),
3769 }
3770 )
3771 );
3772 }
3773 }
3774 }
3775 // Build the full assignment vec, then keep only the
3776 // assignments whose column is in `_wanted_cols`.
3777 let _full: ::std::vec::Vec<#root::core::Assignment> =
3778 ::std::vec![ #( #assignments ),* ];
3779 let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3780 .into_iter()
3781 .filter(|a| _wanted_cols.contains(a.column))
3782 .collect();
3783 if _filtered.is_empty() {
3784 // All field names valid, but they all map to
3785 // non-assignable slots (PK column, computed/
3786 // virtual fields, relations without an
3787 // assignment). Same no-op as an empty list.
3788 #root::__tracing::warn!(
3789 target: "rustango::save_partial",
3790 model = _schema.name,
3791 "save_partial: every named field maps to a non-assignable column — no-op"
3792 );
3793 return ::core::result::Result::Ok(0);
3794 }
3795 let _query = #root::core::UpdateQuery::new(
3796 _schema,
3797 _filtered,
3798 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3799 #pk_column_lit,
3800 #root::core::Op::Eq,
3801 ::core::convert::Into::<#root::core::SqlValue>::into(
3802 ::core::clone::Clone::clone(&self.#pk_ident)
3803 ),
3804 )),
3805 );
3806 let _affected = #root::sql::update_pool(pool, &_query).await?;
3807 ::core::result::Result::Ok(_affected)
3808 }
3809
3810 /// Typed-column counterpart of [`Self::save_partial`] —
3811 /// issue #67. `fields` is a tuple of [`Column`]
3812 /// constants whose `Model` matches `Self`; typos and
3813 /// model mismatches surface at *compile time*
3814 /// (`Author::name` inside a `Post::save_partial_typed`
3815 /// call is a type error, no runtime check).
3816 ///
3817 /// ```ignore
3818 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3819 /// ```
3820 ///
3821 /// Lowers to [`Self::save_partial`] under the hood — the
3822 /// tuple is reduced to a `&[&str]` slice of Rust-side
3823 /// field names and forwarded.
3824 ///
3825 /// [`Column`]: #root::core::Column
3826 ///
3827 /// # Errors
3828 /// As [`Self::save_partial`].
3829 pub async fn save_partial_typed<
3830 L: #root::core::TypedFieldList<Self>,
3831 >(
3832 &mut self,
3833 fields: L,
3834 pool: &#root::sql::Pool,
3835 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3836 let _names = fields.rust_field_names();
3837 let _refs: ::std::vec::Vec<&str> =
3838 _names.iter().copied().collect();
3839 self.save_partial(&_refs, pool).await
3840 }
3841 }
3842 }
3843 } else {
3844 quote!()
3845 };
3846
3847 // Audited `insert_pool` (overrides the placeholder set higher up
3848 // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3849 // audited models get insert_pool routing through
3850 // audit::insert_one_with_audit (per-backend tx wraps INSERT
3851 // + auto-PK readback + audit emit). Snapshot-style audit (the
3852 // PendingEntry's `changes` carries post-write field values).
3853 let pool_insert_method = if audited_fields.is_some() {
3854 if let Some(_) = primary_key {
3855 let pushes = if fields.has_auto {
3856 fields.insert_pushes.clone()
3857 } else {
3858 // For non-Auto-PK models, the macro normally builds
3859 // {columns, values} from fields.insert_columns +
3860 // fields.insert_values rather than insert_pushes.
3861 // Map those into the pushes shape.
3862 fields
3863 .insert_columns
3864 .iter()
3865 .zip(&fields.insert_values)
3866 .map(|(col, val)| {
3867 quote! {
3868 _columns.push(#col);
3869 _values.push(#val);
3870 }
3871 })
3872 .collect()
3873 };
3874 // Rust-filled Auto PKs (`default_uuid_v7`) leave the list empty (#1934).
3875 let returning_cols: Vec<proc_macro2::TokenStream> = if !fields.returning_cols.is_empty()
3876 {
3877 fields.returning_cols.clone()
3878 } else {
3879 // Non-Auto-PK: still need RETURNING something for the
3880 // audit helper's contract (it errors on empty
3881 // returning). Return the PK column so the audit row
3882 // can carry the assigned PK back. Some non-Auto PKs
3883 // are server-side-default (e.g. UUIDv4 default), so
3884 // RETURNING is genuinely useful.
3885 primary_key
3886 .map(|(_, col)| {
3887 let lit = col.as_str();
3888 vec![quote!(#lit)]
3889 })
3890 .unwrap_or_default()
3891 };
3892 quote! {
3893 /// Insert this row against either backend with audit
3894 /// emission inside the same transaction. Bi-dialect
3895 /// counterpart of [`Self::insert`] for audited models.
3896 ///
3897 /// Snapshot-style audit (post-write field values).
3898 ///
3899 /// # Errors
3900 /// As [`Self::insert`].
3901 pub async fn insert_pool(
3902 &mut self,
3903 pool: &#root::sql::Pool,
3904 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3905 let mut _columns: ::std::vec::Vec<&'static str> =
3906 ::std::vec::Vec::new();
3907 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3908 ::std::vec::Vec::new();
3909 #( #pushes )*
3910 let _query = #root::core::InsertQuery::new(
3911 <Self as #root::core::Model>::SCHEMA,
3912 _columns,
3913 _values,
3914 )
3915 .returning(::std::vec![ #( #returning_cols ),* ]);
3916 #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self| {
3917 _m.__rustango_audit_entry(#root::audit::AuditOp::Create)
3918 })
3919 .await
3920 }
3921 }
3922 } else {
3923 quote!()
3924 }
3925 } else {
3926 // Keep the non-audited pool_insert_method we built earlier.
3927 pool_insert_method
3928 };
3929
3930 // Update audited save_pool: now that insert_pool is wired for
3931 // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3932 // insert_pool. Non-audited save_pool already does this.
3933 // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3934 // Replaces the snapshot-only emission with a per-backend transaction
3935 // body that:
3936 // 1. SELECTs the tracked columns by PK (typed Row::try_get per
3937 // column), capturing BEFORE values
3938 // 2. compiles the UPDATE via pool.dialect() and runs it on the tx
3939 // 3. builds AFTER pairs from &self
3940 // 4. diffs BEFORE/AFTER, emits one PendingEntry with
3941 // AuditOp::Update + diff_changes(...) on the same tx connection
3942 // 5. commits
3943 //
3944 // Per-backend arms inline the SQL string + placeholder shape, then
3945 // share the `audit_before_pair_tokens` decoder block (Row::try_get
3946 // is polymorphic over Row type — the same tokens work against
3947 // PgRow and MySqlRow as long as the field's Rust type implements
3948 // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3949 // primitives + chrono/uuid/serde_json::Value all do).
3950 let pool_save_method = if let Some(tracked) = audited_fields {
3951 if let Some((pk_ident, pk_col)) = primary_key {
3952 let pk_column_lit = pk_col.as_str();
3953 // Two iterators — quote!'s `#(#var)*` consumes the
3954 // iterator, and we need to splice the same after-pairs
3955 // sequence into both per-backend arms.
3956 let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
3957 let pk_str = audit_pk_to_string.clone();
3958 // Per-tracked-column BEFORE-pair token list. Each entry
3959 // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3960 // The Row alias resolves to PgRow / MySqlRow per call site,
3961 // so the same template generates both the PG and MySQL bodies.
3962 let mk_before_pairs =
3963 |getter: &dyn Fn(&ColumnEntry) -> TokenStream2| -> Vec<TokenStream2> {
3964 tracked
3965 .iter()
3966 .map(|c| {
3967 let getter = getter(c);
3968 let column_lit = c.column.as_str();
3969 let value_ty = &c.value_ty;
3970 quote! {
3971 (
3972 #column_lit,
3973 match #getter::<#value_ty>(
3974 _audit_before_row, #column_lit,
3975 ) {
3976 ::core::result::Result::Ok(v) => {
3977 #root::__serde_json::to_value(&v)
3978 .unwrap_or(#root::__serde_json::Value::Null)
3979 }
3980 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3981 },
3982 )
3983 }
3984 })
3985 .collect()
3986 };
3987 let before_pairs_pg = mk_before_pairs(&|_| quote!(#root::sql::try_get_returning));
3988 let before_pairs_my = mk_before_pairs(&|c| {
3989 if c.uuid_column {
3990 quote!(#root::sql::try_get_flat_my)
3991 } else {
3992 quote!(#root::sql::try_get_returning_my)
3993 }
3994 });
3995 let before_pairs_sqlite =
3996 mk_before_pairs(&|_| quote!(#root::sql::try_get_returning_sqlite));
3997 let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
3998 let pk_value_for_bind = if fields.pk_is_auto {
3999 quote!(self.#pk_ident.get().copied().unwrap_or_default())
4000 } else {
4001 quote!(::core::clone::Clone::clone(&self.#pk_ident))
4002 };
4003 let assignments = &fields.update_assignments;
4004 let unset_dispatch = if fields.has_auto {
4005 quote! {
4006 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
4007 return self.insert_pool(pool).await.map(|()| 1u64);
4008 }
4009 }
4010 } else {
4011 quote!()
4012 };
4013 quote! {
4014 /// Save this row against either backend with audit
4015 /// emission (diff-style: BEFORE+AFTER) inside the
4016 /// same transaction. Auto::Unset PK routes to
4017 /// insert_pool. Bi-dialect counterpart of
4018 /// [`Self::save`] for audited models.
4019 ///
4020 /// The audit row's `changes` JSON contains one
4021 /// `{ "field": { "before": …, "after": … } }` entry
4022 /// per tracked column whose value actually changed
4023 /// — same shape as the existing &PgPool save() emits.
4024 ///
4025 /// # Errors
4026 /// As [`Self::save`].
4027 pub async fn save_pool(
4028 &mut self,
4029 pool: &#root::sql::Pool,
4030 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4031 #unset_dispatch
4032 let _query = #root::core::UpdateQuery::new(
4033 <Self as #root::core::Model>::SCHEMA,
4034 ::std::vec![ #( #assignments ),* ],
4035 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
4036 #pk_column_lit,
4037 #root::core::Op::Eq,
4038 ::core::convert::Into::<#root::core::SqlValue>::into(
4039 ::core::clone::Clone::clone(&self.#pk_ident)
4040 ),
4041 )),
4042 );
4043 let _after_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
4044 ::std::vec![ #( #after_pairs_pg ),* ];
4045 let _before_query = #root::audit::before_image_query(
4046 <Self as #root::core::Model>::SCHEMA,
4047 #pk_column_lit,
4048 ::core::convert::Into::<#root::core::SqlValue>::into(
4049 #pk_value_for_bind,
4050 ),
4051 &[ #( #tracked_cols ),* ],
4052 );
4053 #root::audit::save_one_with_diff(
4054 pool,
4055 &_query,
4056 &_before_query,
4057 <Self as #root::core::Model>::SCHEMA.table,
4058 #pk_str,
4059 _after_pairs,
4060 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
4061 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
4062 |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
4063 ).await
4064 }
4065 }
4066 } else {
4067 quote!()
4068 }
4069 } else {
4070 pool_save_method
4071 };
4072
4073 // `delete_pool(&Pool)` — emitted for every model with a PK. Two
4074 // body shapes:
4075 // - non-audited: simple dispatch through `sql::delete_pool`
4076 // - audited: routes through `audit::delete_one_with_audit`,
4077 // which opens a per-backend transaction wrapping DELETE +
4078 // audit emit so the data write and audit row commit atomically.
4079 let pool_delete_method = {
4080 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
4081 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
4082 if let Some(pk_ident) = pk_ident_for_pool {
4083 if audited_fields.is_some() {
4084 quote! {
4085 /// Delete this row against either backend with audit
4086 /// emission inside the same transaction. Bi-dialect
4087 /// counterpart of [`Self::delete`] for audited models.
4088 ///
4089 /// # Errors
4090 /// As [`Self::delete`].
4091 pub async fn delete_pool(
4092 &self,
4093 pool: &#root::sql::Pool,
4094 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4095 let _query = #root::core::DeleteQuery::by_pk(
4096 <Self as #root::core::Model>::SCHEMA,
4097 #pk_column_lit,
4098 ::core::convert::Into::<#root::core::SqlValue>::into(
4099 ::core::clone::Clone::clone(&self.#pk_ident)
4100 ),
4101 );
4102 let _audit_entry =
4103 self.__rustango_audit_entry(#root::audit::AuditOp::Delete);
4104 #root::audit::delete_one_with_audit(
4105 pool, &_query, &_audit_entry,
4106 ).await
4107 }
4108 }
4109 } else {
4110 quote! {
4111 /// Delete the row identified by this instance's primary key
4112 /// against either backend. Equivalent to [`Self::delete`] but
4113 /// takes [`#root::sql::Pool`] and dispatches per backend.
4114 ///
4115 /// # Errors
4116 /// As [`Self::delete`].
4117 pub async fn delete_pool(
4118 &self,
4119 pool: &#root::sql::Pool,
4120 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4121 let _query = #root::core::DeleteQuery::by_pk(
4122 <Self as #root::core::Model>::SCHEMA,
4123 #pk_column_lit,
4124 ::core::convert::Into::<#root::core::SqlValue>::into(
4125 ::core::clone::Clone::clone(&self.#pk_ident)
4126 ),
4127 );
4128 #root::sql::delete_pool(pool, &_query).await
4129 }
4130 }
4131 }
4132 } else {
4133 quote!()
4134 }
4135 };
4136
4137 // Bulk writers on audited models write one audit row per affected
4138 // row, in the write's transaction (#1747).
4139 let (bulk_delete_run, bulk_update_run, truncate_run) = if audited_fields.is_some() {
4140 (
4141 quote!(#root::audit::delete_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4142 _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4143 })
4144 .await),
4145 quote!(#root::audit::update_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4146 _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
4147 })
4148 .await),
4149 quote!(#root::audit::truncate_with_audit(pool, _table, &_sql).await),
4150 )
4151 } else {
4152 (
4153 quote!(#root::sql::delete_pool(pool, &_query).await),
4154 quote!(#root::sql::update_pool(pool, &_query).await),
4155 quote!(#root::sql::raw_execute_pool(pool, &_sql, ::std::vec::Vec::new()).await),
4156 )
4157 };
4158
4159 // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
4160 // matching this instance's PK and overwrite the in-memory state
4161 // with the freshly-fetched columns.
4162 // Issue #825. Only emitted when the model declares a PK; non-PK
4163 // models can't address a specific row.
4164 //
4165 // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
4166 // every field from `self`; resets the PK to `Auto::Unset` when
4167 // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
4168 // a fresh autoincrement value. Non-Auto PKs preserve the source
4169 // PK — the caller must overwrite before insert. Pure-Rust, no
4170 // I/O, no dialect surface.
4171 let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
4172 let other_field_clones: Vec<TokenStream2> = fields
4173 .column_entries
4174 .iter()
4175 .filter(|c| &c.ident != pk_ident)
4176 .map(|c| {
4177 let ident = &c.ident;
4178 quote! {
4179 #ident: ::core::clone::Clone::clone(&self.#ident)
4180 }
4181 })
4182 .collect();
4183 let pk_clone_token = if fields.pk_is_auto {
4184 quote! { #pk_ident: #root::sql::Auto::Unset }
4185 } else {
4186 quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
4187 };
4188 let replicate_doc = if fields.pk_is_auto {
4189 quote! {
4190 /// Eloquent-style `replicate()` — return a clone of this
4191 /// row with the primary key reset to [`Auto::Unset`] so
4192 /// the copy is ready for `insert_pool` / `save_pool` to
4193 /// allocate a fresh autoincrement value. Every other
4194 /// field is `Clone`d verbatim. Issue #825.
4195 ///
4196 /// `auto_now_add` / `auto_now` timestamp fields are
4197 /// **not** reset (Eloquent's `replicate` doesn't reset
4198 /// them either) — pass them through the normal insert
4199 /// path if you want fresh values, or assign them
4200 /// explicitly after the call.
4201 }
4202 } else {
4203 quote! {
4204 /// Eloquent-style `replicate()` — clone this row
4205 /// verbatim. Because the primary key is **not** an
4206 /// `Auto<T>`, the clone keeps the source PK; the
4207 /// caller must overwrite `copy.<pk>` before inserting
4208 /// to avoid a unique-key violation. Issue #825.
4209 }
4210 };
4211 // 2026-06-07 — field-name / shortcut collision guard.
4212 //
4213 // The macro emits both `pub const <field>: <field>_col = ...`
4214 // (per-field typed-column const, used by the typed-builder
4215 // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
4216 // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
4217 // model has a field named e.g. `count`, both items would land
4218 // in the same inherent impl with the same name, and the
4219 // compiler rejects the derive with "duplicate definitions".
4220 //
4221 // Drop the conflicting shortcut for that model. Callers can
4222 // still reach the same behavior via
4223 // `QuerySet::<Model>::default().count(&pool)`.
4224 let column_names: ::std::collections::HashSet<String> = fields
4225 .column_entries
4226 .iter()
4227 .map(|c| ident_name(&c.ident))
4228 .collect();
4229 let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
4230 if column_names.contains(name) {
4231 quote! {}
4232 } else {
4233 tokens
4234 }
4235 };
4236 let count_method = emit_if_no_field_collision(
4237 "count",
4238 quote! {
4239 /// Count rows of this model — `SELECT COUNT(*) FROM
4240 /// <table>`. Eloquent `Model::count()` parity.
4241 ///
4242 /// Skipped on models that already declare a field named
4243 /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
4244 /// in that case.
4245 ///
4246 /// # Errors
4247 /// As [`CounterPool::count`].
4248 ///
4249 /// [`CounterPool::count`]: rustango::sql::CounterPool::count
4250 pub async fn count(
4251 pool: &#root::sql::Pool,
4252 ) -> ::core::result::Result<i64, #root::sql::ExecError> {
4253 use #root::sql::CounterPool as _;
4254 #root::query::QuerySet::<Self>::default()
4255 .count(pool)
4256 .await
4257 }
4258 },
4259 );
4260 let value_method = emit_if_no_field_collision(
4261 "value",
4262 quote! {
4263 /// Pluck a single scalar from the first row.
4264 /// Eloquent `Model::query()->value($col)` parity.
4265 ///
4266 /// Skipped on models that already declare a field named
4267 /// `value`. Drop into
4268 /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4269 ///
4270 /// # Errors
4271 /// As `ValuesFlatQuerySet::first`.
4272 pub async fn value<U>(
4273 col: &str,
4274 pool: &#root::sql::Pool,
4275 ) -> ::core::result::Result<
4276 ::core::option::Option<U>,
4277 #root::sql::ExecError,
4278 >
4279 where
4280 U: #root::sql::FlatScalar,
4281 {
4282 let _col_static: &'static str = Self::__resolve_col(col)?;
4283 #root::query::QuerySet::<Self>::default()
4284 .values_list_flat(_col_static)
4285 .first::<U>(pool)
4286 .await
4287 }
4288 },
4289 );
4290 let sum_method = emit_if_no_field_collision(
4291 "sum",
4292 quote! {
4293 /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4294 /// Skipped on models that already declare a field named
4295 /// `sum`.
4296 ///
4297 /// # Errors
4298 /// As [`#root::sql::fetch_aggregate_pool`].
4299 pub async fn sum<U>(
4300 col: &str,
4301 pool: &#root::sql::Pool,
4302 ) -> ::core::result::Result<
4303 ::core::option::Option<U>,
4304 #root::sql::ExecError,
4305 >
4306 where
4307 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4308 + #root::sql::MaybeMyFromRow
4309 + #root::sql::MaybeSqliteFromRow
4310 + ::core::marker::Send
4311 + ::core::marker::Unpin,
4312 {
4313 #root::query::QuerySet::<Self>::default()
4314 .sum::<U>(col, pool)
4315 .await
4316 }
4317 },
4318 );
4319 let avg_method = emit_if_no_field_collision(
4320 "avg",
4321 quote! {
4322 /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4323 /// models that already declare a field named `avg`.
4324 ///
4325 /// # Errors
4326 /// As [`#root::sql::fetch_aggregate_pool`].
4327 pub async fn avg<U>(
4328 col: &str,
4329 pool: &#root::sql::Pool,
4330 ) -> ::core::result::Result<
4331 ::core::option::Option<U>,
4332 #root::sql::ExecError,
4333 >
4334 where
4335 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4336 + #root::sql::MaybeMyFromRow
4337 + #root::sql::MaybeSqliteFromRow
4338 + ::core::marker::Send
4339 + ::core::marker::Unpin,
4340 {
4341 #root::query::QuerySet::<Self>::default()
4342 .avg::<U>(col, pool)
4343 .await
4344 }
4345 },
4346 );
4347 let min_method = emit_if_no_field_collision(
4348 "min",
4349 quote! {
4350 /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4351 /// models that already declare a field named `min`.
4352 ///
4353 /// # Errors
4354 /// As [`#root::sql::fetch_aggregate_pool`].
4355 pub async fn min<U>(
4356 col: &str,
4357 pool: &#root::sql::Pool,
4358 ) -> ::core::result::Result<
4359 ::core::option::Option<U>,
4360 #root::sql::ExecError,
4361 >
4362 where
4363 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4364 + #root::sql::MaybeMyFromRow
4365 + #root::sql::MaybeSqliteFromRow
4366 + ::core::marker::Send
4367 + ::core::marker::Unpin,
4368 {
4369 #root::query::QuerySet::<Self>::default()
4370 .min::<U>(col, pool)
4371 .await
4372 }
4373 },
4374 );
4375 let max_method = emit_if_no_field_collision(
4376 "max",
4377 quote! {
4378 /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4379 /// models that already declare a field named `max`.
4380 ///
4381 /// # Errors
4382 /// As [`#root::sql::fetch_aggregate_pool`].
4383 pub async fn max<U>(
4384 col: &str,
4385 pool: &#root::sql::Pool,
4386 ) -> ::core::result::Result<
4387 ::core::option::Option<U>,
4388 #root::sql::ExecError,
4389 >
4390 where
4391 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4392 + #root::sql::MaybeMyFromRow
4393 + #root::sql::MaybeSqliteFromRow
4394 + ::core::marker::Send
4395 + ::core::marker::Unpin,
4396 {
4397 #root::query::QuerySet::<Self>::default()
4398 .max::<U>(col, pool)
4399 .await
4400 }
4401 },
4402 );
4403 let first_method = emit_if_no_field_collision(
4404 "first",
4405 quote! {
4406 /// First row of this model. Eloquent `Model::first()`.
4407 /// Skipped on models that already declare a field named
4408 /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4409 ///
4410 /// # Errors
4411 /// As `QuerySet::first`.
4412 pub async fn first(
4413 pool: &#root::sql::Pool,
4414 ) -> ::core::result::Result<
4415 ::core::option::Option<Self>,
4416 #root::sql::ExecError,
4417 > {
4418 #root::query::QuerySet::<Self>::default()
4419 .first(pool)
4420 .await
4421 }
4422 },
4423 );
4424 let last_method = emit_if_no_field_collision(
4425 "last",
4426 quote! {
4427 /// Last row of this model by primary-key DESC.
4428 /// Eloquent `Model::query()->latest('id')->first()`
4429 /// parity — fetches the highest-PK row without
4430 /// requiring the caller to spell the PK column.
4431 /// Returns `None` on an empty table.
4432 ///
4433 /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4434 /// Skipped on models that already declare a field
4435 /// named `last`.
4436 ///
4437 /// # Errors
4438 /// As `QuerySet::last`.
4439 pub async fn last(
4440 pool: &#root::sql::Pool,
4441 ) -> ::core::result::Result<
4442 ::core::option::Option<Self>,
4443 #root::sql::ExecError,
4444 > {
4445 #root::query::QuerySet::<Self>::default()
4446 .last(pool)
4447 .await
4448 }
4449 },
4450 );
4451 quote! {
4452 /// Re-SELECT this row by its primary key and overwrite
4453 /// every in-memory field with the freshly-fetched value.
4454 /// Issue #825.
4455 ///
4456 /// Use this when the row may have been modified by another
4457 /// process / connection / job since you read it — e.g. after
4458 /// a queued task callback, or to re-sync stale UI state
4459 /// before re-saving.
4460 ///
4461 /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4462 /// when the primary key no longer matches any row (e.g.
4463 /// the row was deleted concurrently).
4464 ///
4465 /// # Errors
4466 /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4467 /// the PK no longer exists.
4468 ///
4469 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4470 pub async fn refresh_from_db(
4471 &mut self,
4472 pool: &#root::sql::Pool,
4473 ) -> ::core::result::Result<(), #root::sql::ExecError> {
4474 use #root::sql::FetcherPool as _;
4475 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4476 ::core::clone::Clone::clone(&self.#pk_ident),
4477 );
4478 let mut _rows: ::std::vec::Vec<Self> =
4479 #root::query::QuerySet::<Self>::default()
4480 .filter(::core::stringify!(#pk_ident), _pk_val)
4481 .limit(1)
4482 .fetch(pool)
4483 .await?;
4484 match _rows.into_iter().next() {
4485 ::core::option::Option::Some(_fresh) => {
4486 *self = _fresh;
4487 ::core::result::Result::Ok(())
4488 }
4489 ::core::option::Option::None => ::core::result::Result::Err(
4490 #root::sql::ExecError::Driver(
4491 #root::sql::sqlx::Error::RowNotFound,
4492 ),
4493 ),
4494 }
4495 }
4496
4497 /// Atomically increment the integer column `col` by
4498 /// `by` for this row. Equivalent to
4499 /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4500 ///
4501 /// **Doesn't mutate `self`** — the in-memory copy is now
4502 /// stale; call [`Self::refresh_from_db`] /
4503 /// [`Self::fresh`] to re-sync. Returns the rows-
4504 /// affected count (0 when the PK doesn't match any row,
4505 /// 1 on success).
4506 ///
4507 /// `col` is the Rust field name as a string; unknown
4508 /// fields surface as `UnknownField` at runtime. Negative
4509 /// `by` values atomically decrement (see also
4510 /// [`Self::decrement`]).
4511 ///
4512 /// # Errors
4513 /// As [`UpdaterPool::execute_pool`].
4514 ///
4515 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4516 pub async fn increment(
4517 &self,
4518 col: &str,
4519 by: i64,
4520 pool: &#root::sql::Pool,
4521 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4522 Self::__increment_one(self, col, by, pool).await
4523 }
4524
4525 /// Sibling of [`Self::increment`] — atomically
4526 /// decrement this row's `col` by `by`. Eloquent
4527 /// `$model->decrement($col, $by)` parity. Equivalent to
4528 /// `self.increment(col, -by, &pool)`; the separate name
4529 /// keeps call sites readable.
4530 ///
4531 /// # Errors
4532 /// As [`Self::increment`].
4533 pub async fn decrement(
4534 &self,
4535 col: &str,
4536 by: i64,
4537 pool: &#root::sql::Pool,
4538 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4539 Self::__increment_one(self, col, -by, pool).await
4540 }
4541
4542 /// Bulk-increment: add `by` to `col` on every row of the
4543 /// table. Eloquent `Model::query()->increment($col, $by)`
4544 /// parity. Use for counters, score adjustments, view
4545 /// rollups.
4546 ///
4547 /// # Errors
4548 /// As [`UpdaterPool::execute_pool`].
4549 ///
4550 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4551 pub async fn increment_each(
4552 col: &str,
4553 by: i64,
4554 pool: &#root::sql::Pool,
4555 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4556 Self::__increment_all(col, by, pool).await
4557 }
4558
4559 /// Sibling of [`Self::increment_each`] — bulk-decrement.
4560 ///
4561 /// # Errors
4562 /// As [`Self::increment_each`].
4563 pub async fn decrement_each(
4564 col: &str,
4565 by: i64,
4566 pool: &#root::sql::Pool,
4567 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4568 Self::__increment_all(col, -by, pool).await
4569 }
4570
4571 /// Internal: forward to
4572 /// [`#root::sql::model_shortcuts::increment_one_pool`].
4573 /// One-line wrapper kept as a per-Model method so the
4574 /// macro's emitted `increment` / `decrement` instance
4575 /// calls don't have to thread `Self` through manually.
4576 #[doc(hidden)]
4577 pub async fn __increment_one(
4578 this: &Self,
4579 col: &str,
4580 by: i64,
4581 pool: &#root::sql::Pool,
4582 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4583 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4584 ::core::clone::Clone::clone(&this.#pk_ident),
4585 );
4586 #root::sql::model_shortcuts::increment_one_pool::<Self>(
4587 ::core::stringify!(#pk_ident),
4588 _pk_val,
4589 col,
4590 by,
4591 pool,
4592 )
4593 .await
4594 }
4595
4596 /// Internal: run
4597 /// [`#root::sql::model_shortcuts::increment_all_query`].
4598 #[doc(hidden)]
4599 pub async fn __increment_all(
4600 col: &str,
4601 by: i64,
4602 pool: &#root::sql::Pool,
4603 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4604 let _query =
4605 #root::sql::model_shortcuts::increment_all_query::<Self>(col, by)?;
4606 #bulk_update_run
4607 }
4608
4609 /// Internal: forward to
4610 /// [`#root::sql::model_shortcuts::resolve_col`].
4611 #[doc(hidden)]
4612 pub fn __resolve_col(
4613 col: &str,
4614 ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4615 #root::sql::model_shortcuts::resolve_col::<Self>(col)
4616 }
4617
4618 /// Internal: forward to
4619 /// [`#root::sql::model_shortcuts::add_signed_expr`].
4620 #[doc(hidden)]
4621 #[must_use]
4622 pub fn __add_signed_expr(
4623 col_static: &'static str,
4624 signed_by: i64,
4625 ) -> #root::core::Expr {
4626 #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4627 }
4628
4629 /// Re-SELECT this row by its primary key and return a
4630 /// **new** instance with the freshly-fetched fields.
4631 /// Eloquent `Model::fresh()` parity — non-mutating
4632 /// counterpart of [`Self::refresh_from_db`].
4633 ///
4634 /// Returns `Ok(None)` when the row was deleted
4635 /// concurrently — vs [`Self::refresh_from_db`]
4636 /// which surfaces that as `RowNotFound` because
4637 /// in-place mutation has nothing to write to.
4638 ///
4639 /// Useful when you want to compare the in-memory
4640 /// instance against the persisted state (audit-style
4641 /// diffs, conflict detection) without mutating the
4642 /// reference you already hold.
4643 ///
4644 /// # Errors
4645 /// As [`FetcherPool::fetch`].
4646 ///
4647 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4648 pub async fn fresh(
4649 &self,
4650 pool: &#root::sql::Pool,
4651 ) -> ::core::result::Result<
4652 ::core::option::Option<Self>,
4653 #root::sql::ExecError,
4654 > {
4655 use #root::sql::FetcherPool as _;
4656 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4657 ::core::clone::Clone::clone(&self.#pk_ident),
4658 );
4659 let _rows: ::std::vec::Vec<Self> =
4660 #root::query::QuerySet::<Self>::default()
4661 .filter(::core::stringify!(#pk_ident), _pk_val)
4662 .limit(1)
4663 .fetch(pool)
4664 .await?;
4665 ::core::result::Result::Ok(_rows.into_iter().next())
4666 }
4667
4668 #replicate_doc
4669 #[must_use]
4670 pub fn replicate(&self) -> Self {
4671 Self {
4672 #pk_clone_token,
4673 #( #other_field_clones, )*
4674 }
4675 }
4676
4677 #first_method
4678
4679 #last_method
4680
4681 /// Throwing counterpart of [`Self::first`] —
4682 /// errors with `RowNotFound` when the table is empty.
4683 /// Eloquent `Model::firstOrFail()` parity.
4684 ///
4685 /// # Errors
4686 /// As [`Self::first`]; additionally
4687 /// [`sqlx::Error::RowNotFound`] on empty tables.
4688 ///
4689 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4690 pub async fn first_or_fail(
4691 pool: &#root::sql::Pool,
4692 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4693 // Route through the queryset rather than `Self::first`,
4694 // which is suppressed on models with a field named
4695 // `first` (field/shortcut collision guard).
4696 match #root::query::QuerySet::<Self>::default().first(pool).await? {
4697 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4698 ::core::option::Option::None => ::core::result::Result::Err(
4699 #root::sql::ExecError::Driver(
4700 #root::sql::sqlx::Error::RowNotFound,
4701 ),
4702 ),
4703 }
4704 }
4705
4706 /// Single-column projection — `SELECT <col> FROM
4707 /// <table>`. Returns `Vec<U>` where each element is the
4708 /// decoded value of the column. Eloquent
4709 /// `Model::pluck($column)` parity.
4710 ///
4711 /// Thin wrapper over `QuerySet::<Self>::default()
4712 /// .values_list_flat(col).fetch::<U>(pool)`. `U` is a
4713 /// `FlatScalar`; use `Option<_>` for a nullable column.
4714 ///
4715 /// # Errors
4716 /// As `ValuesFlatQuerySet::fetch`.
4717 pub async fn pluck<U>(
4718 col: &'static str,
4719 pool: &#root::sql::Pool,
4720 ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4721 where
4722 U: #root::sql::FlatScalar,
4723 {
4724 #root::query::QuerySet::<Self>::default()
4725 .values_list_flat(col)
4726 .fetch::<U>(pool)
4727 .await
4728 }
4729
4730 /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4731 /// stream every row of this model in batches of `n`,
4732 /// invoking the callback once per batch. Stable PK-ASC
4733 /// ordering so the LIMIT/OFFSET pagination is
4734 /// deterministic across drivers.
4735 ///
4736 /// The callback is async — it can do further DB work
4737 /// (writes, related-row lookups, queue dispatch) per
4738 /// batch. Return `Err(...)` from the callback to abort
4739 /// the iteration early; the error bubbles up.
4740 ///
4741 /// **When to use**: bulk processing flows that can't
4742 /// fit the whole table in memory — sending newsletters,
4743 /// running data migrations, computing summary
4744 /// statistics. For small / known-bounded tables, plain
4745 /// `Self::all(&pool)` is simpler.
4746 ///
4747 /// **Caveat**: ascending OFFSET pagination is O(N²) on
4748 /// large tables. For multi-million-row scans prefer
4749 /// keyset-by-PK (the standard "WHERE id > last_seen"
4750 /// shape) over `chunk(...)`.
4751 ///
4752 /// Skipped on models without a primary key — chunking
4753 /// needs a stable order to avoid skipping / repeating
4754 /// rows across batches.
4755 pub async fn chunk<F, Fut>(
4756 n: i64,
4757 pool: &#root::sql::Pool,
4758 mut cb: F,
4759 ) -> ::core::result::Result<(), #root::sql::ExecError>
4760 where
4761 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4762 Fut: ::core::future::Future<
4763 Output = ::core::result::Result<(), #root::sql::ExecError>,
4764 >,
4765 {
4766 use #root::sql::FetcherPool as _;
4767 let pk_col = match Self::primary_key_column() {
4768 ::core::option::Option::Some(c) => c,
4769 ::core::option::Option::None => {
4770 return ::core::result::Result::Ok(());
4771 }
4772 };
4773 let mut offset: i64 = 0;
4774 loop {
4775 let rows: ::std::vec::Vec<Self> =
4776 #root::query::QuerySet::<Self>::default()
4777 .order_by(&[(pk_col, false)])
4778 .limit(n)
4779 .offset(offset)
4780 .fetch(pool)
4781 .await?;
4782 if rows.is_empty() {
4783 return ::core::result::Result::Ok(());
4784 }
4785 let len = rows.len() as i64;
4786 cb(rows).await?;
4787 if len < n {
4788 return ::core::result::Result::Ok(());
4789 }
4790 offset += n;
4791 }
4792 }
4793
4794 /// Eloquent `Model::chunkById($n, fn (...))` — same
4795 /// per-batch callback shape as [`Self::chunk`], but uses
4796 /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4797 /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4798 /// — the right choice for multi-million-row sweeps.
4799 ///
4800 /// Requires the primary key to be a signed integer type
4801 /// (`i64` / `i32`); the keyset comparison rides on
4802 /// `__rustango_pk_value()` lowering through
4803 /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4804 /// non-integer PKs (UUID, String) — those should use
4805 /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4806 /// keyset loop.
4807 ///
4808 /// Callback errors abort iteration; the error bubbles up
4809 /// unchanged. Empty table → callback invoked zero times.
4810 pub async fn chunk_by_id<F, Fut>(
4811 n: i64,
4812 pool: &#root::sql::Pool,
4813 mut cb: F,
4814 ) -> ::core::result::Result<(), #root::sql::ExecError>
4815 where
4816 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4817 Fut: ::core::future::Future<
4818 Output = ::core::result::Result<(), #root::sql::ExecError>,
4819 >,
4820 {
4821 use #root::sql::FetcherPool as _;
4822 let pk_col = match Self::primary_key_column() {
4823 ::core::option::Option::Some(c) => c,
4824 ::core::option::Option::None => {
4825 return ::core::result::Result::Ok(());
4826 }
4827 };
4828 // Track the largest PK seen so the next batch picks
4829 // up from there. `i64::MIN` as the sentinel — the
4830 // very first iteration's `> MIN` matches every row,
4831 // so the loop entry is uniform with subsequent
4832 // iterations.
4833 let mut last_seen: i64 = i64::MIN;
4834 loop {
4835 let key = ::std::format!("{}__gt", pk_col);
4836 let rows: ::std::vec::Vec<Self> =
4837 #root::query::QuerySet::<Self>::default()
4838 .filter(key.as_str(), last_seen)
4839 .order_by(&[(pk_col, false)])
4840 .limit(n)
4841 .fetch(pool)
4842 .await?;
4843 if rows.is_empty() {
4844 return ::core::result::Result::Ok(());
4845 }
4846 let len = rows.len() as i64;
4847 // Capture the last row's PK BEFORE moving rows
4848 // into the callback.
4849 let max_pk = match rows
4850 .last()
4851 .map(|r| r.__rustango_pk_value())
4852 {
4853 ::core::option::Option::Some(
4854 #root::core::SqlValue::I64(v),
4855 ) => v,
4856 ::core::option::Option::Some(
4857 #root::core::SqlValue::I32(v),
4858 ) => i64::from(v),
4859 _ => return ::core::result::Result::Ok(()),
4860 };
4861 cb(rows).await?;
4862 if len < n {
4863 return ::core::result::Result::Ok(());
4864 }
4865 last_seen = max_pk;
4866 }
4867 }
4868
4869 /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4870 /// per-row callback companion to [`Self::chunk`].
4871 /// Streams every row in keyset-paginated batches of
4872 /// `batch` size, calling `cb` once per row.
4873 ///
4874 /// Inherits the keyset-paginated scan of
4875 /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4876 /// non-integer PKs are silently a no-op).
4877 ///
4878 /// ```ignore
4879 /// Post::each(500, &pool, |p| async move {
4880 /// reindex(p).await?;
4881 /// Ok(())
4882 /// }).await?;
4883 /// ```
4884 ///
4885 /// Return `Err(...)` from the callback to abort
4886 /// iteration; the error bubbles up unchanged.
4887 pub async fn each<F, Fut>(
4888 batch: i64,
4889 pool: &#root::sql::Pool,
4890 mut cb: F,
4891 ) -> ::core::result::Result<(), #root::sql::ExecError>
4892 where
4893 F: ::core::ops::FnMut(Self) -> Fut,
4894 Fut: ::core::future::Future<
4895 Output = ::core::result::Result<(), #root::sql::ExecError>,
4896 >,
4897 {
4898 use #root::sql::FetcherPool as _;
4899 let pk_col = match Self::primary_key_column() {
4900 ::core::option::Option::Some(c) => c,
4901 ::core::option::Option::None => {
4902 return ::core::result::Result::Ok(());
4903 }
4904 };
4905 let mut last_seen: i64 = i64::MIN;
4906 loop {
4907 let key = ::std::format!("{}__gt", pk_col);
4908 let rows: ::std::vec::Vec<Self> =
4909 #root::query::QuerySet::<Self>::default()
4910 .filter(key.as_str(), last_seen)
4911 .order_by(&[(pk_col, false)])
4912 .limit(batch)
4913 .fetch(pool)
4914 .await?;
4915 if rows.is_empty() {
4916 return ::core::result::Result::Ok(());
4917 }
4918 let len = rows.len() as i64;
4919 let max_pk = match rows
4920 .last()
4921 .map(|r| r.__rustango_pk_value())
4922 {
4923 ::core::option::Option::Some(
4924 #root::core::SqlValue::I64(v),
4925 ) => v,
4926 ::core::option::Option::Some(
4927 #root::core::SqlValue::I32(v),
4928 ) => i64::from(v),
4929 _ => return ::core::result::Result::Ok(()),
4930 };
4931 for row in rows {
4932 cb(row).await?;
4933 }
4934 if len < batch {
4935 return ::core::result::Result::Ok(());
4936 }
4937 last_seen = max_pk;
4938 }
4939 }
4940
4941 /// Delete every row of this model — `TRUNCATE TABLE
4942 /// <table> RESTART IDENTITY CASCADE` on Postgres,
4943 /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4944 /// support `TRUNCATE` inside foreign-key constraints
4945 /// or — for SQLite — at all). Eloquent `Model::truncate()`
4946 /// parity.
4947 ///
4948 /// **Use only in tests / fixture-reset flows.** Production
4949 /// writes through this would silently bypass the
4950 /// `pre_delete` / `post_delete` signals (no per-row hooks
4951 /// fire on a TRUNCATE / bulk DELETE FROM). An audited model
4952 /// gets one bulk audit entry, not one per row.
4953 ///
4954 /// # Errors
4955 /// As [`raw_execute_pool`].
4956 ///
4957 /// [`raw_execute_pool`]: rustango::sql::raw_execute_pool
4958 pub async fn truncate(
4959 pool: &#root::sql::Pool,
4960 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4961 let _table = <Self as #root::core::Model>::SCHEMA.table;
4962 let _dialect = pool.dialect();
4963 let _quoted = _dialect.quote_ident(_table);
4964 let _sql = if _dialect.name() == "postgres" {
4965 ::std::format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", _quoted)
4966 } else {
4967 ::std::format!("DELETE FROM {}", _quoted)
4968 };
4969 #truncate_run
4970 }
4971
4972 /// Bulk-delete every row whose primary key is in
4973 /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4974 /// Returns the affected row count.
4975 ///
4976 /// Eloquent `Model::destroy([1, 2, 3])` parity.
4977 /// Empty `pks` is a no-op (returns 0).
4978 ///
4979 /// Accepts any iterable whose elements are
4980 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4981 /// `[i64; N]`, etc.
4982 ///
4983 /// # Errors
4984 /// As `delete_pool`.
4985 pub async fn destroy<V>(
4986 pks: impl ::core::iter::IntoIterator<Item = V>,
4987 pool: &#root::sql::Pool,
4988 ) -> ::core::result::Result<u64, #root::sql::ExecError>
4989 where
4990 V: ::core::convert::Into<#root::core::SqlValue>,
4991 {
4992 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4993 pks.into_iter().map(::core::convert::Into::into).collect();
4994 if _values.is_empty() {
4995 return ::core::result::Result::Ok(0);
4996 }
4997 let _pk = <Self as #root::core::Model>::SCHEMA
4998 .primary_key()
4999 .ok_or_else(|| {
5000 #root::sql::ExecError::Sql(#root::sql::SqlError::MissingPrimaryKey)
5001 })?;
5002 let _query = #root::query::QuerySet::<Self>::default()
5003 .filter_op(
5004 _pk.name,
5005 #root::core::Op::In,
5006 #root::core::SqlValue::List(_values),
5007 )
5008 .compile_delete()?;
5009 #bulk_delete_run
5010 }
5011
5012 /// Fetch every row where `<col> = <val>`. Eloquent
5013 /// `Model::where($col, $val)->get()` parity.
5014 ///
5015 /// Thin wrapper over `QuerySet::<Self>::default()
5016 /// .filter(col, val).fetch(pool)`. For one row,
5017 /// use [`Self::first_where`]; for a chain that
5018 /// needs further `.filter()` / `.order_by()` /
5019 /// `.limit()`, drop down to `Self::query().filter(...)`
5020 /// directly.
5021 ///
5022 /// `val` accepts any value `Into<SqlValue>` so plain
5023 /// strings, ints, UUIDs, etc. all work.
5024 ///
5025 /// # Errors
5026 /// As [`FetcherPool::fetch`].
5027 ///
5028 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5029 pub async fn where_(
5030 col: &str,
5031 val: impl ::core::convert::Into<#root::core::SqlValue>,
5032 pool: &#root::sql::Pool,
5033 ) -> ::core::result::Result<
5034 ::std::vec::Vec<Self>,
5035 #root::sql::ExecError,
5036 > {
5037 use #root::sql::FetcherPool as _;
5038 #root::query::QuerySet::<Self>::default()
5039 .filter(col, val)
5040 .fetch(pool)
5041 .await
5042 }
5043
5044 /// Fetch every row where `<col> IN (vals)`. Eloquent
5045 /// `Model::whereIn($col, $vals)->get()` parity. Empty
5046 /// `vals` returns no rows (matches SQL's empty-IN
5047 /// semantics).
5048 ///
5049 /// `vals` accepts any iterable whose items are
5050 /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
5051 /// etc.
5052 ///
5053 /// # Errors
5054 /// As [`FetcherPool::fetch`].
5055 ///
5056 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5057 pub async fn where_in<V>(
5058 col: &str,
5059 vals: impl ::core::iter::IntoIterator<Item = V>,
5060 pool: &#root::sql::Pool,
5061 ) -> ::core::result::Result<
5062 ::std::vec::Vec<Self>,
5063 #root::sql::ExecError,
5064 >
5065 where
5066 V: ::core::convert::Into<#root::core::SqlValue>,
5067 {
5068 use #root::sql::FetcherPool as _;
5069 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5070 vals.into_iter().map(::core::convert::Into::into).collect();
5071 if _values.is_empty() {
5072 return ::core::result::Result::Ok(::std::vec::Vec::new());
5073 }
5074 let _key = ::std::format!("{}__in", col);
5075 #root::query::QuerySet::<Self>::default()
5076 .filter(&_key, #root::core::SqlValue::List(_values))
5077 .fetch(pool)
5078 .await
5079 }
5080
5081 /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
5082 /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
5083 /// `vals` returns every row (matches SQL's empty-NOT-IN
5084 /// semantics — vacuously true for every row).
5085 ///
5086 /// # Errors
5087 /// As [`FetcherPool::fetch`].
5088 ///
5089 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5090 pub async fn where_not_in<V>(
5091 col: &str,
5092 vals: impl ::core::iter::IntoIterator<Item = V>,
5093 pool: &#root::sql::Pool,
5094 ) -> ::core::result::Result<
5095 ::std::vec::Vec<Self>,
5096 #root::sql::ExecError,
5097 >
5098 where
5099 V: ::core::convert::Into<#root::core::SqlValue>,
5100 {
5101 use #root::sql::FetcherPool as _;
5102 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5103 vals.into_iter().map(::core::convert::Into::into).collect();
5104 if _values.is_empty() {
5105 return #root::query::QuerySet::<Self>::default()
5106 .fetch(pool)
5107 .await;
5108 }
5109 let _key = ::std::format!("{}__not_in", col);
5110 #root::query::QuerySet::<Self>::default()
5111 .filter(&_key, #root::core::SqlValue::List(_values))
5112 .fetch(pool)
5113 .await
5114 }
5115
5116 /// Fetch every row where `<col> IS NULL`. Eloquent
5117 /// `Model::whereNull($col)->get()` parity.
5118 ///
5119 /// # Errors
5120 /// As [`FetcherPool::fetch`].
5121 ///
5122 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5123 pub async fn where_null(
5124 col: &str,
5125 pool: &#root::sql::Pool,
5126 ) -> ::core::result::Result<
5127 ::std::vec::Vec<Self>,
5128 #root::sql::ExecError,
5129 > {
5130 use #root::sql::FetcherPool as _;
5131 let _key = ::std::format!("{}__isnull", col);
5132 #root::query::QuerySet::<Self>::default()
5133 .filter(&_key, #root::core::SqlValue::Bool(true))
5134 .fetch(pool)
5135 .await
5136 }
5137
5138 /// Fetch every row where `<col> IS NOT NULL`. Eloquent
5139 /// `Model::whereNotNull($col)->get()` parity.
5140 ///
5141 /// # Errors
5142 /// As [`FetcherPool::fetch`].
5143 ///
5144 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5145 pub async fn where_not_null(
5146 col: &str,
5147 pool: &#root::sql::Pool,
5148 ) -> ::core::result::Result<
5149 ::std::vec::Vec<Self>,
5150 #root::sql::ExecError,
5151 > {
5152 use #root::sql::FetcherPool as _;
5153 let _key = ::std::format!("{}__isnull", col);
5154 #root::query::QuerySet::<Self>::default()
5155 .filter(&_key, #root::core::SqlValue::Bool(false))
5156 .fetch(pool)
5157 .await
5158 }
5159
5160 /// Fetch up to `n` rows in random order. Eloquent
5161 /// `Model::inRandomOrder()->limit($n)->get()` /
5162 /// `Model::query()->inRandomOrder()->get()->take($n)`
5163 /// parity. **Performance caveat**: random ordering
5164 /// forces a full table scan + per-row random key sort;
5165 /// the optimizer cannot use an index. Prefer a
5166 /// `pk >= rand_offset LIMIT N` walk for huge tables.
5167 ///
5168 /// # Errors
5169 /// As [`FetcherPool::fetch`].
5170 ///
5171 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5172 pub async fn random_n(
5173 n: i64,
5174 pool: &#root::sql::Pool,
5175 ) -> ::core::result::Result<
5176 ::std::vec::Vec<Self>,
5177 #root::sql::ExecError,
5178 > {
5179 use #root::sql::FetcherPool as _;
5180 #root::query::QuerySet::<Self>::default()
5181 .order_random()
5182 .limit(n)
5183 .fetch(pool)
5184 .await
5185 }
5186
5187 /// Fetch one row in random order. Eloquent
5188 /// `Model::inRandomOrder()->first()` parity. Same
5189 /// performance caveat as [`Self::random_n`].
5190 ///
5191 /// # Errors
5192 /// As [`FetcherPool::fetch`].
5193 ///
5194 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5195 pub async fn random(
5196 pool: &#root::sql::Pool,
5197 ) -> ::core::result::Result<
5198 ::core::option::Option<Self>,
5199 #root::sql::ExecError,
5200 > {
5201 ::core::result::Result::Ok(
5202 Self::random_n(1, pool).await?.into_iter().next(),
5203 )
5204 }
5205
5206 /// Fetch every row ordered ASC by `field`. Eloquent
5207 /// `Model::oldest($field)->get()` parity — the multi-row
5208 /// counterpart of [`Self::earliest`].
5209 ///
5210 /// # Errors
5211 /// As [`FetcherPool::fetch`].
5212 ///
5213 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5214 pub async fn oldest(
5215 field: &str,
5216 pool: &#root::sql::Pool,
5217 ) -> ::core::result::Result<
5218 ::std::vec::Vec<Self>,
5219 #root::sql::ExecError,
5220 > {
5221 use #root::sql::FetcherPool as _;
5222 #root::query::QuerySet::<Self>::default()
5223 .order_by(&[(field, false)])
5224 .fetch(pool)
5225 .await
5226 }
5227
5228 /// Fetch every row ordered DESC by `field`. Eloquent
5229 /// `Model::latest($field)->get()` parity — the multi-row
5230 /// counterpart of [`Self::latest`].
5231 ///
5232 /// # Errors
5233 /// As [`FetcherPool::fetch`].
5234 ///
5235 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5236 pub async fn newest(
5237 field: &str,
5238 pool: &#root::sql::Pool,
5239 ) -> ::core::result::Result<
5240 ::std::vec::Vec<Self>,
5241 #root::sql::ExecError,
5242 > {
5243 use #root::sql::FetcherPool as _;
5244 #root::query::QuerySet::<Self>::default()
5245 .order_by(&[(field, true)])
5246 .fetch(pool)
5247 .await
5248 }
5249
5250 /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5251 /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5252 /// Routes through the existing `__year` lookup suffix
5253 /// (issue #829).
5254 ///
5255 /// # Errors
5256 /// As [`FetcherPool::fetch`].
5257 ///
5258 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5259 pub async fn where_year(
5260 col: &str,
5261 year: i64,
5262 pool: &#root::sql::Pool,
5263 ) -> ::core::result::Result<
5264 ::std::vec::Vec<Self>,
5265 #root::sql::ExecError,
5266 > {
5267 use #root::sql::FetcherPool as _;
5268 let _key = ::std::format!("{}__year", col);
5269 #root::query::QuerySet::<Self>::default()
5270 .filter(&_key, #root::core::SqlValue::I64(year))
5271 .fetch(pool)
5272 .await
5273 }
5274
5275 /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5276 /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5277 /// `month` is 1–12.
5278 ///
5279 /// # Errors
5280 /// As [`FetcherPool::fetch`].
5281 ///
5282 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5283 pub async fn where_month(
5284 col: &str,
5285 month: i64,
5286 pool: &#root::sql::Pool,
5287 ) -> ::core::result::Result<
5288 ::std::vec::Vec<Self>,
5289 #root::sql::ExecError,
5290 > {
5291 use #root::sql::FetcherPool as _;
5292 let _key = ::std::format!("{}__month", col);
5293 #root::query::QuerySet::<Self>::default()
5294 .filter(&_key, #root::core::SqlValue::I64(month))
5295 .fetch(pool)
5296 .await
5297 }
5298
5299 /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5300 /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5301 /// `day` is 1–31.
5302 ///
5303 /// # Errors
5304 /// As [`FetcherPool::fetch`].
5305 ///
5306 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5307 pub async fn where_day(
5308 col: &str,
5309 day: i64,
5310 pool: &#root::sql::Pool,
5311 ) -> ::core::result::Result<
5312 ::std::vec::Vec<Self>,
5313 #root::sql::ExecError,
5314 > {
5315 use #root::sql::FetcherPool as _;
5316 let _key = ::std::format!("{}__day", col);
5317 #root::query::QuerySet::<Self>::default()
5318 .filter(&_key, #root::core::SqlValue::I64(day))
5319 .fetch(pool)
5320 .await
5321 }
5322
5323 /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5324 /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5325 /// `hour` is 0–23.
5326 ///
5327 /// # Errors
5328 /// As [`FetcherPool::fetch`].
5329 ///
5330 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5331 pub async fn where_hour(
5332 col: &str,
5333 hour: i64,
5334 pool: &#root::sql::Pool,
5335 ) -> ::core::result::Result<
5336 ::std::vec::Vec<Self>,
5337 #root::sql::ExecError,
5338 > {
5339 use #root::sql::FetcherPool as _;
5340 let _key = ::std::format!("{}__hour", col);
5341 #root::query::QuerySet::<Self>::default()
5342 .filter(&_key, #root::core::SqlValue::I64(hour))
5343 .fetch(pool)
5344 .await
5345 }
5346
5347 /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5348 /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5349 /// `minute` is 0–59.
5350 ///
5351 /// # Errors
5352 /// As [`FetcherPool::fetch`].
5353 ///
5354 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5355 pub async fn where_minute(
5356 col: &str,
5357 minute: i64,
5358 pool: &#root::sql::Pool,
5359 ) -> ::core::result::Result<
5360 ::std::vec::Vec<Self>,
5361 #root::sql::ExecError,
5362 > {
5363 use #root::sql::FetcherPool as _;
5364 let _key = ::std::format!("{}__minute", col);
5365 #root::query::QuerySet::<Self>::default()
5366 .filter(&_key, #root::core::SqlValue::I64(minute))
5367 .fetch(pool)
5368 .await
5369 }
5370
5371 /// Fetch every row where `<col> LIKE <pattern>` —
5372 /// caller-supplied pattern (must include `%` / `_`
5373 /// wildcards manually). Eloquent `Model::whereLike`
5374 /// parity.
5375 ///
5376 /// # Errors
5377 /// As [`FetcherPool::fetch`].
5378 ///
5379 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5380 pub async fn where_like(
5381 col: &str,
5382 pattern: impl ::core::convert::Into<::std::string::String>,
5383 pool: &#root::sql::Pool,
5384 ) -> ::core::result::Result<
5385 ::std::vec::Vec<Self>,
5386 #root::sql::ExecError,
5387 > {
5388 use #root::sql::FetcherPool as _;
5389 let _key = ::std::format!("{}__like", col);
5390 #root::query::QuerySet::<Self>::default()
5391 .filter(
5392 &_key,
5393 #root::core::SqlValue::String(pattern.into()),
5394 )
5395 .fetch(pool)
5396 .await
5397 }
5398
5399 /// Fetch every row where `<col> ILIKE <pattern>` —
5400 /// case-insensitive LIKE (PG native, MySQL/SQLite
5401 /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5402 ///
5403 /// # Errors
5404 /// As [`FetcherPool::fetch`].
5405 ///
5406 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5407 pub async fn where_ilike(
5408 col: &str,
5409 pattern: impl ::core::convert::Into<::std::string::String>,
5410 pool: &#root::sql::Pool,
5411 ) -> ::core::result::Result<
5412 ::std::vec::Vec<Self>,
5413 #root::sql::ExecError,
5414 > {
5415 use #root::sql::FetcherPool as _;
5416 let _key = ::std::format!("{}__ilike", col);
5417 #root::query::QuerySet::<Self>::default()
5418 .filter(
5419 &_key,
5420 #root::core::SqlValue::String(pattern.into()),
5421 )
5422 .fetch(pool)
5423 .await
5424 }
5425
5426 /// Fetch every row where `<col>` starts with `prefix`
5427 /// (auto-appends `%`). Eloquent
5428 /// `whereLike("col", "$prefix%")` parity.
5429 ///
5430 /// # Errors
5431 /// As [`FetcherPool::fetch`].
5432 ///
5433 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5434 pub async fn where_starts_with(
5435 col: &str,
5436 prefix: impl ::core::convert::Into<::std::string::String>,
5437 pool: &#root::sql::Pool,
5438 ) -> ::core::result::Result<
5439 ::std::vec::Vec<Self>,
5440 #root::sql::ExecError,
5441 > {
5442 use #root::sql::FetcherPool as _;
5443 let _key = ::std::format!("{}__startswith", col);
5444 #root::query::QuerySet::<Self>::default()
5445 .filter(
5446 &_key,
5447 #root::core::SqlValue::String(prefix.into()),
5448 )
5449 .fetch(pool)
5450 .await
5451 }
5452
5453 /// Fetch every row where `<col>` ends with `suffix`
5454 /// (auto-prepends `%`). Eloquent
5455 /// `whereLike("col", "%$suffix")` parity.
5456 ///
5457 /// # Errors
5458 /// As [`FetcherPool::fetch`].
5459 ///
5460 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5461 pub async fn where_ends_with(
5462 col: &str,
5463 suffix: impl ::core::convert::Into<::std::string::String>,
5464 pool: &#root::sql::Pool,
5465 ) -> ::core::result::Result<
5466 ::std::vec::Vec<Self>,
5467 #root::sql::ExecError,
5468 > {
5469 use #root::sql::FetcherPool as _;
5470 let _key = ::std::format!("{}__endswith", col);
5471 #root::query::QuerySet::<Self>::default()
5472 .filter(
5473 &_key,
5474 #root::core::SqlValue::String(suffix.into()),
5475 )
5476 .fetch(pool)
5477 .await
5478 }
5479
5480 /// Fetch every row where `<col>` contains `substr`
5481 /// (auto-wraps with `%`).
5482 /// Eloquent `whereLike("col", "%$substr%")` parity.
5483 ///
5484 /// # Errors
5485 /// As [`FetcherPool::fetch`].
5486 ///
5487 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5488 pub async fn where_contains(
5489 col: &str,
5490 substr: impl ::core::convert::Into<::std::string::String>,
5491 pool: &#root::sql::Pool,
5492 ) -> ::core::result::Result<
5493 ::std::vec::Vec<Self>,
5494 #root::sql::ExecError,
5495 > {
5496 use #root::sql::FetcherPool as _;
5497 let _key = ::std::format!("{}__contains", col);
5498 #root::query::QuerySet::<Self>::default()
5499 .filter(
5500 &_key,
5501 #root::core::SqlValue::String(substr.into()),
5502 )
5503 .fetch(pool)
5504 .await
5505 }
5506
5507 /// Fetch every row where `<col> > val`. Eloquent
5508 /// `Model::where($col, ">", $val)->get()` parity.
5509 ///
5510 /// # Errors
5511 /// As [`FetcherPool::fetch`].
5512 ///
5513 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5514 pub async fn where_gt(
5515 col: &str,
5516 val: impl ::core::convert::Into<#root::core::SqlValue>,
5517 pool: &#root::sql::Pool,
5518 ) -> ::core::result::Result<
5519 ::std::vec::Vec<Self>,
5520 #root::sql::ExecError,
5521 > {
5522 use #root::sql::FetcherPool as _;
5523 let _key = ::std::format!("{}__gt", col);
5524 #root::query::QuerySet::<Self>::default()
5525 .filter(&_key, val)
5526 .fetch(pool)
5527 .await
5528 }
5529
5530 /// Fetch every row where `<col> >= val`. Eloquent
5531 /// `Model::where($col, ">=", $val)->get()` parity.
5532 ///
5533 /// # Errors
5534 /// As [`FetcherPool::fetch`].
5535 ///
5536 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5537 pub async fn where_gte(
5538 col: &str,
5539 val: impl ::core::convert::Into<#root::core::SqlValue>,
5540 pool: &#root::sql::Pool,
5541 ) -> ::core::result::Result<
5542 ::std::vec::Vec<Self>,
5543 #root::sql::ExecError,
5544 > {
5545 use #root::sql::FetcherPool as _;
5546 let _key = ::std::format!("{}__gte", col);
5547 #root::query::QuerySet::<Self>::default()
5548 .filter(&_key, val)
5549 .fetch(pool)
5550 .await
5551 }
5552
5553 /// Fetch every row where `<col> < val`. Eloquent
5554 /// `Model::where($col, "<", $val)->get()` parity.
5555 ///
5556 /// # Errors
5557 /// As [`FetcherPool::fetch`].
5558 ///
5559 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5560 pub async fn where_lt(
5561 col: &str,
5562 val: impl ::core::convert::Into<#root::core::SqlValue>,
5563 pool: &#root::sql::Pool,
5564 ) -> ::core::result::Result<
5565 ::std::vec::Vec<Self>,
5566 #root::sql::ExecError,
5567 > {
5568 use #root::sql::FetcherPool as _;
5569 let _key = ::std::format!("{}__lt", col);
5570 #root::query::QuerySet::<Self>::default()
5571 .filter(&_key, val)
5572 .fetch(pool)
5573 .await
5574 }
5575
5576 /// Fetch every row where `<col> <= val`. Eloquent
5577 /// `Model::where($col, "<=", $val)->get()` parity.
5578 ///
5579 /// # Errors
5580 /// As [`FetcherPool::fetch`].
5581 ///
5582 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5583 pub async fn where_lte(
5584 col: &str,
5585 val: impl ::core::convert::Into<#root::core::SqlValue>,
5586 pool: &#root::sql::Pool,
5587 ) -> ::core::result::Result<
5588 ::std::vec::Vec<Self>,
5589 #root::sql::ExecError,
5590 > {
5591 use #root::sql::FetcherPool as _;
5592 let _key = ::std::format!("{}__lte", col);
5593 #root::query::QuerySet::<Self>::default()
5594 .filter(&_key, val)
5595 .fetch(pool)
5596 .await
5597 }
5598
5599 /// Fetch every row where `<col> <> val`. Eloquent
5600 /// `Model::where($col, "!=", $val)->get()` parity.
5601 ///
5602 /// # Errors
5603 /// As [`FetcherPool::fetch`].
5604 ///
5605 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5606 pub async fn where_ne(
5607 col: &str,
5608 val: impl ::core::convert::Into<#root::core::SqlValue>,
5609 pool: &#root::sql::Pool,
5610 ) -> ::core::result::Result<
5611 ::std::vec::Vec<Self>,
5612 #root::sql::ExecError,
5613 > {
5614 use #root::sql::FetcherPool as _;
5615 let _key = ::std::format!("{}__ne", col);
5616 #root::query::QuerySet::<Self>::default()
5617 .filter(&_key, val)
5618 .fetch(pool)
5619 .await
5620 }
5621
5622 /// Fetch every row matching `<col> = val` for ANY of the
5623 /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5624 /// parity. Empty `cols` returns no rows.
5625 ///
5626 /// Resolves each `&str` column to its SCHEMA-registered
5627 /// `&'static str` once and builds a single OR-composed
5628 /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5629 ///
5630 /// # Errors
5631 /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5632 /// when any column is not declared on the model.
5633 ///
5634 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5635 pub async fn where_any(
5636 cols: &[&str],
5637 val: impl ::core::convert::Into<#root::core::SqlValue>,
5638 pool: &#root::sql::Pool,
5639 ) -> ::core::result::Result<
5640 ::std::vec::Vec<Self>,
5641 #root::sql::ExecError,
5642 > {
5643 Self::__where_multi(cols, val, false, pool).await
5644 }
5645
5646 /// Fetch every row matching `<col> = val` for ALL listed
5647 /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5648 /// Empty `cols` returns every row (vacuous AND).
5649 ///
5650 /// # Errors
5651 /// As [`Self::where_any`].
5652 pub async fn where_all(
5653 cols: &[&str],
5654 val: impl ::core::convert::Into<#root::core::SqlValue>,
5655 pool: &#root::sql::Pool,
5656 ) -> ::core::result::Result<
5657 ::std::vec::Vec<Self>,
5658 #root::sql::ExecError,
5659 > {
5660 Self::__where_multi(cols, val, true, pool).await
5661 }
5662
5663 /// Internal: build a Q expression composing `cols` via
5664 /// AND (`all`) or OR (`!all`), then fetch. Backs
5665 /// `where_any` / `where_all`.
5666 #[doc(hidden)]
5667 pub async fn __where_multi(
5668 cols: &[&str],
5669 val: impl ::core::convert::Into<#root::core::SqlValue>,
5670 all: bool,
5671 pool: &#root::sql::Pool,
5672 ) -> ::core::result::Result<
5673 ::std::vec::Vec<Self>,
5674 #root::sql::ExecError,
5675 > {
5676 #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5677 .await
5678 }
5679
5680 /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5681 /// parity. PK-ordered
5682 /// is NOT guaranteed without an explicit `order_by` —
5683 /// drop into `Self::query()` for that.
5684 ///
5685 /// # Errors
5686 /// As [`FetcherPool::fetch`].
5687 ///
5688 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5689 pub async fn take(
5690 n: i64,
5691 pool: &#root::sql::Pool,
5692 ) -> ::core::result::Result<
5693 ::std::vec::Vec<Self>,
5694 #root::sql::ExecError,
5695 > {
5696 use #root::sql::FetcherPool as _;
5697 #root::query::QuerySet::<Self>::default()
5698 .limit(n)
5699 .fetch(pool)
5700 .await
5701 }
5702
5703 /// Fetch the page-th window of `per_page` rows
5704 /// (1-indexed). Eloquent
5705 /// `Model::query()->forPage($page, $perPage)->get()`
5706 /// parity. The DB scans an `OFFSET (page - 1) * per_page
5707 /// LIMIT per_page`; for large offsets this is O(N) —
5708 /// prefer keyset pagination via PK on hot paths.
5709 ///
5710 /// # Errors
5711 /// As [`FetcherPool::fetch`].
5712 ///
5713 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5714 pub async fn for_page(
5715 page: i64,
5716 per_page: i64,
5717 pool: &#root::sql::Pool,
5718 ) -> ::core::result::Result<
5719 ::std::vec::Vec<Self>,
5720 #root::sql::ExecError,
5721 > {
5722 use #root::sql::FetcherPool as _;
5723 let _offset = #root::list_params::page_offset(page, per_page);
5724 #root::query::QuerySet::<Self>::default()
5725 .limit(per_page)
5726 .offset(_offset)
5727 .fetch(pool)
5728 .await
5729 }
5730
5731 /// Eloquent `Model::paginate($per_page, $page)` — fetch
5732 /// one page of rows AND the total row count in one
5733 /// call. Returns `(rows, total)`. Two queries: a
5734 /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5735 /// for the total.
5736 ///
5737 /// Useful for paginated UIs that need both the visible
5738 /// rows AND a "Page X of Y" / total-count widget. For
5739 /// large tables prefer keyset pagination + cached count;
5740 /// every call to `paginate` re-counts the full table.
5741 ///
5742 /// Same 1-indexed `page` convention as [`Self::for_page`].
5743 ///
5744 /// # Errors
5745 /// As [`Self::for_page`] and [`Self::count`].
5746 pub async fn paginate(
5747 page: i64,
5748 per_page: i64,
5749 pool: &#root::sql::Pool,
5750 ) -> ::core::result::Result<
5751 (::std::vec::Vec<Self>, i64),
5752 #root::sql::ExecError,
5753 > {
5754 // Route through the queryset rather than `Self::count`:
5755 // the `count` inherent method is suppressed on models
5756 // that declare a field named `count` (field/shortcut
5757 // collision guard), and `paginate` must still compile
5758 // for those models.
5759 let total = {
5760 use #root::sql::CounterPool as _;
5761 #root::query::QuerySet::<Self>::default()
5762 .count(pool)
5763 .await?
5764 };
5765 let rows = Self::for_page(page, per_page, pool).await?;
5766 ::core::result::Result::Ok((rows, total))
5767 }
5768
5769 /// Bulk-update — set `set_col = set_val` on every row
5770 /// matching `where_col = where_val`. Returns affected row
5771 /// count. Eloquent
5772 /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5773 /// parity.
5774 ///
5775 /// For multi-column updates drop into the queryset
5776 /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5777 ///
5778 /// # Errors
5779 /// As [`UpdaterPool::execute_pool`].
5780 ///
5781 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5782 pub async fn update_where(
5783 where_col: &str,
5784 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5785 set_col: &str,
5786 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5787 pool: &#root::sql::Pool,
5788 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5789 let _query = #root::query::QuerySet::<Self>::default()
5790 .filter(where_col, where_val)
5791 .update()
5792 .set(set_col, set_val)
5793 .compile()?;
5794 #bulk_update_run
5795 }
5796
5797 /// Bulk-delete — remove every row matching
5798 /// `where_col = where_val`. Returns affected row count.
5799 /// Eloquent
5800 /// `Model::where($where_col, $where_val)->delete()` parity.
5801 ///
5802 /// # Errors
5803 /// As [`#root::sql::delete_pool`].
5804 pub async fn delete_where(
5805 where_col: &str,
5806 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5807 pool: &#root::sql::Pool,
5808 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5809 let _query = #root::query::QuerySet::<Self>::default()
5810 .filter_op(where_col, #root::core::Op::Eq, where_val)
5811 .compile_delete()?;
5812 #bulk_delete_run
5813 }
5814
5815 /// Bulk-update — set `set_col = set_val` on EVERY row of
5816 /// this model's table. **No WHERE clause** — use with
5817 /// care. Eloquent `Model::query()->update([$col => $val])`
5818 /// parity.
5819 ///
5820 /// Use for backfills, one-shot reset flows, etc. The
5821 /// returned count is rows affected.
5822 ///
5823 /// # Errors
5824 /// As [`UpdaterPool::execute_pool`].
5825 ///
5826 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5827 pub async fn update_all(
5828 set_col: &str,
5829 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5830 pool: &#root::sql::Pool,
5831 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5832 let _query = #root::query::QuerySet::<Self>::default()
5833 .update()
5834 .set(set_col, set_val)
5835 .compile()?;
5836 #bulk_update_run
5837 }
5838
5839 /// Fetch every row where `<col> NOT LIKE <pattern>`.
5840 /// Eloquent `Model::whereNotLike` parity. Pattern is
5841 /// passed verbatim — caller controls `%` / `_`.
5842 ///
5843 /// # Errors
5844 /// As [`FetcherPool::fetch`].
5845 ///
5846 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5847 pub async fn where_not_like(
5848 col: &str,
5849 pattern: impl ::core::convert::Into<::std::string::String>,
5850 pool: &#root::sql::Pool,
5851 ) -> ::core::result::Result<
5852 ::std::vec::Vec<Self>,
5853 #root::sql::ExecError,
5854 > {
5855 use #root::sql::FetcherPool as _;
5856 let _key = ::std::format!("{}__not_like", col);
5857 #root::query::QuerySet::<Self>::default()
5858 .filter(
5859 &_key,
5860 #root::core::SqlValue::String(pattern.into()),
5861 )
5862 .fetch(pool)
5863 .await
5864 }
5865
5866 /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5867 /// case-insensitive `NOT LIKE` (PG native, MySQL /
5868 /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5869 ///
5870 /// # Errors
5871 /// As [`FetcherPool::fetch`].
5872 ///
5873 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5874 pub async fn where_not_ilike(
5875 col: &str,
5876 pattern: impl ::core::convert::Into<::std::string::String>,
5877 pool: &#root::sql::Pool,
5878 ) -> ::core::result::Result<
5879 ::std::vec::Vec<Self>,
5880 #root::sql::ExecError,
5881 > {
5882 use #root::sql::FetcherPool as _;
5883 let _key = ::std::format!("{}__not_ilike", col);
5884 #root::query::QuerySet::<Self>::default()
5885 .filter(
5886 &_key,
5887 #root::core::SqlValue::String(pattern.into()),
5888 )
5889 .fetch(pool)
5890 .await
5891 }
5892
5893 /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5894 /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5895 /// parity.
5896 ///
5897 /// # Errors
5898 /// As [`FetcherPool::fetch`].
5899 ///
5900 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5901 pub async fn where_not_between(
5902 col: &str,
5903 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5904 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5905 pool: &#root::sql::Pool,
5906 ) -> ::core::result::Result<
5907 ::std::vec::Vec<Self>,
5908 #root::sql::ExecError,
5909 > {
5910 use #root::sql::FetcherPool as _;
5911 let _key = ::std::format!("{}__not_between", col);
5912 let _vals = #root::core::SqlValue::List(::std::vec![
5913 ::core::convert::Into::into(lo),
5914 ::core::convert::Into::into(hi),
5915 ]);
5916 #root::query::QuerySet::<Self>::default()
5917 .filter(&_key, _vals)
5918 .fetch(pool)
5919 .await
5920 }
5921
5922 /// Returns the SQL table name for this model. Eloquent
5923 /// `$model->getTable()` parity.
5924 #[must_use]
5925 pub fn table_name() -> &'static str {
5926 <Self as #root::core::Model>::SCHEMA.table
5927 }
5928
5929 /// Returns the SQL column name of this model's primary
5930 /// key, or `None` when the model has no
5931 /// `#[rustango(primary_key)]`. Eloquent
5932 /// `$model->getKeyName()` parity.
5933 #[must_use]
5934 pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5935 <Self as #root::core::Model>::SCHEMA
5936 .primary_key()
5937 .map(|f| f.column)
5938 }
5939
5940 /// Fetch every row where `<col> BETWEEN lo AND hi`
5941 /// (inclusive on both ends — same as SQL). Eloquent
5942 /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5943 ///
5944 /// # Errors
5945 /// As [`FetcherPool::fetch`].
5946 ///
5947 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5948 pub async fn where_between(
5949 col: &str,
5950 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5951 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5952 pool: &#root::sql::Pool,
5953 ) -> ::core::result::Result<
5954 ::std::vec::Vec<Self>,
5955 #root::sql::ExecError,
5956 > {
5957 use #root::sql::FetcherPool as _;
5958 let _key = ::std::format!("{}__between", col);
5959 let _vals = #root::core::SqlValue::List(::std::vec![
5960 ::core::convert::Into::into(lo),
5961 ::core::convert::Into::into(hi),
5962 ]);
5963 #root::query::QuerySet::<Self>::default()
5964 .filter(&_key, _vals)
5965 .fetch(pool)
5966 .await
5967 }
5968
5969 /// Fetch the first row where `<col> = <val>`. Returns
5970 /// `Ok(None)` when no row matches. Eloquent
5971 /// `Model::firstWhere($col, $val)` parity.
5972 ///
5973 /// Thin wrapper over `QuerySet::<Self>::default()
5974 /// .filter(col, val).first(pool)`. Use this when you
5975 /// want one row identified by a non-PK column (e.g.
5976 /// `User::first_where_pool("email", "x@y.com", &pool)`).
5977 ///
5978 /// `val` accepts any value `Into<SqlValue>` so plain
5979 /// strings, ints, UUIDs, etc. all work.
5980 ///
5981 /// # Errors
5982 /// As `QuerySet::first`.
5983 pub async fn first_where(
5984 col: &str,
5985 val: impl ::core::convert::Into<#root::core::SqlValue>,
5986 pool: &#root::sql::Pool,
5987 ) -> ::core::result::Result<
5988 ::core::option::Option<Self>,
5989 #root::sql::ExecError,
5990 > {
5991 #root::query::QuerySet::<Self>::default()
5992 .filter(col, val)
5993 .first(pool)
5994 .await
5995 }
5996
5997 #value_method
5998
5999 /// Fetch the row with the largest `field` value —
6000 /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
6001 /// `Ok(None)` for an empty table. Eloquent
6002 /// `Model::latest($field)->first()` parity.
6003 /// Thin wrapper over `QuerySet::<Self>::default()
6004 /// .latest(field, pool)`.
6005 ///
6006 /// **Field name** is the Rust field ident as a string
6007 /// (not the SQL column). It is a runtime `&str`, so an
6008 /// unknown one compiles and surfaces as
6009 /// `ExecError::Query(QueryError::UnknownField)` when the
6010 /// query runs.
6011 ///
6012 /// # Errors
6013 /// As `QuerySet::latest`.
6014 pub async fn latest(
6015 field: &str,
6016 pool: &#root::sql::Pool,
6017 ) -> ::core::result::Result<
6018 ::core::option::Option<Self>,
6019 #root::sql::ExecError,
6020 > {
6021 #root::query::QuerySet::<Self>::default()
6022 .latest(field, pool)
6023 .await
6024 }
6025
6026 /// Sibling of [`Self::latest`] — fetches the row
6027 /// with the smallest `field` value (`ORDER BY <field>
6028 /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
6029 /// ->first()` parity.
6030 ///
6031 /// # Errors
6032 /// As [`Self::latest`].
6033 pub async fn earliest(
6034 field: &str,
6035 pool: &#root::sql::Pool,
6036 ) -> ::core::result::Result<
6037 ::core::option::Option<Self>,
6038 #root::sql::ExecError,
6039 > {
6040 #root::query::QuerySet::<Self>::default()
6041 .earliest(field, pool)
6042 .await
6043 }
6044
6045 #count_method
6046
6047 /// `true` when the table contains at least one row.
6048 /// Eloquent `Model::query()->exists()` parity. Thin wrapper
6049 /// over `QuerySet::<Self>::default().exists(pool)`.
6050 ///
6051 /// # Errors
6052 /// As [`ExistsPool::exists`].
6053 ///
6054 /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
6055 pub async fn exists(
6056 pool: &#root::sql::Pool,
6057 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6058 use #root::sql::ExistsPool as _;
6059 #root::query::QuerySet::<Self>::default()
6060 .exists(pool)
6061 .await
6062 }
6063
6064 /// Inverse of [`Self::exists`] — returns `true` when
6065 /// the table has zero rows. Eloquent
6066 /// `Model::doesntExist()` parity.
6067 ///
6068 /// # Errors
6069 /// As [`Self::exists`].
6070 pub async fn doesnt_exist(
6071 pool: &#root::sql::Pool,
6072 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6073 Self::exists(pool).await.map(|e| !e)
6074 }
6075
6076 /// Eloquent `Model::query()->whereKey($pk)->exists()` —
6077 /// `true` when a row with primary key `pk` exists in
6078 /// the table. Sugar over
6079 /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
6080 ///
6081 /// Differs from [`Self::exists`] (which checks "any row
6082 /// in the table") by checking a specific PK existence.
6083 /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
6084 /// because the row is never materialized — the SQL is
6085 /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
6086 ///
6087 /// # Errors
6088 /// As [`#root::sql::ExistsPool::contains_pk`].
6089 pub async fn contains_pk(
6090 pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
6091 pool: &#root::sql::Pool,
6092 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6093 use #root::sql::ExistsPool as _;
6094 #root::query::QuerySet::<Self>::default()
6095 .contains_pk(pool, pk)
6096 .await
6097 }
6098
6099 #sum_method
6100 #avg_method
6101 #min_method
6102 #max_method
6103
6104 /// Fetch every row of this model from `pool`. Eloquent
6105 /// `Model::all()` parity — a thin wrapper over
6106 /// `QuerySet::<Self>::default().fetch(pool)`.
6107 ///
6108 /// **Use with care on large tables** — there's no
6109 /// pagination or limit; the entire table is materialized
6110 /// into memory. For anything beyond fixture / lookup
6111 /// tables, page through `QuerySet::<Self>::default()
6112 /// .order_by(...).limit(N).offset(M).fetch(pool)`
6113 /// or stream via `.iterator(chunk_size)`.
6114 ///
6115 /// # Errors
6116 /// As [`FetcherPool::fetch`].
6117 ///
6118 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6119 pub async fn all(
6120 pool: &#root::sql::Pool,
6121 ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
6122 {
6123 use #root::sql::FetcherPool as _;
6124 #root::query::QuerySet::<Self>::default()
6125 .fetch(pool)
6126 .await
6127 }
6128
6129 /// Look up every row whose primary key is in `pks`.
6130 /// Returns the matching rows in **inventory** order — NOT
6131 /// the order of `pks`. Empty `pks` returns an empty
6132 /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
6133 /// with a list) parity.
6134 ///
6135 /// Thin wrapper over `QuerySet::<Self>::default()
6136 /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
6137 /// Caller-supplied PKs that don't match a row are
6138 /// silently skipped (the returned vec is shorter than
6139 /// the input list). For an order-preserving / "fail
6140 /// when any missing" variant, build the queryset
6141 /// explicitly with `in_bulk` instead.
6142 ///
6143 /// Accepts any iterable whose elements are
6144 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
6145 /// `Vec<Uuid>`, etc.
6146 ///
6147 /// # Errors
6148 /// As [`FetcherPool::fetch`].
6149 ///
6150 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6151 pub async fn find_many<V>(
6152 pks: impl ::core::iter::IntoIterator<Item = V>,
6153 pool: &#root::sql::Pool,
6154 ) -> ::core::result::Result<
6155 ::std::vec::Vec<Self>,
6156 #root::sql::ExecError,
6157 >
6158 where
6159 V: ::core::convert::Into<#root::core::SqlValue>,
6160 {
6161 use #root::sql::FetcherPool as _;
6162 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6163 pks.into_iter().map(::core::convert::Into::into).collect();
6164 if _values.is_empty() {
6165 return ::core::result::Result::Ok(::std::vec::Vec::new());
6166 }
6167 let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
6168 #root::query::QuerySet::<Self>::default()
6169 .filter(&_key, #root::core::SqlValue::List(_values))
6170 .fetch(pool)
6171 .await
6172 }
6173
6174 /// Look up the row whose primary key equals `pk`. Returns
6175 /// `Ok(None)` when no row matches, rather than erroring.
6176 /// Eloquent `Model::find`
6177 /// shape — accepts any value `Into<SqlValue>`.
6178 ///
6179 /// One-liner shortcut for the common
6180 /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6181 /// .limit(1).fetch(pool).await?.into_iter().next()`
6182 /// dance.
6183 ///
6184 /// # Errors
6185 /// As [`FetcherPool::fetch`].
6186 ///
6187 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6188 pub async fn find(
6189 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6190 pool: &#root::sql::Pool,
6191 ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6192 {
6193 use #root::sql::FetcherPool as _;
6194 let _pk_val: #root::core::SqlValue = pk.into();
6195 let mut _rows: ::std::vec::Vec<Self> =
6196 #root::query::QuerySet::<Self>::default()
6197 .filter(::core::stringify!(#pk_ident), _pk_val)
6198 .limit(1)
6199 .fetch(pool)
6200 .await?;
6201 ::core::result::Result::Ok(_rows.into_iter().next())
6202 }
6203
6204 /// Look up the row whose primary key equals `pk`. Errors
6205 /// when no row matches — the throwing counterpart of
6206 /// [`Self::find`]. Eloquent `Model::findOrFail`
6207 /// parity.
6208 ///
6209 /// Translates the miss into
6210 /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6211 /// so callers can `?`-bubble straight through the typical
6212 /// `ExecError` error chain.
6213 ///
6214 /// # Errors
6215 /// As [`Self::find`]; additionally
6216 /// [`sqlx::Error::RowNotFound`] when no row matches.
6217 ///
6218 /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6219 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6220 pub async fn find_or_fail(
6221 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6222 pool: &#root::sql::Pool,
6223 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6224 match Self::find(pk, pool).await? {
6225 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6226 ::core::option::Option::None => ::core::result::Result::Err(
6227 #root::sql::ExecError::Driver(
6228 #root::sql::sqlx::Error::RowNotFound,
6229 ),
6230 ),
6231 }
6232 }
6233
6234 /// Look up multiple rows by primary key, **failing** if
6235 /// any requested PK is missing. Eloquent
6236 /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6237 /// inventory order (NOT request order — same as
6238 /// [`Self::find_many`]).
6239 ///
6240 /// Empty `pks` returns an empty `Vec` (no rows requested
6241 /// → nothing to fail on).
6242 ///
6243 /// Differs from [`Self::find_many`] in that this method
6244 /// requires every PK to resolve. If even one is missing,
6245 /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6246 /// in `ExecError::Driver`). Rows are deduped at the SQL
6247 /// layer, so passing the same PK twice in `pks` counts as
6248 /// one expected row.
6249 ///
6250 /// # Errors
6251 /// As [`Self::find_many`], plus `RowNotFound` when the
6252 /// returned row count is less than the count of distinct
6253 /// requested PKs.
6254 pub async fn find_many_or_fail<V>(
6255 pks: impl ::core::iter::IntoIterator<Item = V>,
6256 pool: &#root::sql::Pool,
6257 ) -> ::core::result::Result<
6258 ::std::vec::Vec<Self>,
6259 #root::sql::ExecError,
6260 >
6261 where
6262 V: ::core::convert::Into<#root::core::SqlValue>,
6263 {
6264 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6265 pks.into_iter().map(::core::convert::Into::into).collect();
6266 if _values.is_empty() {
6267 return ::core::result::Result::Ok(::std::vec::Vec::new());
6268 }
6269 // Dedup the requested PK list before counting so
6270 // duplicate-PK requests don't false-fail (the SQL
6271 // `IN (...)` clause naturally dedups too).
6272 let mut _seen: ::std::collections::HashSet<
6273 ::std::string::String,
6274 > = ::std::collections::HashSet::new();
6275 for v in &_values {
6276 _seen.insert(v.to_display_string());
6277 }
6278 let _expected = _seen.len();
6279 let _rows = Self::find_many(_values, pool).await?;
6280 if _rows.len() < _expected {
6281 return ::core::result::Result::Err(
6282 #root::sql::ExecError::Driver(
6283 #root::sql::sqlx::Error::RowNotFound,
6284 ),
6285 );
6286 }
6287 ::core::result::Result::Ok(_rows)
6288 }
6289
6290 /// Find by primary key or run `fallback` to produce a
6291 /// default row to return. Eloquent
6292 /// `Model::findOr($pk, fn() => …)` parity.
6293 ///
6294 /// Unlike [`Self::find_or_fail`] (which raises on
6295 /// miss), this is the "give me something sensible"
6296 /// branch: typical use is "fetch the user's row, else
6297 /// fall back to an anonymous/guest stub".
6298 ///
6299 /// `fallback` runs only when no row matches — the DB
6300 /// round-trip happens unconditionally.
6301 ///
6302 /// # Errors
6303 /// As [`Self::find`].
6304 pub async fn find_or<F>(
6305 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6306 pool: &#root::sql::Pool,
6307 fallback: F,
6308 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6309 where
6310 F: ::core::ops::FnOnce() -> Self,
6311 {
6312 ::core::result::Result::Ok(
6313 Self::find(pk, pool).await?.unwrap_or_else(fallback),
6314 )
6315 }
6316
6317 /// Same as [`Self::find_or`] but also returns a `bool`
6318 /// indicating whether the row was found in the DB
6319 /// (`true`) or freshly built from `fallback` (`false`).
6320 /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6321 /// in PHP the returned model also exposes `->exists`;
6322 /// here that's surfaced as the second tuple element.
6323 ///
6324 /// Useful for edit-or-create form handlers where the
6325 /// caller needs to know whether to PATCH or POST when
6326 /// the user submits the form.
6327 ///
6328 /// # Errors
6329 /// As [`Self::find`].
6330 pub async fn find_or_new<F>(
6331 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6332 pool: &#root::sql::Pool,
6333 fallback: F,
6334 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6335 where
6336 F: ::core::ops::FnOnce() -> Self,
6337 {
6338 match Self::find(pk, pool).await? {
6339 ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6340 ::core::option::Option::None => {
6341 ::core::result::Result::Ok((fallback(), false))
6342 }
6343 }
6344 }
6345
6346 /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6347 /// [`Self::find_or_new`] but **persists** the new row
6348 /// when the PK isn't found. Returns `(row, exists: bool)`
6349 /// — `exists=true` when the PK was found,
6350 /// `false` when a fresh row was inserted.
6351 ///
6352 /// ```ignore
6353 /// let (post, found) = Post::find_or_insert(
6354 /// pk,
6355 /// &pool,
6356 /// || Post { id: Auto::default(), title: "new".into() },
6357 /// ).await?;
6358 /// // `found` true → returned existing row;
6359 /// // `found` false → fallback was inserted, `post.id` populated.
6360 /// ```
6361 ///
6362 /// **Caveat**: two concurrent calls that both miss the
6363 /// find can both INSERT, violating uniqueness. Wrap in a
6364 /// transaction or rely on a UNIQUE constraint + handle
6365 /// the conflict error if you need race-free semantics.
6366 ///
6367 /// # Errors
6368 /// As [`Self::find`] and [`Self::save_pool`].
6369 pub async fn find_or_insert<F>(
6370 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6371 pool: &#root::sql::Pool,
6372 fallback: F,
6373 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6374 where
6375 F: ::core::ops::FnOnce() -> Self,
6376 {
6377 if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6378 return ::core::result::Result::Ok((_row, true));
6379 }
6380 let mut _new = fallback();
6381 _new.save_pool(pool).await?;
6382 ::core::result::Result::Ok((_new, false))
6383 }
6384
6385 /// Fetch the first row of the table, or run `fallback`
6386 /// when the table is empty. Eloquent
6387 /// `Model::firstOr(fn() => …)` parity.
6388 ///
6389 /// # Errors
6390 /// As [`Self::first`].
6391 pub async fn first_or<F>(
6392 pool: &#root::sql::Pool,
6393 fallback: F,
6394 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6395 where
6396 F: ::core::ops::FnOnce() -> Self,
6397 {
6398 // Route through the queryset rather than `Self::first`,
6399 // which is suppressed on models with a field named
6400 // `first` (field/shortcut collision guard).
6401 ::core::result::Result::Ok(
6402 #root::query::QuerySet::<Self>::default()
6403 .first(pool)
6404 .await?
6405 .unwrap_or_else(fallback),
6406 )
6407 }
6408
6409 /// Fetch exactly one row matching `<col> = <val>`. Errors
6410 /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6411 /// or more than one matches
6412 /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6413 /// Eloquent `Model::sole($col, $val)` parity.
6414 ///
6415 /// # Errors
6416 /// As [`Self::where_`] plus the explicit
6417 /// `RowNotFound` / `MultipleRowsReturned` cases above.
6418 pub async fn sole(
6419 col: &str,
6420 val: impl ::core::convert::Into<#root::core::SqlValue>,
6421 pool: &#root::sql::Pool,
6422 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6423 let mut _rows = Self::where_(col, val, pool).await?;
6424 match _rows.len() {
6425 0 => ::core::result::Result::Err(
6426 #root::sql::ExecError::Driver(
6427 #root::sql::sqlx::Error::RowNotFound,
6428 ),
6429 ),
6430 1 => ::core::result::Result::Ok(_rows.remove(0)),
6431 n => ::core::result::Result::Err(
6432 #root::sql::ExecError::MultipleRowsReturned {
6433 op: "sole",
6434 table: <Self as #root::core::Model>::SCHEMA.name,
6435 count: n,
6436 },
6437 ),
6438 }
6439 }
6440 }
6441 } else {
6442 quote!()
6443 };
6444
6445 // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6446 // the non-audited `_pool` methods but execute against an open
6447 // `PoolTx` so the writes participate in the caller's transaction.
6448 // Auditing inside TX is deferred; these always use the plain
6449 // executor primitives regardless of whether the model is audited.
6450 let tx_insert_method = if fields.has_auto {
6451 let pushes = &fields.insert_pushes;
6452 let returning_cols = &fields.returning_cols;
6453 quote! {
6454 /// Insert this row inside an open transaction, populating
6455 /// any `Auto<T>` PK from the auto-assigned value. Works
6456 /// against any backend that `tx` wraps.
6457 ///
6458 /// # Errors
6459 /// As [`Self::insert_pool`].
6460 pub async fn insert_tx(
6461 &mut self,
6462 tx: &mut #root::sql::PoolTx<'_>,
6463 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6464 let mut _columns: ::std::vec::Vec<&'static str> =
6465 ::std::vec::Vec::new();
6466 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6467 ::std::vec::Vec::new();
6468 #( #pushes )*
6469 let _query = #root::core::InsertQuery::new(
6470 <Self as #root::core::Model>::SCHEMA,
6471 _columns,
6472 _values,
6473 )
6474 .returning(::std::vec![ #( #returning_cols ),* ]);
6475 let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6476 #root::sql::apply_auto_pk(_result, self)
6477 }
6478 }
6479 } else {
6480 let insert_columns = &fields.insert_columns;
6481 let insert_values = &fields.insert_values;
6482 quote! {
6483 /// Insert this row inside an open transaction.
6484 ///
6485 /// # Errors
6486 /// As [`Self::insert_pool`].
6487 pub async fn insert_tx(
6488 &self,
6489 tx: &mut #root::sql::PoolTx<'_>,
6490 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6491 let _query = #root::core::InsertQuery::new(
6492 <Self as #root::core::Model>::SCHEMA,
6493 ::std::vec![ #( #insert_columns ),* ],
6494 ::std::vec![ #( #insert_values ),* ],
6495 );
6496 #root::sql::insert_tx(tx, &_query).await
6497 }
6498 }
6499 };
6500
6501 let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6502 let pk_column_lit = pk_col.as_str();
6503 let assignments = &fields.update_assignments;
6504 let dispatch_unset = if fields.pk_is_auto {
6505 quote! {
6506 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6507 return self.insert_tx(tx).await.map(|()| 1u64);
6508 }
6509 }
6510 } else {
6511 quote!()
6512 };
6513 quote! {
6514 /// Save this row inside an open transaction. `INSERT` when
6515 /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6516 /// PK. Works against any backend that `tx` wraps.
6517 ///
6518 /// # Errors
6519 /// As [`Self::save_pool`].
6520 pub async fn save_tx(
6521 &mut self,
6522 tx: &mut #root::sql::PoolTx<'_>,
6523 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6524 #dispatch_unset
6525 let _query = #root::core::UpdateQuery::new(
6526 <Self as #root::core::Model>::SCHEMA,
6527 ::std::vec![ #( #assignments ),* ],
6528 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6529 #pk_column_lit,
6530 #root::core::Op::Eq,
6531 ::core::convert::Into::<#root::core::SqlValue>::into(
6532 ::core::clone::Clone::clone(&self.#pk_ident)
6533 ),
6534 )),
6535 );
6536 let _affected = #root::sql::update_tx(tx, &_query).await?;
6537 ::core::result::Result::Ok(_affected)
6538 }
6539 }
6540 } else {
6541 quote!()
6542 };
6543
6544 let tx_delete_method = {
6545 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6546 let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6547 if let Some(pk_ident) = pk_ident_for_tx {
6548 quote! {
6549 /// Delete the row identified by this instance's PK
6550 /// inside an open transaction. Works against any backend
6551 /// that `tx` wraps.
6552 ///
6553 /// # Errors
6554 /// As [`Self::delete_pool`].
6555 pub async fn delete_tx(
6556 &self,
6557 tx: &mut #root::sql::PoolTx<'_>,
6558 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6559 let _query = #root::core::DeleteQuery::by_pk(
6560 <Self as #root::core::Model>::SCHEMA,
6561 #pk_column_lit,
6562 ::core::convert::Into::<#root::core::SqlValue>::into(
6563 ::core::clone::Clone::clone(&self.#pk_ident)
6564 ),
6565 );
6566 #root::sql::delete_tx(tx, &_query).await
6567 }
6568 }
6569 } else {
6570 quote!()
6571 }
6572 };
6573
6574 // Update emission captures both BEFORE and AFTER state — runs an
6575 // extra SELECT against `_executor` BEFORE the UPDATE, captures
6576 // each tracked field's prior value, then after the UPDATE diffs
6577 // against the in-memory `&self`. `diff_changes` drops unchanged
6578 // columns so the JSON only contains the actual delta.
6579 //
6580 // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6581 // and binds `_audit_before_pairs`; `audit_update_post` runs
6582 // after the UPDATE and emits the PendingEntry.
6583 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6584 audited_fields
6585 {
6586 if tracked.is_empty() {
6587 (quote!(), quote!())
6588 } else {
6589 let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
6590 let pk_column_lit_for_select =
6591 primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6592 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6593 if fields.pk_is_auto {
6594 quote!(self.#pk_ident.get().copied().unwrap_or_default())
6595 } else {
6596 quote!(::core::clone::Clone::clone(&self.#pk_ident))
6597 }
6598 } else {
6599 quote!(0_i64)
6600 };
6601 let before_pairs = tracked.iter().map(|c| {
6602 let column_lit = c.column.as_str();
6603 let value_ty = &c.value_ty;
6604 quote! {
6605 (
6606 #column_lit,
6607 match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6608 &_audit_before_row, #column_lit,
6609 ) {
6610 ::core::result::Result::Ok(v) => {
6611 #root::__serde_json::to_value(&v)
6612 .unwrap_or(#root::__serde_json::Value::Null)
6613 }
6614 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6615 },
6616 )
6617 }
6618 });
6619 let after_pairs = tracked.iter().map(|c| {
6620 let column_lit = c.column.as_str();
6621 let ident = &c.ident;
6622 quote! {
6623 (
6624 #column_lit,
6625 #root::__serde_json::to_value(&self.#ident)
6626 .unwrap_or(#root::__serde_json::Value::Null),
6627 )
6628 }
6629 });
6630 let pk_str = audit_pk_to_string.clone();
6631 let pre = quote! {
6632 let _audit_before_query = #root::audit::before_image_query(
6633 <Self as #root::core::Model>::SCHEMA,
6634 #pk_column_lit_for_select,
6635 ::core::convert::Into::<#root::core::SqlValue>::into(#pk_value_for_bind),
6636 &[ #( #tracked_cols ),* ],
6637 );
6638 // A failed pre-read must not let the UPDATE run unaudited (#1907).
6639 let _audit_before_pairs:
6640 ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6641 #root::sql::select_rows_on(&mut *_executor, &_audit_before_query)
6642 .await?
6643 .into_iter()
6644 .next()
6645 .map(|_audit_before_row| ::std::vec![ #( #before_pairs ),* ]);
6646 };
6647 let post = quote! {
6648 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6649 let _audit_after:
6650 ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
6651 ::std::vec![ #( #after_pairs ),* ];
6652 if let ::core::option::Option::Some(_audit_entry) =
6653 #root::audit::PendingEntry::update_diff(
6654 <Self as #root::core::Model>::SCHEMA.table,
6655 #pk_str,
6656 &_audit_before,
6657 &_audit_after,
6658 )
6659 {
6660 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6661 }
6662 }
6663 };
6664 (pre, post)
6665 }
6666 } else {
6667 (quote!(), quote!())
6668 };
6669
6670 // Bulk-insert audit: one `Create` entry per row once RETURNING has
6671 // set its PK, written in one batched INSERT.
6672 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6673 quote! {
6674 let _audit_entries: ::std::vec::Vec<#root::audit::PendingEntry> = rows
6675 .iter()
6676 .map(|_row| _row.__rustango_audit_entry(#root::audit::AuditOp::Create))
6677 .collect();
6678 #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6679 }
6680 } else {
6681 quote!()
6682 };
6683
6684 let save_method = if fields.pk_is_auto {
6685 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6686 let pk_column_lit = pk_column.as_str();
6687 let assignments = &fields.update_assignments;
6688 let upsert_cols = &fields.upsert_update_columns;
6689 let upsert_pushes = &fields.insert_pushes;
6690 let upsert_returning = &fields.returning_cols;
6691 let upsert_auto_assigns = &fields.auto_assigns;
6692 // Conflict target: prefer the first declared `unique_together`
6693 // when it exists. Plain `Auto<T>` PKs are server-assigned via
6694 // `BIGSERIAL` and never collide on insert, so a PK-only target
6695 // would silently turn `upsert()` into "always-insert" for
6696 // surrogate-PK models with composite UNIQUE constraints — see
6697 // `RolePermission` / `UserRole` / `UserPermission` in the
6698 // tenancy permission engine. When no `unique_together` is
6699 // declared we keep the PK target (the original behaviour).
6700 let upsert_target_columns: Vec<String> =
6701 upsert_unique.map_or_else(|| vec![pk_column.clone()], <[String]>::to_vec);
6702 let upsert_target_lits = upsert_target_columns
6703 .iter()
6704 .map(String::as_str)
6705 .collect::<Vec<_>>();
6706 let conflict_clause = if fields.upsert_update_columns.is_empty() {
6707 quote!(#root::core::ConflictClause::DoNothing)
6708 } else {
6709 quote!(#root::core::ConflictClause::DoUpdate {
6710 target: ::std::vec![ #( #upsert_target_lits ),* ],
6711 update_columns: ::std::vec![ #( #upsert_cols ),* ],
6712 })
6713 };
6714 // An upsert that may hit a conflict is recorded as `Update`; only
6715 // an unset PK with a PK-only target is surely a `Create`.
6716 let (audit_upsert_pre, audit_upsert_emit) = if audited_fields.is_some() {
6717 (
6718 if upsert_target_columns == [pk_column.clone()] {
6719 quote! {
6720 let _audit_op = if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6721 #root::audit::AuditOp::Create
6722 } else {
6723 #root::audit::AuditOp::Update
6724 };
6725 }
6726 } else {
6727 quote!(let _audit_op = #root::audit::AuditOp::Update;)
6728 },
6729 quote! {
6730 let _audit_entry = self.__rustango_audit_entry(_audit_op);
6731 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6732 },
6733 )
6734 } else {
6735 (quote!(), quote!())
6736 };
6737 Some(quote! {
6738 /// Insert this row if its `Auto<T>` primary key is
6739 /// `Unset`, otherwise update the existing row matching the
6740 /// PK. The caller doesn't need to pick `insert` vs the
6741 /// bulk-update path manually.
6742 ///
6743 /// On the insert branch, populates the PK from `RETURNING`
6744 /// (same behavior as `insert`). On the update branch,
6745 /// writes every non-PK column back; if no row matches the
6746 /// PK, returns `Ok(())` silently.
6747 ///
6748 /// Only generated when the primary key is declared as
6749 /// `Auto<T>`. Models with a manually-managed PK must use
6750 /// `insert` or the QuerySet update builder.
6751 ///
6752 /// **Postgres only — prefer [`Self::save_pool`].** This takes a
6753 /// driver-specific `PgPool`, so it does not exist at all on a
6754 /// build without the `postgres` feature: selecting `sqlite`
6755 /// makes the method vanish rather than fail with anything that
6756 /// names the cause.
6757 ///
6758 /// [`Self::save_pool`] takes `rustango::sql::Pool` and works on
6759 /// all three backends. The naming is inverted — the short name
6760 /// is the narrow one — and that is tracked in #1293.
6761 ///
6762 /// # Errors
6763 /// Returns [`#root::sql::ExecError`] for SQL-writing
6764 /// or driver failures.
6765 #[cfg(feature = "postgres")]
6766 pub async fn save(
6767 &mut self,
6768 pool: &#root::sql::sqlx::PgPool,
6769 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6770 #pool_to_save_on
6771 }
6772
6773 /// Like [`Self::save`] but accepts any sqlx executor —
6774 /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6775 /// escape hatch for tenant-scoped writes: schema-mode
6776 /// tenants share the registry pool but rely on a per-
6777 /// checkout `SET search_path`, so passing `&PgPool` would
6778 /// silently hit the wrong schema. Acquire a connection
6779 /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6780 ///
6781 /// # Errors
6782 /// As [`Self::save`].
6783 #[cfg(feature = "postgres")]
6784 pub async fn save_on #executor_generics (
6785 &mut self,
6786 #executor_param,
6787 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6788 #executor_where
6789 {
6790 // #1029 — INSERT writes exactly one row → 1; UPDATE returns
6791 // the rows-affected count (0 when the PK no longer exists).
6792 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6793 return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
6794 }
6795 #audit_update_pre
6796 let _query = #root::core::UpdateQuery::new(
6797 <Self as #root::core::Model>::SCHEMA,
6798 ::std::vec![ #( #assignments ),* ],
6799 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6800 #pk_column_lit,
6801 #root::core::Op::Eq,
6802 ::core::convert::Into::<#root::core::SqlValue>::into(
6803 ::core::clone::Clone::clone(&self.#pk_ident)
6804 ),
6805 )),
6806 );
6807 let _affected = #root::sql::__macro_internals::update_on(
6808 #executor_passes_to_data_write,
6809 &_query,
6810 ).await?;
6811 #audit_update_post
6812 ::core::result::Result::Ok(_affected)
6813 }
6814
6815 /// Per-call override for the audit source. Runs
6816 /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6817 /// scope so the resulting audit entry records `source`
6818 /// instead of the task-local default. Useful for seed
6819 /// scripts and one-off CLI tools that don't sit inside an
6820 /// admin handler. The override applies only to this call;
6821 /// no global state changes.
6822 ///
6823 /// # Errors
6824 /// As [`Self::save_on`].
6825 #[cfg(feature = "postgres")]
6826 pub async fn save_on_with #executor_generics (
6827 &mut self,
6828 #executor_param,
6829 source: #root::audit::AuditSource,
6830 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6831 #executor_where
6832 {
6833 #root::audit::with_source(source, self.save_on(_executor)).await
6834 }
6835
6836 /// Insert this row or update it in-place if the primary key already
6837 /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6838 ///
6839 /// With `Auto::Unset` PK the server assigns a new key and no conflict
6840 /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6841 /// inserted if absent or all non-PK columns are overwritten if present.
6842 ///
6843 /// # Errors
6844 /// As [`Self::insert_on`].
6845 #[cfg(feature = "postgres")]
6846 pub async fn upsert(
6847 &mut self,
6848 pool: &#root::sql::sqlx::PgPool,
6849 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6850 #pool_to_upsert_on
6851 }
6852
6853 /// Like [`Self::upsert`] but accepts any sqlx executor.
6854 /// See [`Self::save_on`] for tenancy-scoped rationale.
6855 ///
6856 /// # Errors
6857 /// As [`Self::upsert`].
6858 #[cfg(feature = "postgres")]
6859 pub async fn upsert_on #executor_generics (
6860 &mut self,
6861 #executor_param,
6862 ) -> ::core::result::Result<(), #root::sql::ExecError>
6863 #executor_where
6864 {
6865 let mut _columns: ::std::vec::Vec<&'static str> =
6866 ::std::vec::Vec::new();
6867 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6868 ::std::vec::Vec::new();
6869 #( #upsert_pushes )*
6870 let query = #root::core::InsertQuery::new(
6871 <Self as #root::core::Model>::SCHEMA,
6872 _columns,
6873 _values,
6874 )
6875 .returning(::std::vec![ #( #upsert_returning ),* ])
6876 .on_conflict(#conflict_clause);
6877 #audit_upsert_pre
6878 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
6879 #executor_passes_to_data_write,
6880 &query,
6881 ).await?;
6882 let _returning_row = &_returning_row_v;
6883 #( #upsert_auto_assigns )*
6884 #audit_upsert_emit
6885 ::core::result::Result::Ok(())
6886 }
6887 })
6888 } else {
6889 None
6890 };
6891
6892 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6893 let pk_column_lit = pk_column.as_str();
6894 // Optional `soft_delete_on` / `restore_on` companions when the
6895 // model has a `#[rustango(soft_delete)]` column. They land
6896 // alongside the regular `delete_on` so callers have both
6897 // options — a hard delete (audit-tracked as a real DELETE) and
6898 // a logical delete (audit-tracked as an UPDATE setting the
6899 // deleted_at column to NOW()).
6900 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6901 let col_lit = col;
6902 let sd_field_ident = fields
6903 .soft_delete_field_ident
6904 .clone()
6905 .expect("soft_delete_column without ident");
6906 quote! {
6907 /// Soft-delete this row by setting its
6908 /// `#[rustango(soft_delete)]` column to `NOW()`.
6909 /// The row stays in the table; query helpers can
6910 /// filter it out by checking the column for `IS NOT
6911 /// NULL`.
6912 ///
6913 /// # Errors
6914 /// As [`Self::delete`].
6915 #[cfg(feature = "postgres")]
6916 pub async fn soft_delete_on #executor_generics (
6917 &self,
6918 #executor_param,
6919 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6920 #executor_where
6921 {
6922 let _now = #root::__chrono::Utc::now();
6923 let _query = #root::soft_delete::__mark_query(
6924 <Self as #root::core::Model>::SCHEMA,
6925 #col_lit,
6926 #pk_column_lit,
6927 ::core::convert::Into::<#root::core::SqlValue>::into(
6928 ::core::clone::Clone::clone(&self.#pk_ident)
6929 ),
6930 ::core::option::Option::Some(_now),
6931 );
6932 let _affected = #root::sql::__macro_internals::update_on(
6933 #executor_passes_to_data_write,
6934 &_query,
6935 ).await?;
6936 #audit_softdelete_emit
6937 ::core::result::Result::Ok(_affected)
6938 }
6939
6940 /// Inverse of [`Self::soft_delete_on`] — clears the
6941 /// soft-delete column back to NULL so the row is
6942 /// considered live again.
6943 ///
6944 /// # Errors
6945 /// As [`Self::delete`].
6946 #[cfg(feature = "postgres")]
6947 pub async fn restore_on #executor_generics (
6948 &self,
6949 #executor_param,
6950 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6951 #executor_where
6952 {
6953 let _query = #root::soft_delete::__mark_query(
6954 <Self as #root::core::Model>::SCHEMA,
6955 #col_lit,
6956 #pk_column_lit,
6957 ::core::convert::Into::<#root::core::SqlValue>::into(
6958 ::core::clone::Clone::clone(&self.#pk_ident)
6959 ),
6960 ::core::option::Option::None,
6961 );
6962 let _affected = #root::sql::__macro_internals::update_on(
6963 #executor_passes_to_data_write,
6964 &_query,
6965 ).await?;
6966 #audit_restore_emit
6967 ::core::result::Result::Ok(_affected)
6968 }
6969
6970 /// Tri-dialect counterpart of [`Self::soft_delete_on`]
6971 /// — takes [`#root::sql::Pool`] and dispatches per
6972 /// backend. Eloquent `Model::delete()` semantics on
6973 /// soft-delete-enabled models (closes #821 partial).
6974 ///
6975 /// Sets the `#[rustango(soft_delete)]` column to
6976 /// `NOW()` on every backend. Query helpers
6977 /// (`QuerySet::active()` / `only_trashed()`,
6978 /// `soft_delete::active_filter` /
6979 /// `compose_with_active`) filter trashed rows out by
6980 /// reading `IS NULL` on the same column.
6981 ///
6982 /// # Errors
6983 /// As [`#root::sql::update_pool`].
6984 pub async fn soft_delete(
6985 &self,
6986 pool: &#root::sql::Pool,
6987 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6988 let _now = #root::__chrono::Utc::now();
6989 let _query = #root::soft_delete::__mark_query(
6990 <Self as #root::core::Model>::SCHEMA,
6991 #col_lit,
6992 #pk_column_lit,
6993 ::core::convert::Into::<#root::core::SqlValue>::into(
6994 ::core::clone::Clone::clone(&self.#pk_ident)
6995 ),
6996 ::core::option::Option::Some(_now),
6997 );
6998 #pool_softdelete_run
6999 }
7000
7001 /// Tri-dialect counterpart of [`Self::restore_on`].
7002 /// Clears the `#[rustango(soft_delete)]` column back
7003 /// to `NULL`, marking the row live again. Eloquent
7004 /// `Model::restore()` parity.
7005 ///
7006 /// # Errors
7007 /// As [`#root::sql::update_pool`].
7008 pub async fn restore(
7009 &self,
7010 pool: &#root::sql::Pool,
7011 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7012 let _query = #root::soft_delete::__mark_query(
7013 <Self as #root::core::Model>::SCHEMA,
7014 #col_lit,
7015 #pk_column_lit,
7016 ::core::convert::Into::<#root::core::SqlValue>::into(
7017 ::core::clone::Clone::clone(&self.#pk_ident)
7018 ),
7019 ::core::option::Option::None,
7020 );
7021 #pool_restore_run
7022 }
7023
7024 /// Hard-delete this row, ignoring the soft-delete
7025 /// column. Eloquent `Model::forceDelete()` parity —
7026 /// the escape hatch when you need to actually purge
7027 /// data (GDPR, fixture cleanup, etc.).
7028 ///
7029 /// Equivalent to [`Self::delete_pool`] (the framework's
7030 /// non-soft delete) — exposed under the Eloquent name
7031 /// for muscle-memory + so soft-delete-enabled models
7032 /// have all three operations (soft / restore / force)
7033 /// in one place.
7034 ///
7035 /// # Errors
7036 /// As [`Self::delete_pool`].
7037 pub async fn force_delete(
7038 &self,
7039 pool: &#root::sql::Pool,
7040 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7041 Self::delete_pool(self, pool).await
7042 }
7043
7044 /// Returns `true` when this row is soft-deleted (its
7045 /// `#[rustango(soft_delete)]` column is currently
7046 /// set — Eloquent `$model->trashed()` parity).
7047 ///
7048 /// Pure in-memory predicate over `&self`; does not
7049 /// hit the database. Useful in admin/template code
7050 /// like `{% if post.trashed() %}…{% endif %}` and in
7051 /// guard clauses on restore/force-delete flows.
7052 pub fn trashed(&self) -> bool {
7053 ::core::option::Option::is_some(&self.#sd_field_ident)
7054 }
7055
7056 /// Fetch every row whose `#[rustango(soft_delete)]`
7057 /// column is `NULL` (a.k.a. the live, non-trashed
7058 /// rows). Eloquent's default `Model::all()` behavior on
7059 /// a soft-delete model (Eloquent auto-scopes trashed
7060 /// rows out; rustango doesn't have auto-scoping yet —
7061 /// see issue #820 — so this is the explicit shortcut).
7062 ///
7063 /// One-liner over `QuerySet::<Self>::default()
7064 /// .active().fetch(pool)`. Closes #821 partial.
7065 ///
7066 /// # Errors
7067 /// As [`#root::sql::FetcherPool::fetch`].
7068 pub async fn active(
7069 pool: &#root::sql::Pool,
7070 ) -> ::core::result::Result<
7071 ::std::vec::Vec<Self>,
7072 #root::sql::ExecError,
7073 > {
7074 use #root::sql::FetcherPool as _;
7075 #root::query::QuerySet::<Self>::default()
7076 .active()
7077 .fetch(pool)
7078 .await
7079 }
7080
7081 /// Fetch ONLY soft-deleted rows. Eloquent
7082 /// `Model::onlyTrashed()->get()` parity — drives the
7083 /// admin "Trash" page, restore flows, GDPR purge
7084 /// scans, etc.
7085 ///
7086 /// One-liner over `QuerySet::<Self>::default()
7087 /// .only_trashed().fetch(pool)`. Closes #821
7088 /// partial.
7089 ///
7090 /// # Errors
7091 /// As [`#root::sql::FetcherPool::fetch`].
7092 pub async fn only_trashed(
7093 pool: &#root::sql::Pool,
7094 ) -> ::core::result::Result<
7095 ::std::vec::Vec<Self>,
7096 #root::sql::ExecError,
7097 > {
7098 use #root::sql::FetcherPool as _;
7099 #root::query::QuerySet::<Self>::default()
7100 .only_trashed()
7101 .fetch(pool)
7102 .await
7103 }
7104
7105 /// Fetch every row, both live and soft-deleted.
7106 /// Eloquent `Model::withTrashed()->get()` parity.
7107 ///
7108 /// Today every queryset already includes trashed rows
7109 /// (rustango has no global-scope tracking yet — issue
7110 /// #820), so this is functionally equivalent to
7111 /// [`Self::all`]. Exposed as a named shortcut so
7112 /// soft-delete-aware code reads `Model::with_trashed_pool`
7113 /// rather than `Model::all_pool` — keeps intent visible
7114 /// in callers and stays correct when auto-scoping lands.
7115 ///
7116 /// Closes #821 partial.
7117 ///
7118 /// # Errors
7119 /// As [`#root::sql::FetcherPool::fetch`].
7120 pub async fn with_trashed(
7121 pool: &#root::sql::Pool,
7122 ) -> ::core::result::Result<
7123 ::std::vec::Vec<Self>,
7124 #root::sql::ExecError,
7125 > {
7126 use #root::sql::FetcherPool as _;
7127 #root::query::QuerySet::<Self>::default()
7128 .with_trashed()
7129 .fetch(pool)
7130 .await
7131 }
7132
7133 }
7134 } else {
7135 quote!()
7136 };
7137 quote! {
7138 /// Delete the row identified by this instance's primary key.
7139 ///
7140 /// Returns the number of rows affected (0 or 1).
7141 ///
7142 /// **Postgres only — prefer [`Self::delete_pool`].** This takes
7143 /// a driver-specific `PgPool`, so it does not exist on a build
7144 /// without the `postgres` feature. [`Self::delete_pool`] takes
7145 /// `rustango::sql::Pool` and works on all three. The naming is
7146 /// inverted; tracked in #1293.
7147 ///
7148 /// # Errors
7149 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7150 /// driver failures.
7151 #[cfg(feature = "postgres")]
7152 pub async fn delete(
7153 &self,
7154 pool: &#root::sql::sqlx::PgPool,
7155 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7156 #pool_to_delete_on
7157 }
7158
7159 /// Like [`Self::delete`] but accepts any sqlx executor —
7160 /// for tenant-scoped deletes against an explicitly-acquired
7161 /// connection. See [`Self::save_on`] for the rationale.
7162 ///
7163 /// # Errors
7164 /// As [`Self::delete`].
7165 #[cfg(feature = "postgres")]
7166 pub async fn delete_on #executor_generics (
7167 &self,
7168 #executor_param,
7169 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7170 #executor_where
7171 {
7172 let query = #root::core::DeleteQuery::by_pk(
7173 <Self as #root::core::Model>::SCHEMA,
7174 #pk_column_lit,
7175 ::core::convert::Into::<#root::core::SqlValue>::into(
7176 ::core::clone::Clone::clone(&self.#pk_ident)
7177 ),
7178 );
7179 let _affected = #root::sql::__macro_internals::delete_on(
7180 #executor_passes_to_data_write,
7181 &query,
7182 ).await?;
7183 #audit_delete_emit
7184 ::core::result::Result::Ok(_affected)
7185 }
7186
7187 /// Per-call audit-source override for [`Self::delete_on`].
7188 /// See [`Self::save_on_with`] for shape rationale.
7189 ///
7190 /// # Errors
7191 /// As [`Self::delete_on`].
7192 #[cfg(feature = "postgres")]
7193 pub async fn delete_on_with #executor_generics (
7194 &self,
7195 #executor_param,
7196 source: #root::audit::AuditSource,
7197 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7198 #executor_where
7199 {
7200 #root::audit::with_source(source, self.delete_on(_executor)).await
7201 }
7202 #pool_delete_method
7203 #pool_insert_method
7204 #audit_entry_method
7205 #pool_save_method
7206 #refresh_replicate_methods
7207 #tx_delete_method
7208 #tx_insert_method
7209 #tx_save_method
7210 #soft_delete_methods
7211
7212 /// Returns `true` when `other` represents the same DB
7213 /// row as `self` — i.e. their primary keys compare
7214 /// equal. Eloquent `$model->is($other)` parity.
7215 ///
7216 /// Because both arguments are typed `&Self`, the
7217 /// model/table check is automatic — `Post::is` cannot
7218 /// be invoked against a `Comment` at compile time. Only
7219 /// the PK has to be compared at runtime.
7220 pub fn is(&self, other: &Self) -> bool {
7221 self.#pk_ident == other.#pk_ident
7222 }
7223
7224 /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7225 /// parity.
7226 pub fn is_not(&self, other: &Self) -> bool {
7227 self.#pk_ident != other.#pk_ident
7228 }
7229
7230 /// Returns this row's primary-key value as an
7231 /// [`#root::core::SqlValue`]. Eloquent
7232 /// `$model->getKey()` parity.
7233 ///
7234 /// Useful when you need to thread the PK through a
7235 /// generic `Into<SqlValue>`-bound API without knowing the
7236 /// concrete PK type (`i64` vs `Uuid` vs `String`).
7237 #[must_use]
7238 pub fn get_key(&self) -> #root::core::SqlValue {
7239 ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7240 }
7241
7242 }
7243 });
7244
7245 let insert_method = if fields.has_auto {
7246 let pushes = &fields.insert_pushes;
7247 let returning_cols = &fields.returning_cols;
7248 let auto_assigns = &fields.auto_assigns;
7249 quote! {
7250 /// Insert this row into its table. Skips columns whose
7251 /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7252 /// sequence fills them in, then reads each `Auto` column
7253 /// back via `RETURNING` and stores it on `self`.
7254 ///
7255 /// **Postgres only — prefer [`Self::insert_pool`].** This takes
7256 /// a driver-specific `PgPool`, so it does not exist on a build
7257 /// without the `postgres` feature. [`Self::insert_pool`] takes
7258 /// `rustango::sql::Pool` and works on all three. The naming is
7259 /// inverted; tracked in #1293.
7260 ///
7261 /// # Errors
7262 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7263 /// driver failures.
7264 #[cfg(feature = "postgres")]
7265 pub async fn insert(
7266 &mut self,
7267 pool: &#root::sql::sqlx::PgPool,
7268 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7269 #pool_to_insert_on
7270 }
7271
7272 /// Like [`Self::insert`] but accepts any sqlx executor.
7273 /// See [`Self::save_on`] for tenancy-scoped rationale.
7274 ///
7275 /// # Errors
7276 /// As [`Self::insert`].
7277 #[cfg(feature = "postgres")]
7278 pub async fn insert_on #executor_generics (
7279 &mut self,
7280 #executor_param,
7281 ) -> ::core::result::Result<(), #root::sql::ExecError>
7282 #executor_where
7283 {
7284 let mut _columns: ::std::vec::Vec<&'static str> =
7285 ::std::vec::Vec::new();
7286 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7287 ::std::vec::Vec::new();
7288 #( #pushes )*
7289 let query = #root::core::InsertQuery::new(
7290 <Self as #root::core::Model>::SCHEMA,
7291 _columns,
7292 _values,
7293 )
7294 .returning(::std::vec![ #( #returning_cols ),* ]);
7295 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7296 #executor_passes_to_data_write,
7297 &query,
7298 ).await?;
7299 let _returning_row = &_returning_row_v;
7300 #( #auto_assigns )*
7301 #audit_insert_emit
7302 ::core::result::Result::Ok(())
7303 }
7304
7305 /// Per-call audit-source override for [`Self::insert_on`].
7306 /// See [`Self::save_on_with`] for shape rationale.
7307 ///
7308 /// # Errors
7309 /// As [`Self::insert_on`].
7310 #[cfg(feature = "postgres")]
7311 pub async fn insert_on_with #executor_generics (
7312 &mut self,
7313 #executor_param,
7314 source: #root::audit::AuditSource,
7315 ) -> ::core::result::Result<(), #root::sql::ExecError>
7316 #executor_where
7317 {
7318 #root::audit::with_source(source, self.insert_on(_executor)).await
7319 }
7320 }
7321 } else {
7322 let insert_columns = &fields.insert_columns;
7323 let insert_values = &fields.insert_values;
7324 quote! {
7325 /// Insert this row into its table.
7326 ///
7327 /// # Errors
7328 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7329 /// driver failures.
7330 #[cfg(feature = "postgres")]
7331 pub async fn insert(
7332 &self,
7333 pool: &#root::sql::sqlx::PgPool,
7334 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7335 self.insert_on(pool).await
7336 }
7337
7338 /// Like [`Self::insert`] but accepts any sqlx executor.
7339 /// See [`Self::save_on`] for tenancy-scoped rationale.
7340 ///
7341 /// # Errors
7342 /// As [`Self::insert`].
7343 #[cfg(feature = "postgres")]
7344 pub async fn insert_on<'_c, _E>(
7345 &self,
7346 _executor: _E,
7347 ) -> ::core::result::Result<(), #root::sql::ExecError>
7348 where
7349 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7350 {
7351 let query = #root::core::InsertQuery::new(
7352 <Self as #root::core::Model>::SCHEMA,
7353 ::std::vec![ #( #insert_columns ),* ],
7354 ::std::vec![ #( #insert_values ),* ],
7355 );
7356 #root::sql::__macro_internals::insert_on(_executor, &query).await
7357 }
7358 }
7359 };
7360
7361 let bulk_insert_method = if fields.has_auto {
7362 let cols_no_auto = &fields.bulk_columns_no_auto;
7363 let cols_all = &fields.bulk_columns_all;
7364 let pushes_no_auto = &fields.bulk_pushes_no_auto;
7365 let pushes_all = &fields.bulk_pushes_all;
7366 let returning_cols = &fields.returning_cols;
7367 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7368 let uniformity = &fields.bulk_auto_uniformity;
7369 let fill_in_place = &fields.bulk_fill_in_place;
7370 // Nothing to read back when every Auto field is filled Rust-side (#1934).
7371 let read_back = if fields.returning_cols.is_empty() {
7372 quote!()
7373 } else {
7374 quote! {
7375 if _returned.len() != rows.len() {
7376 return ::core::result::Result::Err(
7377 #root::sql::ExecError::Sql(
7378 #root::sql::SqlError::BulkInsertReturningMismatch {
7379 expected: rows.len(),
7380 actual: _returned.len(),
7381 }
7382 )
7383 );
7384 }
7385 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7386 #auto_assigns_for_row
7387 }
7388 }
7389 };
7390 // The DB-filled `Auto` picks the path; Rust-filled ones fill per row (#1950).
7391 let first_auto_ident = fields
7392 .first_db_auto
7393 .as_ref()
7394 .map(|(ident, _)| ident)
7395 .or(fields.first_auto_ident.as_ref())
7396 .expect("has_auto implies first_auto_ident is Some");
7397 quote! {
7398 /// Bulk-insert `rows` in a single round-trip. Every row's
7399 /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7400 /// (sequence fills them in) or uniformly `Auto::Set(_)`
7401 /// (caller-supplied values). Mixed Set/Unset is rejected
7402 /// — call `insert` per row for that case.
7403 ///
7404 /// Empty slice is a no-op. Each row's `Auto` fields are
7405 /// populated from the `RETURNING` clause in input order
7406 /// before this returns.
7407 ///
7408 /// # Errors
7409 /// Returns [`#root::sql::ExecError`] for validation,
7410 /// SQL-writing, mixed-Auto rejection, or driver failures.
7411 #[cfg(feature = "postgres")]
7412 pub async fn bulk_insert(
7413 rows: &mut [Self],
7414 pool: &#root::sql::sqlx::PgPool,
7415 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7416 #pool_to_bulk_insert_on
7417 }
7418
7419 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7420 /// See [`Self::save_on`] for tenancy-scoped rationale.
7421 ///
7422 /// # Errors
7423 /// As [`Self::bulk_insert`].
7424 #[cfg(feature = "postgres")]
7425 pub async fn bulk_insert_on #executor_generics (
7426 rows: &mut [Self],
7427 #executor_param,
7428 ) -> ::core::result::Result<(), #root::sql::ExecError>
7429 #executor_where
7430 {
7431 if rows.is_empty() {
7432 return ::core::result::Result::Ok(());
7433 }
7434 let _first_unset = matches!(
7435 rows[0].#first_auto_ident,
7436 #root::sql::Auto::Unset
7437 );
7438 #( #uniformity )*
7439 for _row in rows.iter_mut() {
7440 #( #fill_in_place )*
7441 }
7442
7443 let mut _all_rows: ::std::vec::Vec<
7444 ::std::vec::Vec<#root::core::SqlValue>,
7445 > = ::std::vec::Vec::with_capacity(rows.len());
7446 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7447 for _row in rows.iter() {
7448 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7449 ::std::vec::Vec::new();
7450 #( #pushes_no_auto )*
7451 _all_rows.push(_row_vals);
7452 }
7453 ::std::vec![ #( #cols_no_auto ),* ]
7454 } else {
7455 for _row in rows.iter() {
7456 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7457 ::std::vec::Vec::new();
7458 #( #pushes_all )*
7459 _all_rows.push(_row_vals);
7460 }
7461 ::std::vec![ #( #cols_all ),* ]
7462 };
7463
7464 let _query = #root::core::BulkInsertQuery::new(
7465 <Self as #root::core::Model>::SCHEMA,
7466 _columns,
7467 _all_rows,
7468 )
7469 .returning(::std::vec![ #( #returning_cols ),* ]);
7470 let _returned = #root::sql::__macro_internals::bulk_insert_on(
7471 #executor_passes_to_data_write,
7472 &_query,
7473 ).await?;
7474 #read_back
7475 #audit_bulk_insert_emit
7476 ::core::result::Result::Ok(())
7477 }
7478 }
7479 } else {
7480 let cols_all = &fields.bulk_columns_all;
7481 let pushes_all = &fields.bulk_pushes_all;
7482 quote! {
7483 /// Bulk-insert `rows` in a single round-trip. Every row's
7484 /// fields are written verbatim — there are no `Auto<T>`
7485 /// fields on this model.
7486 ///
7487 /// Empty slice is a no-op.
7488 ///
7489 /// # Errors
7490 /// Returns [`#root::sql::ExecError`] for validation,
7491 /// SQL-writing, or driver failures.
7492 #[cfg(feature = "postgres")]
7493 pub async fn bulk_insert(
7494 rows: &[Self],
7495 pool: &#root::sql::sqlx::PgPool,
7496 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7497 #pool_to_bulk_insert_on
7498 }
7499
7500 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7501 /// See [`Self::save_on`] for tenancy-scoped rationale.
7502 ///
7503 /// # Errors
7504 /// As [`Self::bulk_insert`].
7505 #[cfg(feature = "postgres")]
7506 pub async fn bulk_insert_on #executor_generics (
7507 rows: &[Self],
7508 #executor_param,
7509 ) -> ::core::result::Result<(), #root::sql::ExecError>
7510 #executor_where
7511 {
7512 if rows.is_empty() {
7513 return ::core::result::Result::Ok(());
7514 }
7515 let mut _all_rows: ::std::vec::Vec<
7516 ::std::vec::Vec<#root::core::SqlValue>,
7517 > = ::std::vec::Vec::with_capacity(rows.len());
7518 for _row in rows.iter() {
7519 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7520 ::std::vec::Vec::new();
7521 #( #pushes_all )*
7522 _all_rows.push(_row_vals);
7523 }
7524 let _query = #root::core::BulkInsertQuery::new(
7525 <Self as #root::core::Model>::SCHEMA,
7526 ::std::vec![ #( #cols_all ),* ],
7527 _all_rows,
7528 );
7529 let _ = #root::sql::__macro_internals::bulk_insert_on(
7530 #executor_passes_to_data_write,
7531 &_query,
7532 ).await?;
7533 #audit_bulk_insert_emit
7534 ::core::result::Result::Ok(())
7535 }
7536 }
7537 };
7538
7539 // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7540 // (no postgres-feature gate); routes through the existing
7541 // `bulk_insert_pool` + per-dialect conflict writer.
7542 //
7543 // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7544 // sequence picks the PK for fresh inserts; the UPDATE branch never
7545 // touches the Auto column.
7546 // No RETURNING on MySQL, so which rows landed is unknown: refuse on
7547 // audited models rather than write unaudited (#1747).
7548 let refuse_unaudited_bulk_insert = quote! {
7549 if <Self as #root::core::Model>::SCHEMA.audit_track.is_some() {
7550 return ::core::result::Result::Err(#root::sql::ExecError::AuditUnsupported {
7551 table: <Self as #root::core::Model>::SCHEMA.table,
7552 reason: "conflict-handling bulk inserts cannot tell which rows landed",
7553 });
7554 }
7555 };
7556 let bulk_upsert_pool_method = {
7557 // Pick the "no Auto" columns when the model has Auto fields,
7558 // else every column.
7559 let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7560 (
7561 fields.bulk_columns_no_auto.clone(),
7562 fields.bulk_pushes_no_auto.clone(),
7563 )
7564 } else {
7565 (
7566 fields.bulk_columns_all.clone(),
7567 fields.bulk_pushes_all.clone(),
7568 )
7569 };
7570 quote! {
7571 /// Tri-dialect bulk insert that updates on conflict — the
7572 /// "import a batch idempotently" shape. Issue #267 / T1.5.
7573 ///
7574 /// Per-row values are extracted and lowered into a
7575 /// [`#root::core::BulkInsertQuery`] with
7576 /// `on_conflict = DoUpdate { target, update_columns }`. The
7577 /// writer dispatches per-dialect:
7578 /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7579 /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)` (target ignored — MySQL matches every UNIQUE index)
7580 ///
7581 /// `target` names the column(s) whose unique constraint
7582 /// defines the conflict (typically a `unique` or
7583 /// `unique_together` natural-key column, NOT the `Auto<T>`
7584 /// PK). `update_cols` names the columns to overwrite on
7585 /// conflict — every other column is left untouched on the
7586 /// existing row.
7587 ///
7588 /// Auto-PK rows must all have `Auto::Unset` (the sequence
7589 /// picks the PK on insert; the update path never touches
7590 /// the Auto column). Auto-set rows trigger a hard error.
7591 /// Empty slice is a no-op.
7592 ///
7593 /// # Errors
7594 /// Returns [`#root::sql::ExecError`] for validation,
7595 /// SQL-writing, or driver failures.
7596 pub async fn bulk_upsert_pool(
7597 rows: &[Self],
7598 target: &[&'static str],
7599 update_cols: &[&'static str],
7600 pool: &#root::sql::Pool,
7601 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7602 #refuse_unaudited_bulk_insert
7603 if rows.is_empty() {
7604 return ::core::result::Result::Ok(());
7605 }
7606 let mut _all_rows: ::std::vec::Vec<
7607 ::std::vec::Vec<#root::core::SqlValue>,
7608 > = ::std::vec::Vec::with_capacity(rows.len());
7609 for _row in rows.iter() {
7610 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7611 ::std::vec::Vec::new();
7612 #( #upsert_pushes )*
7613 _all_rows.push(_row_vals);
7614 }
7615 let _query = #root::core::BulkInsertQuery::new(
7616 <Self as #root::core::Model>::SCHEMA,
7617 ::std::vec![ #( #upsert_cols ),* ],
7618 _all_rows,
7619 )
7620 .on_conflict_do_update(target, update_cols);
7621 #root::sql::bulk_insert_pool(pool, &_query).await
7622 }
7623
7624 /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7625 /// skip rows that would violate a unique constraint. Issue
7626 /// #267 / T1.5. Same per-dialect dispatch as
7627 /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7628 /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7629 /// <pivot> = <pivot>` (MySQL no-op write).
7630 ///
7631 /// # Errors
7632 /// As [`Self::bulk_upsert_pool`].
7633 pub async fn bulk_insert_or_ignore_pool(
7634 rows: &[Self],
7635 pool: &#root::sql::Pool,
7636 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7637 #refuse_unaudited_bulk_insert
7638 if rows.is_empty() {
7639 return ::core::result::Result::Ok(());
7640 }
7641 let mut _all_rows: ::std::vec::Vec<
7642 ::std::vec::Vec<#root::core::SqlValue>,
7643 > = ::std::vec::Vec::with_capacity(rows.len());
7644 for _row in rows.iter() {
7645 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7646 ::std::vec::Vec::new();
7647 #( #upsert_pushes )*
7648 _all_rows.push(_row_vals);
7649 }
7650 let _query = #root::core::BulkInsertQuery::new(
7651 <Self as #root::core::Model>::SCHEMA,
7652 ::std::vec![ #( #upsert_cols ),* ],
7653 _all_rows,
7654 )
7655 .on_conflict_do_nothing();
7656 #root::sql::bulk_insert_pool(pool, &_query).await
7657 }
7658 }
7659 };
7660
7661 // Ergonomic `Model::bulk_update(objs, fields)`.
7662 // The SQL/IR/executor stack
7663 // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7664 // `write_bulk_update_*` writers) already existed; what was missing
7665 // was the per-model constructor that maps `&[Self]` + a runtime
7666 // column list into rows of `[pk, col_vals…]` so callers don't
7667 // hand-build the IR. Emitted only when the model has a primary key
7668 // (the PK is the join key and can't itself be updated).
7669 let bulk_update_rows_run = if audited_fields.is_some() {
7670 quote!(#root::audit::bulk_update_with_audit::<Self>(pool, &_query, |_r: &Self| {
7671 _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
7672 })
7673 .await)
7674 } else {
7675 quote!(#root::sql::bulk_update_pool(pool, &_query).await)
7676 };
7677 let bulk_update_method = match &fields.primary_key {
7678 None => quote! {},
7679 Some((pk_ident, pk_col)) => {
7680 // One pair of match arms per non-PK column: a validation arm
7681 // resolving the runtime name to its `&'static str` column,
7682 // and a value arm pushing that field off the row.
7683 let mut col_arms: Vec<TokenStream2> = Vec::new();
7684 let mut val_arms: Vec<TokenStream2> = Vec::new();
7685 for entry in &fields.column_entries {
7686 if &entry.column == pk_col {
7687 continue;
7688 }
7689 let col = &entry.column;
7690 let ident = &entry.ident;
7691 col_arms.push(quote! { #col => #col, });
7692 val_arms.push(quote! {
7693 #col => _row_vals.push(
7694 ::core::convert::Into::<#root::core::SqlValue>::into(
7695 ::core::clone::Clone::clone(&_o.#ident)
7696 )
7697 ),
7698 });
7699 }
7700 quote! {
7701 /// Write per-row-different values for the named `fields`
7702 /// across every object in `objs` in a single statement,
7703 /// matched by primary key.
7704 ///
7705 /// `fields` names the **columns** to update. The primary
7706 /// key identifies each row and cannot itself be updated
7707 /// (pass it and you get
7708 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7709 /// Empty `objs` or `fields` is a no-op returning `0`.
7710 /// Objects whose PK matches no row are simply not updated.
7711 /// Returns the number of rows affected.
7712 ///
7713 /// Tri-dialect: lowers to one
7714 /// [`#root::core::BulkUpdateQuery`] and dispatches
7715 /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7716 /// a CTE + correlated subquery on SQLite, an inner
7717 /// `JOIN (VALUES …)` on MySQL.
7718 ///
7719 /// # Errors
7720 /// [`#root::core::QueryError::UnknownField`] for a name
7721 /// that isn't a column on this model,
7722 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7723 /// `fields` names the PK, or [`#root::sql::ExecError`] for
7724 /// SQL-writing / driver failures.
7725 pub async fn bulk_update(
7726 objs: &[Self],
7727 fields: &[&str],
7728 pool: &#root::sql::Pool,
7729 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7730 if objs.is_empty() || fields.is_empty() {
7731 return ::core::result::Result::Ok(0);
7732 }
7733 let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7734 let mut _update_columns: ::std::vec::Vec<&'static str> =
7735 ::std::vec::Vec::with_capacity(fields.len());
7736 for &_f in fields {
7737 let _col: &'static str = match _f {
7738 #pk_col => {
7739 return ::core::result::Result::Err(
7740 ::core::convert::Into::into(
7741 #root::core::QueryError::BulkUpdatePrimaryKey {
7742 model: _model_name,
7743 field: ::std::string::ToString::to_string(_f),
7744 }
7745 )
7746 );
7747 }
7748 #( #col_arms )*
7749 _ => {
7750 return ::core::result::Result::Err(
7751 ::core::convert::Into::into(
7752 #root::core::QueryError::UnknownField {
7753 model: _model_name,
7754 field: ::std::string::ToString::to_string(_f),
7755 }
7756 )
7757 );
7758 }
7759 };
7760 // A model whose only field is the primary key emits no
7761 // non-PK arms, so every arm above diverges and this push
7762 // is genuinely unreachable — correct, since such a model
7763 // has nothing to bulk-update. Scoped to the statement
7764 // rather than the fn so a real unreachability elsewhere
7765 // in this body still warns. Without it every PK-only
7766 // model warns in the *user's* crate, which they cannot
7767 // act on (#1290).
7768 #[allow(unreachable_code)]
7769 _update_columns.push(_col);
7770 }
7771 let mut _rows: ::std::vec::Vec<
7772 ::std::vec::Vec<#root::core::SqlValue>,
7773 > = ::std::vec::Vec::with_capacity(objs.len());
7774 for _o in objs.iter() {
7775 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7776 ::std::vec::Vec::with_capacity(fields.len() + 1);
7777 // PK first — the writers expect `[pk, …update cols]`.
7778 _row_vals.push(
7779 ::core::convert::Into::<#root::core::SqlValue>::into(
7780 ::core::clone::Clone::clone(&_o.#pk_ident)
7781 )
7782 );
7783 for &_f in fields {
7784 match _f {
7785 #( #val_arms )*
7786 // Unreachable: every name was validated
7787 // against the same arm set above.
7788 _ => {}
7789 }
7790 }
7791 _rows.push(_row_vals);
7792 }
7793 let _query = #root::core::BulkUpdateQuery::new(
7794 <Self as #root::core::Model>::SCHEMA,
7795 _update_columns,
7796 _rows,
7797 );
7798 #bulk_update_rows_run
7799 }
7800 }
7801 }
7802 };
7803
7804 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7805 quote! {
7806 /// Hidden runtime accessor for the primary-key value as a
7807 /// [`SqlValue`]. Used by reverse-relation helpers
7808 /// (`<parent>::<child>_set`) emitted from sibling models'
7809 /// FK fields. Not part of the public API.
7810 #[doc(hidden)]
7811 pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7812 ::core::convert::Into::<#root::core::SqlValue>::into(
7813 ::core::clone::Clone::clone(&self.#pk_ident)
7814 )
7815 }
7816 }
7817 });
7818
7819 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7820 quote! {
7821 impl #root::sql::HasPkValue for #struct_name {
7822 fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7823 ::core::convert::Into::<#root::core::SqlValue>::into(
7824 ::core::clone::Clone::clone(&self.#pk_ident)
7825 )
7826 }
7827 }
7828 }
7829 });
7830
7831 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7832
7833 // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7834 // dispatch to the right per-backend body without the macro emitting
7835 // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7836 // so audited models with non-Auto PKs (which still go through
7837 // `insert_one_with_audit` → `apply_auto_pk`) link.
7838 let assign_auto_pk_pool_impl = {
7839 let auto_assigns = &fields.auto_assigns;
7840 // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7841 // so the body is structurally identical to `auto_assigns` —
7842 // only the helper name swaps from `try_get_returning` to
7843 // `try_get_returning_sqlite` so the closure typechecks against
7844 // a `SqliteRow` instead of a `PgRow`.
7845 let auto_assigns_sqlite: Vec<TokenStream2> = fields
7846 .auto_field_idents
7847 .iter()
7848 .map(|(ident, column)| {
7849 quote! {
7850 self.#ident = #root::sql::try_get_returning_sqlite(
7851 _returning_row, #column
7852 )?;
7853 }
7854 })
7855 .collect();
7856 // #1028 — decode each `generated_as` column from the same
7857 // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7858 // own type drives the decode (no `Auto<T>` wrapper).
7859 let generated_assigns: Vec<TokenStream2> = fields
7860 .generated_field_idents
7861 .iter()
7862 .map(|(ident, column)| {
7863 quote! {
7864 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7865 }
7866 })
7867 .collect();
7868 let generated_assigns_sqlite: Vec<TokenStream2> = fields
7869 .generated_field_idents
7870 .iter()
7871 .map(|(ident, column)| {
7872 quote! {
7873 self.#ident = #root::sql::try_get_returning_sqlite(
7874 _returning_row, #column
7875 )?;
7876 }
7877 })
7878 .collect();
7879 let mysql_body = if let Some((first, value_ty)) = fields.first_db_auto.as_ref() {
7880 // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7881 // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7882 // Auto<Uuid>/etc. fail to link against MySQL (intended —
7883 // those models can't use AUTO_INCREMENT). The trait is only
7884 // touched on the MySQL arm at runtime, so PG-only consumers
7885 // never see the bound failure.
7886 //
7887 // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7888 // Auto<i64> PK + auto_now_add timestamp) errored hard at
7889 // runtime with "multi-column RETURNING". MySQL has no
7890 // multi-column RETURNING semantic and a follow-up SELECT
7891 // would need cross-trait plumbing. Pragmatic shape: succeed
7892 // with the FIRST Auto field populated from LAST_INSERT_ID();
7893 // any other Auto fields stay `Auto::Unset`. Callers that
7894 // need the DB-defaulted timestamp / UUID can re-fetch the
7895 // row by PK after `save_pool`. Fixes the cookbook chapter
7896 // 12 dialect divergence.
7897 quote! {
7898 let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7899 ::rustango_from_mysql_auto_id(_id)?;
7900 self.#first = #root::sql::Auto::Set(_converted);
7901 ::core::result::Result::Ok(())
7902 }
7903 } else {
7904 quote! {
7905 let _ = _id;
7906 ::core::result::Result::Ok(())
7907 }
7908 };
7909 quote! {
7910 impl #root::sql::AssignAutoPkPool for #struct_name {
7911 fn __rustango_assign_from_pg_row(
7912 &mut self,
7913 _returning_row: &#root::sql::PgReturningRow,
7914 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7915 #( #auto_assigns )*
7916 #( #generated_assigns )*
7917 ::core::result::Result::Ok(())
7918 }
7919 fn __rustango_assign_from_mysql_id(
7920 &mut self,
7921 _id: i64,
7922 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7923 #mysql_body
7924 }
7925 fn __rustango_assign_from_sqlite_row(
7926 &mut self,
7927 _returning_row: &#root::sql::SqliteReturningRow,
7928 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7929 #( #auto_assigns_sqlite )*
7930 #( #generated_assigns_sqlite )*
7931 ::core::result::Result::Ok(())
7932 }
7933 }
7934 }
7935 };
7936
7937 let from_aliased_row_inits = &fields.from_aliased_row_inits;
7938 let aliased_row_helper = quote! {
7939 /// Decode a row's aliased target columns (produced by
7940 /// `select_related`'s LEFT JOIN) into a fresh instance of
7941 /// this model. Reads each column via
7942 /// `format!("{prefix}__{col}")`, matching the alias the
7943 /// SELECT writer emitted. Slice 9.0d.
7944 #[doc(hidden)]
7945 #[cfg(feature = "postgres")]
7946 pub fn __rustango_from_aliased_row(
7947 row: &#root::sql::sqlx::postgres::PgRow,
7948 prefix: &str,
7949 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7950 ::core::result::Result::Ok(Self {
7951 #( #from_aliased_row_inits ),*
7952 })
7953 }
7954 };
7955 // v0.23.0-batch8 — MySQL counterpart, gated through the
7956 // cfg-aware macro_rules so PG-only builds expand to nothing.
7957 let from_aliased_row_inits_my = &fields.from_aliased_row_inits_my;
7958 let aliased_row_helper_my = quote! {
7959 #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7960 #( #from_aliased_row_inits_my ),*
7961 });
7962 };
7963
7964 // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7965 // pattern + cfg gate on the `sqlite` feature.
7966 let aliased_row_helper_sqlite = quote! {
7967 #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7968 #( #from_aliased_row_inits ),*
7969 });
7970 };
7971
7972 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7973 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7974 let load_related_impl_sqlite =
7975 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7976
7977 // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7978 // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7979 // default `Self::objects()`. Each accessor returns a fresh
7980 // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7981 // methods the user defined.
7982 let extra_manager_fns: Vec<TokenStream2> = manager_fns
7983 .iter()
7984 .map(|fn_ident| {
7985 let model_name_str = struct_name.to_string();
7986 let fn_name_str = fn_ident.to_string();
7987 let doc = format!(
7988 "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7989 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7990 equivalent to `Self::objects()`. Chains with any \
7991 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7992 extension methods."
7993 );
7994 quote! {
7995 #[doc = #doc]
7996 #[must_use]
7997 pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7998 #root::query::QuerySet::new()
7999 }
8000 }
8001 })
8002 .collect();
8003
8004 quote! {
8005 impl #struct_name {
8006 /// Start a new `QuerySet` over this model.
8007 #[must_use]
8008 pub fn objects() -> #root::query::QuerySet<#struct_name> {
8009 #root::query::QuerySet::new()
8010 }
8011
8012 /// Eloquent-shape alias of [`Self::objects`]. Returns
8013 /// a fresh `QuerySet<Self>` ready for `.filter()` /
8014 /// `.where_()` / etc. Matches Laravel muscle-memory:
8015 ///
8016 /// ```ignore
8017 /// // Eloquent: Post::query()->where('published', true)
8018 /// // rustango: Post::query().filter("published", true)
8019 /// // or: Post::objects().filter("published", true)
8020 /// ```
8021 ///
8022 /// Both names point at the same underlying constructor;
8023 /// neither is preferred.
8024 #[must_use]
8025 pub fn query() -> #root::query::QuerySet<#struct_name> {
8026 #root::query::QuerySet::new()
8027 }
8028
8029 #( #extra_manager_fns )*
8030
8031 #insert_method
8032
8033 #bulk_insert_method
8034
8035 #bulk_upsert_pool_method
8036
8037 #bulk_update_method
8038
8039 #save_method
8040
8041 #pk_methods
8042
8043 #pk_value_helper
8044
8045 #aliased_row_helper
8046
8047 #column_consts
8048 }
8049
8050 #aliased_row_helper_my
8051
8052 #aliased_row_helper_sqlite
8053
8054 #load_related_impl
8055
8056 #load_related_impl_my
8057
8058 #load_related_impl_sqlite
8059
8060 #has_pk_value_impl
8061
8062 #fk_pk_access_impl
8063
8064 #assign_auto_pk_pool_impl
8065 }
8066}
8067
8068/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
8069/// `auto_assigns` but reading from `_returning_row` and writing to
8070/// `_row_mut` instead of `self`.
8071fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
8072 let root = rustango_root();
8073 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
8074 let col_lit = column.as_str();
8075 quote! {
8076 _row_mut.#ident = #root::sql::sqlx::Row::try_get(
8077 _returning_row,
8078 #col_lit,
8079 )?;
8080 }
8081 });
8082 quote! { #( #lines )* }
8083}
8084
8085/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
8086fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
8087 let lines = entries.iter().map(|e| {
8088 let ident = &e.ident;
8089 let col_ty = column_type_ident(ident);
8090 quote! {
8091 #[allow(non_upper_case_globals)]
8092 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
8093 }
8094 });
8095 quote! { #(#lines)* }
8096}
8097
8098/// Emit a hidden per-model module carrying one zero-sized type per field,
8099/// each with a `Column` impl pointing back at the model.
8100fn column_module_tokens(
8101 module_ident: &syn::Ident,
8102 struct_name: &syn::Ident,
8103 entries: &[ColumnEntry],
8104) -> TokenStream2 {
8105 let root = rustango_root();
8106 let items = entries.iter().map(|e| {
8107 let col_ty = column_type_ident(&e.ident);
8108 let value_ty = &e.value_ty;
8109 let name = &e.name;
8110 let column = &e.column;
8111 let field_type_tokens = &e.field_type_tokens;
8112 quote! {
8113 #[derive(::core::clone::Clone, ::core::marker::Copy)]
8114 pub struct #col_ty;
8115
8116 impl #root::core::Column for #col_ty {
8117 type Model = super::#struct_name;
8118 type Value = #value_ty;
8119 const NAME: &'static str = #name;
8120 const COLUMN: &'static str = #column;
8121 const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
8122 }
8123 }
8124 });
8125 quote! {
8126 #[doc(hidden)]
8127 #[allow(non_camel_case_types, non_snake_case)]
8128 pub mod #module_ident {
8129 // Re-import the parent scope so field types referencing
8130 // sibling models (e.g. `ForeignKey<Author>`) resolve
8131 // inside this submodule. Without this we'd hit
8132 // `proc_macro_derive_resolution_fallback` warnings.
8133 #[allow(unused_imports)]
8134 use super::*;
8135 #(#items)*
8136 }
8137 }
8138}
8139
8140fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
8141 let name = ident_name(field_ident);
8142 syn::Ident::new(&format!("{name}_col"), field_ident.span())
8143}
8144
8145/// A field's name without the `r#` of a raw ident: `r#type` is `type` (#1937).
8146fn ident_name(ident: &syn::Ident) -> String {
8147 syn::ext::IdentExt::unraw(ident).to_string()
8148}
8149
8150/// The ident for a field named `name`, raw when it is a keyword (`type` → `r#type`).
8151fn ident_from_name(name: &str, span: proc_macro2::Span) -> syn::Ident {
8152 match syn::parse_str::<syn::Ident>(name) {
8153 Ok(mut ident) => {
8154 ident.set_span(span);
8155 ident
8156 }
8157 Err(_) => syn::Ident::new_raw(name, span),
8158 }
8159}
8160
8161fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8162 syn::Ident::new(
8163 &format!("__rustango_cols_{struct_name}"),
8164 struct_name.span(),
8165 )
8166}
8167
8168fn from_row_impl_tokens(
8169 struct_name: &syn::Ident,
8170 from_row_inits: &[TokenStream2],
8171 from_row_inits_my: &[TokenStream2],
8172) -> TokenStream2 {
8173 let root = rustango_root();
8174 // The Postgres impl is always emitted — every rustango build pulls in
8175 // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8176 // routed through `#root::__impl_my_from_row!`, a cfg-gated
8177 // macro_rules whose body collapses to nothing when rustango is built
8178 // without the `mysql` feature. No user-facing feature shim required.
8179 //
8180 // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8181 // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8182 // back into a comma-separated list inside square brackets. Since each
8183 // entry is already in `field: expr` shape, the existing tokens slot in.
8184 quote! {
8185 #[cfg(feature = "postgres")]
8186 impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8187 for #struct_name
8188 {
8189 fn from_row(
8190 row: &'r #root::sql::sqlx::postgres::PgRow,
8191 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8192 ::core::result::Result::Ok(Self {
8193 #( #from_row_inits ),*
8194 })
8195 }
8196 }
8197
8198 #root::__impl_my_from_row!(#struct_name, |row| {
8199 #( #from_row_inits_my ),*
8200 });
8201
8202 #root::__impl_sqlite_from_row!(#struct_name, |row| {
8203 #( #from_row_inits ),*
8204 });
8205 }
8206}
8207
8208struct ContainerAttrs {
8209 table: Option<String>,
8210 display: Option<(String, proc_macro2::Span)>,
8211 /// Explicit app label from `#[rustango(app = "blog")]`.
8212 /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8213 /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8214 /// at runtime — this attribute is the override for cases where
8215 /// the inference is wrong (e.g. a model that conceptually belongs
8216 /// to one app but is physically in another module).
8217 app: Option<String>,
8218 /// Per-model admin knobs from
8219 /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8220 /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8221 /// admin code falls back to `AdminConfig::DEFAULT`.
8222 admin: Option<AdminAttrs>,
8223 /// Per-model audit configuration from `#[rustango(audit(...))]`.
8224 /// `None` when the model isn't audited — write paths emit no
8225 /// audit entries. When present, single-row writes capture
8226 /// before/after for the listed fields and bulk writes batch
8227 /// snapshots into one INSERT into `rustango_audit_log`.
8228 audit: Option<AuditAttrs>,
8229 /// `true` when `#[rustango(permissions)]` is present. Signals that
8230 /// `auto_create_permissions` should seed the four CRUD codenames for
8231 /// this model.
8232 permissions: bool,
8233 /// Many-to-many relations declared via
8234 /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8235 /// src = "post_id", dst = "tag_id"))]`.
8236 m2m: Vec<M2MAttr>,
8237 /// Polymorphic M2M relations declared via
8238 /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8239 /// pk_column = "taggable_id", ct_column = "taggable_type",
8240 /// related_column = "tag_id"))]` (issue #818).
8241 generic_m2m: Vec<GenericM2MAttr>,
8242 /// Composite indexes declared via
8243 /// `#[rustango(index("col1, col2"))]` or
8244 /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8245 /// Single-column indexes from `#[rustango(index)]` on fields are
8246 /// accumulated here during field collection.
8247 indexes: Vec<IndexAttr>,
8248 /// Table-level CHECK constraints declared via
8249 /// `#[rustango(check(name = "…", expr = "…"))]`.
8250 checks: Vec<CheckAttr>,
8251 /// Table-level PG `EXCLUDE` constraints declared via
8252 /// `#[rustango(exclude(name = "…", using = "gist", elements =
8253 /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8254 /// migration writer renders them on Postgres and skips with a
8255 /// warning on MySQL/SQLite. Issue #319.
8256 excludes: Vec<ExcludeAttr>,
8257 /// Composite (multi-column) FKs declared via
8258 /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8259 /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8260 composite_fks: Vec<CompositeFkAttr>,
8261 /// Generic ("any model") FKs declared via
8262 /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8263 /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8264 generic_fks: Vec<GenericFkAttr>,
8265 /// Where this model lives in a tenancy deployment, declared via
8266 /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8267 /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8268 /// to partition diff output between registry-scoped and
8269 /// tenant-scoped migration files.
8270 scope: Option<String>,
8271 /// Custom-Manager extension-trait name from
8272 /// `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8273 /// When set, the macro emits an empty `pub trait <name>: Sized {}`
8274 /// adjacent to the model so users can write
8275 /// `impl FooManagerExt for QuerySet<Foo> { fn published(self) -> Self ... }`
8276 /// and discover the convention from the model definition.
8277 manager_ext: Option<syn::Ident>,
8278 /// Extra QuerySet accessor names from
8279 /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8280 /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8281 /// the default `Self::objects()`. Multiple attributes allowed.
8282 manager_fns: Vec<syn::Ident>,
8283 /// Default ordering declared via `#[rustango(default_order =
8284 /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8285 /// `(column_name, desc_flag, span_for_error_reporting)` — the
8286 /// `-` prefix means descending; the `+` prefix or no prefix means
8287 /// ascending.
8288 default_order: Vec<(String, bool, proc_macro2::Span)>,
8289 /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8290 /// Routes the emitted schema's `is_view = true` so the migration
8291 /// snapshot skips this model (its underlying SQL view is operator-
8292 /// managed, not rustango-managed).
8293 is_view: bool,
8294 /// `#[rustango(managed = false)]` — who owns the table.
8295 /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8296 /// the migration snapshot skips this model so `makemigrations` /
8297 /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8298 /// TABLE` against it (operator-managed schema).
8299 managed: bool,
8300 /// Name of the model's base manager, from
8301 /// `#[rustango(base_manager_name = "...")]`. Threaded into
8302 /// `ModelSchema::base_manager_name`. Declarative-only today.
8303 base_manager_name: Option<String>,
8304 /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8305 /// field this model's instances are ordered relative to.
8306 /// Declarative-only today; threaded onto
8307 /// `ModelSchema::order_with_respect_to`.
8308 order_with_respect_to: Option<String>,
8309 /// `#[rustango(proxy)]` / `#[rustango(proxy = true)]`.
8310 /// Marks the model as a proxy that
8311 /// shares its DB table with another struct. Threaded into
8312 /// `ModelSchema::proxy` so future codegen can skip table-owning
8313 /// behavior for proxies.
8314 proxy: bool,
8315 /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8316 /// Each comma-separated capability token surfaces on
8317 /// `ModelSchema::required_db_features` so `manage check --deploy`
8318 /// can warn when the active dialect lacks one.
8319 required_db_features: Vec<String>,
8320 /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8321 /// Normalized to the dialect name `manage check --deploy`
8322 /// compares against `Settings.database.backend`. Aliases
8323 /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8324 /// stored under the canonical name.
8325 required_db_vendor: Option<String>,
8326 /// `#[rustango(default_related_name = "...")]`. Threaded into
8327 /// `ModelSchema::default_related_name`. Reverse-relation accessor
8328 /// name to use when an FK / M2M field doesn't override it.
8329 /// Today rustango doesn't auto-emit reverse managers; the
8330 /// metadata is the foundation for that work.
8331 default_related_name: Option<String>,
8332 /// `#[rustango(db_table_comment = "...")]`. Threaded into
8333 /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8334 /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8335 /// inline `COMMENT='...'`, SQLite: no-op).
8336 db_table_comment: Option<String>,
8337 /// `#[rustango(get_latest_by = "created_at")]` /
8338 /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8339 /// `(column, descending)` where `descending = true` when the
8340 /// attribute value starts with `-`. Threaded into
8341 /// `ModelSchema::get_latest_by`.
8342 get_latest_by: Option<(String, bool)>,
8343 /// Extra `(codename, label)` permissions
8344 /// from `#[rustango(extra_permissions = "approve:Can approve,
8345 /// archive:Can archive")]`. Comma-separated `codename:label`
8346 /// pairs. Threaded into `ModelSchema::extra_permissions`.
8347 extra_permissions: Vec<(String, String)>,
8348 /// Which CRUD codenames (`"add"` / `"change"` / `"delete"` /
8349 /// `"view"`) the framework
8350 /// auto-creates. Empty `Vec` (default) means **all four**. Set via
8351 /// `#[rustango(default_permissions = "view,change")]` to opt out.
8352 /// Validated at parse time; unknown actions fail with a span-pointing
8353 /// error.
8354 default_permissions: Vec<String>,
8355 /// `#[rustango(verbose_name = "blog post")]` — the
8356 /// human-readable singular label for the model. Threaded into
8357 /// `ModelSchema::verbose_name` so admin section headers /
8358 /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8359 /// over the Rust struct identifier.
8360 verbose_name: Option<String>,
8361 /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8362 /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8363 /// When unset, `display_label_plural()` auto-suffixes `s`.
8364 verbose_name_plural: Option<String>,
8365 /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8366 /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8367 /// a name (used by `QuerySet::without_global_scope`) with a
8368 /// `fn() -> WhereExpr` path that the macro emits into
8369 /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8370 global_scopes: Vec<GlobalScopeAttr>,
8371 /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8372 /// `#[rustango(through(name, far, far_fk_column, intermediate,
8373 /// intermediate_fk_column, intermediate_pk_column))]` — issue
8374 /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8375 /// entry emits an inherent `<name>_through(&self)` accessor that
8376 /// returns a `QuerySet<Far>` filtered via a correlated subquery
8377 /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8378 /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8379 through_relations: Vec<ThroughAttr>,
8380 /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8381 /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8382 /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8383 /// Each entry emits two associated functions on the parent —
8384 /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8385 /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8386 /// over a correlated subquery against the child table. Users
8387 /// drop the result into `QuerySet::where_raw(...)`.
8388 reverse_has_relations: Vec<ReverseHasAttr>,
8389 /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8390 /// issue #830. Each emits a `Model::generic_reverse_relations()`
8391 /// entry so the relation-existence family resolves polymorphic
8392 /// children by name.
8393 generic_has_relations: Vec<GenericHasAttr>,
8394}
8395
8396/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8397/// declaration. Each entry becomes one `core::GlobalScope` in the
8398/// emitted schema literal; `apply` resolves at macro-expand time
8399/// against the consumer's scope so the function must be in scope at
8400/// the use site. Issue #820.
8401struct GlobalScopeAttr {
8402 name: String,
8403 apply: syn::Path,
8404}
8405
8406/// Parsed `#[rustango(through(...))]` declaration. Issue
8407/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8408/// `hasManyThrough` / `hasOneThrough` parity.
8409///
8410/// `Country hasManyThrough Post via User` declares as:
8411///
8412/// ```ignore
8413/// #[rustango(through(
8414/// name = "posts",
8415/// far = "Post",
8416/// far_fk_column = "author_id",
8417/// intermediate = "User",
8418/// intermediate_fk_column = "country_id",
8419/// ))]
8420/// struct Country { ... }
8421/// ```
8422///
8423/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8424/// which returns a queryset filtered via a correlated subquery —
8425/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8426/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8427/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8428/// because the subquery lives inside a `WhereExpr::InSubquery` node
8429/// and the outer queryset's pending list stays empty.
8430///
8431/// All four required arguments use **SQL column / table names**
8432/// (not Rust field names) to sidestep the multi-hop-filter substrate
8433/// gap. Once that substrate lands, a higher-level Rust-field-name
8434/// shorthand can be added without breaking this surface.
8435struct ThroughAttr {
8436 /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8437 name: String,
8438 /// Far model type identifier. `far = "Post"` → returns
8439 /// `QuerySet<Post>`. Resolved verbatim against the scope where
8440 /// the derive expands.
8441 far: syn::Ident,
8442 /// SQL column on the far model's table that references the
8443 /// intermediate model's primary key. For `Post`'s
8444 /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8445 /// default `<field>_id` convention) or whatever the user
8446 /// declared via `#[rustango(db_column = "...")]`.
8447 far_fk_column: String,
8448 /// Intermediate model type identifier. Needed to look up its
8449 /// `SCHEMA` so the subquery's `FROM` clause points at the
8450 /// intermediate table. `intermediate = "User"`.
8451 intermediate: syn::Ident,
8452 /// SQL column on the intermediate's table that references the
8453 /// source (this) model's primary key. For `User`'s
8454 /// `country: ForeignKey<Country>` the column is `"country_id"`.
8455 intermediate_fk_column: String,
8456 /// SQL primary-key column on the intermediate's table — the
8457 /// column the subquery projects. Optional; defaults to `"id"`
8458 /// (rustango's default PK column name). Override when the
8459 /// intermediate declares a custom PK column.
8460 intermediate_pk_column: String,
8461}
8462
8463/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8464/// child_fk_column = "..."))]` declaration. Issue
8465/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8466/// `whereHas` / `whereDoesntHave` parity.
8467///
8468/// `Post hasMany Comment` declares as:
8469///
8470/// ```ignore
8471/// #[rustango(reverse_has(
8472/// name = "comments",
8473/// child = "Comment",
8474/// child_fk_column = "post_id",
8475/// ))]
8476/// struct Post { ... }
8477/// ```
8478///
8479/// The macro emits two associated functions on `Post`:
8480///
8481/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8482/// `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8483/// <outer>.<self_pk_column>)`.
8484/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8485/// but `NOT EXISTS`, the `whereDoesntHave` analog.
8486///
8487/// User code:
8488///
8489/// ```ignore
8490/// // Posts with at least one comment:
8491/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8492/// // Posts with no comments:
8493/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8494/// ```
8495///
8496/// As with #817, all column / table identifiers are **SQL names**
8497/// (not Rust field names) so the substrate is independent of the
8498/// outstanding multi-hop filter resolver gap. The emitted
8499/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8500/// SQL-emit time via the writer's scope stack.
8501struct ReverseHasAttr {
8502 /// Accessor name. `name = "comments"` → emits
8503 /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8504 name: String,
8505 /// Child model type identifier. `child = "Comment"` — needed to
8506 /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8507 /// points at the child table.
8508 child: syn::Ident,
8509 /// SQL column on the child's table that references this model's
8510 /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8511 /// column is `"post_id"`.
8512 child_fk_column: String,
8513 /// SQL primary-key column on **this** model's table — the column
8514 /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8515 /// (rustango's default PK column name).
8516 self_pk_column: String,
8517}
8518
8519/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8520/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8521/// relation-existence family (issue #830). The child is a polymorphic,
8522/// content-type-discriminated model; emits a `Model::
8523/// generic_reverse_relations()` entry the queryset resolves by name.
8524struct GenericHasAttr {
8525 /// Accessor name, e.g. `name = "tags"`.
8526 name: String,
8527 /// Child model type identifier (`child = "Tag"`) — looked up for its
8528 /// `SCHEMA` so the subquery's `FROM` points at the child table.
8529 child: syn::Ident,
8530 /// Column on the child table holding the parent's content-type id.
8531 /// Optional; defaults to `"content_type_id"`.
8532 ct_column: String,
8533 /// Column on the child table holding the parent's PK value.
8534 /// Optional; defaults to `"object_pk"`.
8535 pk_column: String,
8536 /// SQL primary-key column on **this** (parent) model's table.
8537 /// Optional; defaults to `"id"`.
8538 self_pk_column: String,
8539}
8540
8541/// Parsed form of one index declaration (field-level or container-level).
8542struct IndexAttr {
8543 /// Index name; auto-derived when `None` at parse time.
8544 name: Option<String>,
8545 /// Column names in the index.
8546 columns: Vec<String>,
8547 /// `true` for `CREATE UNIQUE INDEX`.
8548 unique: bool,
8549 /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8550 /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8551 /// `"btree"` when the attribute is absent — the DDL writer omits
8552 /// the `USING` clause and the backend uses its own default
8553 /// (btree on every supported dialect).
8554 method: String,
8555 /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8556 /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8557 /// condition = "...", name = "..."))]`. `None` for plain indexes.
8558 where_clause: Option<String>,
8559 /// Covering-index columns (PG 11+ `INCLUDE (...)` clause).
8560 /// Empty `Vec` (the default) means "no covering columns".
8561 include: Vec<String>,
8562}
8563
8564/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8565struct CheckAttr {
8566 name: String,
8567 expr: String,
8568}
8569
8570/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8571/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8572/// PG-only — surfaced on every backend in the macro emit; the migration
8573/// writer skips the op on MySQL/SQLite. Issue #319.
8574struct ExcludeAttr {
8575 /// Constraint name (free-form Rust identifier).
8576 name: String,
8577 /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8578 using: String,
8579 /// Comma-separated `(column, operator)` pairs, in declaration
8580 /// order. Parsed from `"col WITH op, col WITH op"`.
8581 elements: Vec<(String, String)>,
8582 /// Optional `WHERE` predicate (raw SQL).
8583 where_clause: Option<String>,
8584}
8585
8586/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8587/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8588/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8589/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8590/// the model, not the field.
8591struct CompositeFkAttr {
8592 /// Logical relation name (free-form Rust identifier).
8593 name: String,
8594 /// SQL table name of the target.
8595 to: String,
8596 /// Source-side column names, in declaration order.
8597 from: Vec<String>,
8598 /// Target-side column names, same length / order as `from`.
8599 on: Vec<String>,
8600}
8601
8602/// Parsed form of one `#[rustango(generic_fk(name = "target",
8603/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8604/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8605/// generic ("any model") FKs live on the model, not the field.
8606struct GenericFkAttr {
8607 /// Logical relation name (free-form Rust identifier).
8608 name: String,
8609 /// Source-side column carrying the `content_type_id` value.
8610 ct_column: String,
8611 /// Source-side column carrying the target row's primary key.
8612 pk_column: String,
8613}
8614
8615/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8616struct M2MAttr {
8617 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8618 name: String,
8619 /// Target table (e.g. `"app_tags"`).
8620 to: String,
8621 /// Junction table (e.g. `"post_tags"`).
8622 through: String,
8623 /// Source FK column in the junction table (e.g. `"post_id"`).
8624 src: String,
8625 /// Destination FK column in the junction table (e.g. `"tag_id"`).
8626 dst: String,
8627 /// Whether the migration writer should auto-create the junction
8628 /// table. Default `true`. Set `auto_create = false` (#324) when
8629 /// the operator declares the through table as its own
8630 /// `#[derive(Model)]` struct with extra columns.
8631 auto_create: bool,
8632}
8633
8634/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8635/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8636/// junction carries a ContentType discriminator so unrelated models
8637/// share one pivot + related set.
8638struct GenericM2MAttr {
8639 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8640 name: String,
8641 /// Polymorphic junction table (e.g. `"taggables"`).
8642 through: String,
8643 /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8644 pk_column: String,
8645 /// Junction column holding the owning model's `content_type_id`
8646 /// discriminator (e.g. `"taggable_type"`).
8647 ct_column: String,
8648 /// Junction column holding the related model PK (e.g. `"tag_id"`).
8649 related_column: String,
8650}
8651
8652/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8653/// "user"))]`. `track` is a comma-separated list of field names whose
8654/// before/after values land in the JSONB `changes` column. `source`
8655/// is informational only — it pins a default source when the model
8656/// is written outside any `audit::with_source(...)` scope (rare).
8657#[derive(Default)]
8658struct AuditAttrs {
8659 /// Field names to capture in the `changes` JSONB. Validated
8660 /// against declared scalar fields at compile time. Empty means
8661 /// "track every scalar field".
8662 track: Option<(Vec<String>, proc_macro2::Span)>,
8663}
8664
8665/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8666/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8667/// comma-separated strings; we validate each ident against the model's
8668/// declared fields at compile time.
8669#[derive(Default)]
8670struct AdminAttrs {
8671 list_display: Option<(Vec<String>, proc_macro2::Span)>,
8672 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8673 list_per_page: Option<usize>,
8674 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8675 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8676 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8677 /// Bulk action names. No field-validation against model fields —
8678 /// these are action handlers, not column references.
8679 actions: Option<(Vec<String>, proc_macro2::Span)>,
8680 /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8681 /// sections, comma-separated fields per section, optional
8682 /// `Title:` prefix. Empty title omits the `<legend>`.
8683 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8684 /// `admin(list_display_links = "title")`. Names
8685 /// from `list_display` whose cells should link to detail/edit.
8686 /// Issue #350.
8687 list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8688 /// `admin(search_help_text = "...")`. Short
8689 /// caption rendered beside the admin list view's search box.
8690 /// Issue #353.
8691 search_help_text: Option<String>,
8692 /// `admin(actions_on_top = false)`. Hides the
8693 /// action-bar above the table. Default `true`. Issue #354.
8694 actions_on_top: Option<bool>,
8695 /// `admin(actions_on_bottom = true)`. Renders an
8696 /// additional action-bar below the table. Default `false`.
8697 /// Issue #354.
8698 actions_on_bottom: Option<bool>,
8699 /// `admin(date_hierarchy = "created_at")`. Name of
8700 /// a date / datetime field whose values render as a clickable
8701 /// year / month / day drill-down strip above the list table.
8702 /// Empty / unset disables the strip. Issue #355.
8703 date_hierarchy: Option<String>,
8704 /// `admin(prepopulated_fields = "slug:title")`.
8705 /// Each entry is `target:source[+source2]`; multiple entries are
8706 /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8707 /// The admin change-form emits JS that slugifies the source values
8708 /// into the target field on every keystroke. Issue #356.
8709 prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8710 /// `admin(raw_id_fields = "parent, owner")`. Names
8711 /// of FK fields whose change-form widget renders a lookup link
8712 /// next to the input. Issue #357.
8713 raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8714 /// `admin(autocomplete_fields = "author_id")`.
8715 /// Names of FK fields whose change-form widget renders an
8716 /// Ajax-driven typeahead populated from a `__autocomplete`
8717 /// endpoint on the target model. Issue #358.
8718 autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8719 /// `admin(list_select_related = "all" | "none" | "author, …")`
8720 /// — tunes the admin list view's FK auto-JOIN
8721 /// policy. Default `"all"` matches rustango's join-everything
8722 /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8723 list_select_related: Option<String>,
8724 /// `admin(formfield_overrides = "field:widget, field2:widget2")`.
8725 /// Each entry is `field_name:widget_name`; multiple
8726 /// entries comma-separated. Empty / unset → no overrides. The
8727 /// list of widget names supported is documented on
8728 /// `AdminConfig::formfield_overrides`. Issue #359.
8729 formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8730}
8731
8732fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8733 let mut out = ContainerAttrs {
8734 table: None,
8735 display: None,
8736 app: None,
8737 admin: None,
8738 audit: None,
8739 // Default `permissions = true` so every `#[derive(Model)]`
8740 // gets the four CRUD codenames seeded by `auto_create_permissions`
8741 // and is visible to non-superusers in the tenant admin without
8742 // manual per-model annotation. Models that intentionally don't
8743 // want permission rows (registry-internal types, framework
8744 // tables operators shouldn't manage directly) opt out via
8745 // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8746 // out-of-the-box admin invisibility regression (#62).
8747 permissions: true,
8748 m2m: Vec::new(),
8749 generic_m2m: Vec::new(),
8750 indexes: Vec::new(),
8751 checks: Vec::new(),
8752 excludes: Vec::new(),
8753 composite_fks: Vec::new(),
8754 generic_fks: Vec::new(),
8755 scope: None,
8756 manager_ext: None,
8757 manager_fns: Vec::new(),
8758 default_order: Vec::new(),
8759 is_view: false,
8760 managed: true,
8761 verbose_name: None,
8762 verbose_name_plural: None,
8763 base_manager_name: None,
8764 order_with_respect_to: None,
8765 proxy: false,
8766 required_db_features: Vec::new(),
8767 required_db_vendor: None,
8768 default_related_name: None,
8769 db_table_comment: None,
8770 get_latest_by: None,
8771 extra_permissions: Vec::new(),
8772 default_permissions: Vec::new(),
8773 global_scopes: Vec::new(),
8774 through_relations: Vec::new(),
8775 reverse_has_relations: Vec::new(),
8776 generic_has_relations: Vec::new(),
8777 };
8778 for attr in &input.attrs {
8779 if !attr.path().is_ident("rustango") {
8780 continue;
8781 }
8782 attr.parse_nested_meta(|meta| {
8783 if meta.path.is_ident("table") {
8784 let s: LitStr = meta.value()?.parse()?;
8785 let name = s.value();
8786 // v0.27.3 (#65) — macro-time guard against table names
8787 // that compile but break SQL downstream. Hyphens are
8788 // the common footgun: PostgreSQL accepts a quoted
8789 // `"intermediate-region"` in CREATE TABLE, but the
8790 // FK / index name derivation in `migrate::ddl`
8791 // emits `intermediate-region_field_fkey` unquoted,
8792 // which then fails the SQL parser. Same shape rule
8793 // as Postgres regular identifiers so the safe path
8794 // is the only path.
8795 validate_table_name(&name, s.span())?;
8796 out.table = Some(name);
8797 return Ok(());
8798 }
8799 if meta.path.is_ident("display") {
8800 let s: LitStr = meta.value()?.parse()?;
8801 out.display = Some((s.value(), s.span()));
8802 return Ok(());
8803 }
8804 if meta.path.is_ident("app") {
8805 let s: LitStr = meta.value()?.parse()?;
8806 out.app = Some(s.value());
8807 return Ok(());
8808 }
8809 if meta.path.is_ident("scope") {
8810 let s: LitStr = meta.value()?.parse()?;
8811 let val = s.value();
8812 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8813 return Err(meta.error(format!(
8814 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8815 )));
8816 }
8817 out.scope = Some(val);
8818 return Ok(());
8819 }
8820 if meta.path.is_ident("admin") {
8821 let mut admin = AdminAttrs::default();
8822 meta.parse_nested_meta(|inner| {
8823 if inner.path.is_ident("list_display") {
8824 let s: LitStr = inner.value()?.parse()?;
8825 admin.list_display =
8826 Some((split_field_list(&s.value()), s.span()));
8827 return Ok(());
8828 }
8829 if inner.path.is_ident("search_fields") {
8830 let s: LitStr = inner.value()?.parse()?;
8831 admin.search_fields =
8832 Some((split_field_list(&s.value()), s.span()));
8833 return Ok(());
8834 }
8835 if inner.path.is_ident("readonly_fields") {
8836 let s: LitStr = inner.value()?.parse()?;
8837 admin.readonly_fields =
8838 Some((split_field_list(&s.value()), s.span()));
8839 return Ok(());
8840 }
8841 if inner.path.is_ident("list_per_page") {
8842 let lit: syn::LitInt = inner.value()?.parse()?;
8843 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8844 return Ok(());
8845 }
8846 if inner.path.is_ident("ordering") {
8847 let s: LitStr = inner.value()?.parse()?;
8848 admin.ordering = Some((
8849 parse_ordering_list(&s.value()),
8850 s.span(),
8851 ));
8852 return Ok(());
8853 }
8854 if inner.path.is_ident("list_filter") {
8855 let s: LitStr = inner.value()?.parse()?;
8856 admin.list_filter =
8857 Some((split_field_list(&s.value()), s.span()));
8858 return Ok(());
8859 }
8860 if inner.path.is_ident("actions") {
8861 let s: LitStr = inner.value()?.parse()?;
8862 admin.actions =
8863 Some((split_field_list(&s.value()), s.span()));
8864 return Ok(());
8865 }
8866 if inner.path.is_ident("fieldsets") {
8867 let s: LitStr = inner.value()?.parse()?;
8868 admin.fieldsets =
8869 Some((parse_fieldset_list(&s.value()), s.span()));
8870 return Ok(());
8871 }
8872 if inner.path.is_ident("list_display_links") {
8873 let s: LitStr = inner.value()?.parse()?;
8874 admin.list_display_links =
8875 Some((split_field_list(&s.value()), s.span()));
8876 return Ok(());
8877 }
8878 if inner.path.is_ident("search_help_text") {
8879 let s: LitStr = inner.value()?.parse()?;
8880 admin.search_help_text = Some(s.value());
8881 return Ok(());
8882 }
8883 if inner.path.is_ident("actions_on_top") {
8884 let lit: syn::LitBool = inner.value()?.parse()?;
8885 admin.actions_on_top = Some(lit.value);
8886 return Ok(());
8887 }
8888 if inner.path.is_ident("actions_on_bottom") {
8889 let lit: syn::LitBool = inner.value()?.parse()?;
8890 admin.actions_on_bottom = Some(lit.value);
8891 return Ok(());
8892 }
8893 if inner.path.is_ident("date_hierarchy") {
8894 let s: LitStr = inner.value()?.parse()?;
8895 admin.date_hierarchy = Some(s.value());
8896 return Ok(());
8897 }
8898 if inner.path.is_ident("prepopulated_fields") {
8899 let s: LitStr = inner.value()?.parse()?;
8900 admin.prepopulated_fields =
8901 Some((parse_prepopulated_list(&s.value()), s.span()));
8902 return Ok(());
8903 }
8904 if inner.path.is_ident("raw_id_fields") {
8905 let s: LitStr = inner.value()?.parse()?;
8906 admin.raw_id_fields =
8907 Some((split_field_list(&s.value()), s.span()));
8908 return Ok(());
8909 }
8910 if inner.path.is_ident("autocomplete_fields") {
8911 let s: LitStr = inner.value()?.parse()?;
8912 admin.autocomplete_fields =
8913 Some((split_field_list(&s.value()), s.span()));
8914 return Ok(());
8915 }
8916 if inner.path.is_ident("list_select_related") {
8917 let s: LitStr = inner.value()?.parse()?;
8918 admin.list_select_related = Some(s.value());
8919 return Ok(());
8920 }
8921 if inner.path.is_ident("formfield_overrides") {
8922 let s: LitStr = inner.value()?.parse()?;
8923 admin.formfield_overrides =
8924 Some((parse_formfield_overrides(&s.value()), s.span()));
8925 return Ok(());
8926 }
8927 Err(inner.error(
8928 "unknown admin attribute (supported: \
8929 `list_display`, `list_display_links`, \
8930 `search_fields`, `search_help_text`, \
8931 `readonly_fields`, \
8932 `list_filter`, `list_per_page`, `ordering`, `actions`, \
8933 `actions_on_top`, `actions_on_bottom`, \
8934 `date_hierarchy`, \
8935 `prepopulated_fields`, \
8936 `raw_id_fields`, \
8937 `autocomplete_fields`, \
8938 `list_select_related`, \
8939 `formfield_overrides`, \
8940 `fieldsets`)",
8941 ))
8942 })?;
8943 out.admin = Some(admin);
8944 return Ok(());
8945 }
8946 if meta.path.is_ident("manager") {
8947 // `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8948 // Stretch `from_queryset = "..."` is left as a
8949 // follow-up — the issue's primary acceptance is the
8950 // `ext = ...` trait emission.
8951 meta.parse_nested_meta(|inner| {
8952 if inner.path.is_ident("ext") {
8953 let s: LitStr = inner.value()?.parse()?;
8954 let name = s.value();
8955 if name.is_empty() {
8956 return Err(inner.error("manager(ext = \"...\") cannot be empty"));
8957 }
8958 out.manager_ext =
8959 Some(syn::Ident::new(&name, s.span()));
8960 return Ok(());
8961 }
8962 Err(inner.error(
8963 "unknown manager attribute (supported: `ext = \"TraitName\"`)",
8964 ))
8965 })?;
8966 return Ok(());
8967 }
8968 if meta.path.is_ident("manager_fn") {
8969 // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8970 // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8971 // next to the default `Self::objects()`. Multiple
8972 // attributes accumulate.
8973 let s: LitStr = meta.value()?.parse()?;
8974 let name = s.value();
8975 if name.is_empty() {
8976 return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8977 }
8978 if name == "objects" {
8979 return Err(meta.error(
8980 "`manager_fn = \"objects\"` collides with the default \
8981 accessor — pick a different name",
8982 ));
8983 }
8984 let ident = syn::Ident::new(&name, s.span());
8985 if out.manager_fns.iter().any(|prev| *prev == ident) {
8986 return Err(meta.error(format!(
8987 "duplicate `manager_fn = \"{name}\"`"
8988 )));
8989 }
8990 out.manager_fns.push(ident);
8991 return Ok(());
8992 }
8993 if meta.path.is_ident("default_order") {
8994 // `#[rustango(default_order = "-created_at, status")]`
8995 // — issue #291 / T2.5. Comma-separated list; `-prefix`
8996 // means descending, `+prefix` or bare name means ascending.
8997 // Per-query opt-in via `QuerySet::with_default_order()`.
8998 let s: LitStr = meta.value()?.parse()?;
8999 let raw = s.value();
9000 let span = s.span();
9001 let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
9002 Vec::new();
9003 for entry in raw.split(',') {
9004 let trimmed = entry.trim();
9005 if trimmed.is_empty() {
9006 return Err(syn::Error::new(
9007 span,
9008 "`default_order = \"...\"` has an empty entry — \
9009 check for a stray comma",
9010 ));
9011 }
9012 let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
9013 (true, rest.trim().to_owned())
9014 } else if let Some(rest) = trimmed.strip_prefix('+') {
9015 (false, rest.trim().to_owned())
9016 } else {
9017 (false, trimmed.to_owned())
9018 };
9019 if name.is_empty() {
9020 return Err(syn::Error::new(
9021 span,
9022 "`default_order` entry has no column name after the prefix",
9023 ));
9024 }
9025 if parsed.iter().any(|(n, _, _)| *n == name) {
9026 return Err(syn::Error::new(
9027 span,
9028 format!("duplicate column `{name}` in `default_order`"),
9029 ));
9030 }
9031 parsed.push((name, desc, span));
9032 }
9033 if parsed.is_empty() {
9034 return Err(syn::Error::new(
9035 span,
9036 "`default_order = \"...\"` cannot be empty",
9037 ));
9038 }
9039 out.default_order = parsed;
9040 return Ok(());
9041 }
9042 if meta.path.is_ident("global_scope") {
9043 // `#[rustango(global_scope(name = "active", apply =
9044 // path::to::fn))]` — issue #820. The apply function
9045 // path resolves at macro-expand time in the consumer's
9046 // scope; the macro re-emits it verbatim into the
9047 // `ModelSchema::global_scopes` slice literal.
9048 let span = meta.path.span();
9049 let mut scope_name: Option<String> = None;
9050 let mut apply_path: Option<syn::Path> = None;
9051 meta.parse_nested_meta(|inner| {
9052 if inner.path.is_ident("name") {
9053 let s: LitStr = inner.value()?.parse()?;
9054 let raw = s.value();
9055 if raw.trim().is_empty() {
9056 return Err(syn::Error::new(
9057 s.span(),
9058 "`global_scope(name = \"...\")` must not be empty",
9059 ));
9060 }
9061 scope_name = Some(raw);
9062 return Ok(());
9063 }
9064 if inner.path.is_ident("apply") {
9065 let p: syn::Path = inner.value()?.parse()?;
9066 apply_path = Some(p);
9067 return Ok(());
9068 }
9069 Err(inner.error(
9070 "unknown `global_scope` attribute (supported: \
9071 `name`, `apply`)",
9072 ))
9073 })?;
9074 let Some(name) = scope_name else {
9075 return Err(syn::Error::new(
9076 span,
9077 "`global_scope` requires `name = \"...\"`",
9078 ));
9079 };
9080 let Some(apply) = apply_path else {
9081 return Err(syn::Error::new(
9082 span,
9083 "`global_scope` requires `apply = fn_path`",
9084 ));
9085 };
9086 if out.global_scopes.iter().any(|s| s.name == name) {
9087 return Err(syn::Error::new(
9088 span,
9089 format!(
9090 "duplicate global scope name `{name}` — \
9091 pick a unique identifier so \
9092 `QuerySet::without_global_scope(\"{name}\")` \
9093 is unambiguous"
9094 ),
9095 ));
9096 }
9097 out.global_scopes.push(GlobalScopeAttr { name, apply });
9098 return Ok(());
9099 }
9100 if meta.path.is_ident("through") {
9101 // `#[rustango(through(name = "posts", far = "Post",
9102 // far_fk_column = "author_id", intermediate = "User",
9103 // intermediate_fk_column = "country_id"
9104 // [, intermediate_pk_column = "id"]))]` — issue #817.
9105 let span = meta.path.span();
9106 let mut accessor_name: Option<String> = None;
9107 let mut far_ident: Option<syn::Ident> = None;
9108 let mut far_fk_column: Option<String> = None;
9109 let mut intermediate_ident: Option<syn::Ident> = None;
9110 let mut intermediate_fk_column: Option<String> = None;
9111 let mut intermediate_pk_column: Option<String> = None;
9112 fn parse_nonempty_string(
9113 inner: &syn::meta::ParseNestedMeta<'_>,
9114 field: &str,
9115 ) -> syn::Result<String> {
9116 let s: LitStr = inner.value()?.parse()?;
9117 let raw = s.value();
9118 let trimmed = raw.trim();
9119 if trimmed.is_empty() {
9120 return Err(syn::Error::new(
9121 s.span(),
9122 format!("`through({field} = \"...\")` must not be empty"),
9123 ));
9124 }
9125 Ok(trimmed.to_owned())
9126 }
9127 meta.parse_nested_meta(|inner| {
9128 if inner.path.is_ident("name") {
9129 accessor_name = Some(parse_nonempty_string(&inner, "name")?);
9130 return Ok(());
9131 }
9132 if inner.path.is_ident("far") {
9133 // Far model name accepted as a string literal
9134 // so the attribute fits inside the existing
9135 // `parse_nested_meta` shape; parsed back into a
9136 // `syn::Ident` so the emitted accessor can
9137 // reference the type directly.
9138 let s: LitStr = inner.value()?.parse()?;
9139 let raw = s.value();
9140 let trimmed = raw.trim();
9141 if trimmed.is_empty() {
9142 return Err(syn::Error::new(
9143 s.span(),
9144 "`through(far = \"...\")` must not be empty",
9145 ));
9146 }
9147 far_ident = Some(syn::Ident::new(trimmed, s.span()));
9148 return Ok(());
9149 }
9150 if inner.path.is_ident("far_fk_column") {
9151 far_fk_column =
9152 Some(parse_nonempty_string(&inner, "far_fk_column")?);
9153 return Ok(());
9154 }
9155 if inner.path.is_ident("intermediate") {
9156 let s: LitStr = inner.value()?.parse()?;
9157 let raw = s.value();
9158 let trimmed = raw.trim();
9159 if trimmed.is_empty() {
9160 return Err(syn::Error::new(
9161 s.span(),
9162 "`through(intermediate = \"...\")` must not be empty",
9163 ));
9164 }
9165 intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9166 return Ok(());
9167 }
9168 if inner.path.is_ident("intermediate_fk_column") {
9169 intermediate_fk_column =
9170 Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9171 return Ok(());
9172 }
9173 if inner.path.is_ident("intermediate_pk_column") {
9174 intermediate_pk_column =
9175 Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9176 return Ok(());
9177 }
9178 Err(inner.error(
9179 "unknown `through` attribute (supported: \
9180 `name`, `far`, `far_fk_column`, \
9181 `intermediate`, `intermediate_fk_column`, \
9182 `intermediate_pk_column`)",
9183 ))
9184 })?;
9185 let Some(name) = accessor_name else {
9186 return Err(syn::Error::new(
9187 span,
9188 "`through` requires `name = \"...\"`",
9189 ));
9190 };
9191 let Some(far) = far_ident else {
9192 return Err(syn::Error::new(
9193 span,
9194 "`through` requires `far = \"FarModelType\"`",
9195 ));
9196 };
9197 let Some(far_fk_column) = far_fk_column else {
9198 return Err(syn::Error::new(
9199 span,
9200 "`through` requires `far_fk_column = \"<column>\"`",
9201 ));
9202 };
9203 let Some(intermediate) = intermediate_ident else {
9204 return Err(syn::Error::new(
9205 span,
9206 "`through` requires `intermediate = \"IntermediateModelType\"`",
9207 ));
9208 };
9209 let Some(intermediate_fk_column) = intermediate_fk_column else {
9210 return Err(syn::Error::new(
9211 span,
9212 "`through` requires `intermediate_fk_column = \"<column>\"`",
9213 ));
9214 };
9215 let intermediate_pk_column =
9216 intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9217 if out.through_relations.iter().any(|t| t.name == name) {
9218 return Err(syn::Error::new(
9219 span,
9220 format!(
9221 "duplicate `through(name = \"{name}\")` — \
9222 pick a unique accessor name"
9223 ),
9224 ));
9225 }
9226 out.through_relations.push(ThroughAttr {
9227 name,
9228 far,
9229 far_fk_column,
9230 intermediate,
9231 intermediate_fk_column,
9232 intermediate_pk_column,
9233 });
9234 return Ok(());
9235 }
9236 if meta.path.is_ident("reverse_has") {
9237 // `#[rustango(reverse_has(name = "comments",
9238 // child = "Comment", child_fk_column = "post_id"
9239 // [, self_pk_column = "id"]))]` — issue #830.
9240 let span = meta.path.span();
9241 let mut accessor_name: Option<String> = None;
9242 let mut child_ident: Option<syn::Ident> = None;
9243 let mut child_fk_column: Option<String> = None;
9244 let mut self_pk_column: Option<String> = None;
9245 meta.parse_nested_meta(|inner| {
9246 if inner.path.is_ident("name") {
9247 let s: LitStr = inner.value()?.parse()?;
9248 let raw = s.value();
9249 if raw.trim().is_empty() {
9250 return Err(syn::Error::new(
9251 s.span(),
9252 "`reverse_has(name = \"...\")` must not be empty",
9253 ));
9254 }
9255 accessor_name = Some(raw);
9256 return Ok(());
9257 }
9258 if inner.path.is_ident("child") {
9259 let s: LitStr = inner.value()?.parse()?;
9260 let raw = s.value();
9261 let trimmed = raw.trim();
9262 if trimmed.is_empty() {
9263 return Err(syn::Error::new(
9264 s.span(),
9265 "`reverse_has(child = \"...\")` must not be empty",
9266 ));
9267 }
9268 child_ident = Some(syn::Ident::new(trimmed, s.span()));
9269 return Ok(());
9270 }
9271 if inner.path.is_ident("child_fk_column") {
9272 let s: LitStr = inner.value()?.parse()?;
9273 let raw = s.value();
9274 let trimmed = raw.trim();
9275 if trimmed.is_empty() {
9276 return Err(syn::Error::new(
9277 s.span(),
9278 "`reverse_has(child_fk_column = \"...\")` must not be empty",
9279 ));
9280 }
9281 child_fk_column = Some(trimmed.to_owned());
9282 return Ok(());
9283 }
9284 if inner.path.is_ident("self_pk_column") {
9285 let s: LitStr = inner.value()?.parse()?;
9286 let raw = s.value();
9287 let trimmed = raw.trim();
9288 if trimmed.is_empty() {
9289 return Err(syn::Error::new(
9290 s.span(),
9291 "`reverse_has(self_pk_column = \"...\")` must not be empty",
9292 ));
9293 }
9294 self_pk_column = Some(trimmed.to_owned());
9295 return Ok(());
9296 }
9297 Err(inner.error(
9298 "unknown `reverse_has` attribute (supported: \
9299 `name`, `child`, `child_fk_column`, \
9300 `self_pk_column`)",
9301 ))
9302 })?;
9303 let Some(name) = accessor_name else {
9304 return Err(syn::Error::new(
9305 span,
9306 "`reverse_has` requires `name = \"...\"`",
9307 ));
9308 };
9309 let Some(child) = child_ident else {
9310 return Err(syn::Error::new(
9311 span,
9312 "`reverse_has` requires `child = \"ChildModelType\"`",
9313 ));
9314 };
9315 let Some(child_fk_column) = child_fk_column else {
9316 return Err(syn::Error::new(
9317 span,
9318 "`reverse_has` requires `child_fk_column = \"<column>\"`",
9319 ));
9320 };
9321 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9322 if out.reverse_has_relations.iter().any(|r| r.name == name) {
9323 return Err(syn::Error::new(
9324 span,
9325 format!(
9326 "duplicate `reverse_has(name = \"{name}\")` — \
9327 pick a unique accessor name"
9328 ),
9329 ));
9330 }
9331 out.reverse_has_relations.push(ReverseHasAttr {
9332 name,
9333 child,
9334 child_fk_column,
9335 self_pk_column,
9336 });
9337 return Ok(());
9338 }
9339 if meta.path.is_ident("generic_has") {
9340 // `#[rustango(generic_has(name = "tags",
9341 // child = "Tag", ct_column = "content_type_id",
9342 // pk_column = "object_pk" [, self_pk_column = "id"]))]`
9343 // — issue #830, the reverse generic-FK (GFK) arm of the
9344 // relation-existence family.
9345 let span = meta.path.span();
9346 let mut accessor_name: Option<String> = None;
9347 let mut child_ident: Option<syn::Ident> = None;
9348 let mut ct_column: Option<String> = None;
9349 let mut pk_column: Option<String> = None;
9350 let mut self_pk_column: Option<String> = None;
9351 meta.parse_nested_meta(|inner| {
9352 if inner.path.is_ident("name") {
9353 let s: LitStr = inner.value()?.parse()?;
9354 let raw = s.value();
9355 if raw.trim().is_empty() {
9356 return Err(syn::Error::new(
9357 s.span(),
9358 "`generic_has(name = \"...\")` must not be empty",
9359 ));
9360 }
9361 accessor_name = Some(raw);
9362 return Ok(());
9363 }
9364 if inner.path.is_ident("child") {
9365 let s: LitStr = inner.value()?.parse()?;
9366 let trimmed = s.value().trim().to_owned();
9367 if trimmed.is_empty() {
9368 return Err(syn::Error::new(
9369 s.span(),
9370 "`generic_has(child = \"...\")` must not be empty",
9371 ));
9372 }
9373 child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9374 return Ok(());
9375 }
9376 if inner.path.is_ident("ct_column") {
9377 let s: LitStr = inner.value()?.parse()?;
9378 let trimmed = s.value().trim().to_owned();
9379 if trimmed.is_empty() {
9380 return Err(syn::Error::new(
9381 s.span(),
9382 "`generic_has(ct_column = \"...\")` must not be empty",
9383 ));
9384 }
9385 ct_column = Some(trimmed);
9386 return Ok(());
9387 }
9388 if inner.path.is_ident("pk_column") {
9389 let s: LitStr = inner.value()?.parse()?;
9390 let trimmed = s.value().trim().to_owned();
9391 if trimmed.is_empty() {
9392 return Err(syn::Error::new(
9393 s.span(),
9394 "`generic_has(pk_column = \"...\")` must not be empty",
9395 ));
9396 }
9397 pk_column = Some(trimmed);
9398 return Ok(());
9399 }
9400 if inner.path.is_ident("self_pk_column") {
9401 let s: LitStr = inner.value()?.parse()?;
9402 let trimmed = s.value().trim().to_owned();
9403 if trimmed.is_empty() {
9404 return Err(syn::Error::new(
9405 s.span(),
9406 "`generic_has(self_pk_column = \"...\")` must not be empty",
9407 ));
9408 }
9409 self_pk_column = Some(trimmed);
9410 return Ok(());
9411 }
9412 Err(inner.error(
9413 "unknown `generic_has` attribute (supported: \
9414 `name`, `child`, `ct_column`, `pk_column`, \
9415 `self_pk_column`)",
9416 ))
9417 })?;
9418 let Some(name) = accessor_name else {
9419 return Err(syn::Error::new(
9420 span,
9421 "`generic_has` requires `name = \"...\"`",
9422 ));
9423 };
9424 let Some(child) = child_ident else {
9425 return Err(syn::Error::new(
9426 span,
9427 "`generic_has` requires `child = \"ChildModelType\"`",
9428 ));
9429 };
9430 let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9431 let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9432 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9433 if out.generic_has_relations.iter().any(|r| r.name == name) {
9434 return Err(syn::Error::new(
9435 span,
9436 format!(
9437 "duplicate `generic_has(name = \"{name}\")` — \
9438 pick a unique accessor name"
9439 ),
9440 ));
9441 }
9442 out.generic_has_relations.push(GenericHasAttr {
9443 name,
9444 child,
9445 ct_column,
9446 pk_column,
9447 self_pk_column,
9448 });
9449 return Ok(());
9450 }
9451 if meta.path.is_ident("audit") {
9452 let mut audit = AuditAttrs::default();
9453 meta.parse_nested_meta(|inner| {
9454 if inner.path.is_ident("track") {
9455 let s: LitStr = inner.value()?.parse()?;
9456 audit.track =
9457 Some((split_field_list(&s.value()), s.span()));
9458 return Ok(());
9459 }
9460 Err(inner.error(
9461 "unknown audit attribute (supported: `track`)",
9462 ))
9463 })?;
9464 out.audit = Some(audit);
9465 return Ok(());
9466 }
9467 if meta.path.is_ident("permissions") {
9468 // Two forms accepted:
9469 // #[rustango(permissions)] — flag form, true
9470 // #[rustango(permissions = false)] — explicit opt-out
9471 // #[rustango(permissions = true)] — explicit opt-in
9472 if let Ok(v) = meta.value() {
9473 let lit: syn::LitBool = v.parse()?;
9474 out.permissions = lit.value;
9475 } else {
9476 out.permissions = true;
9477 }
9478 return Ok(());
9479 }
9480 if meta.path.is_ident("view") {
9481 // Issue #293 / T2.10. Two forms accepted, matching
9482 // the `permissions` flag pattern:
9483 // #[rustango(view)] — flag form, true
9484 // #[rustango(view = false)] — explicit opt-out
9485 // #[rustango(view = true)] — explicit opt-in
9486 if let Ok(v) = meta.value() {
9487 let lit: syn::LitBool = v.parse()?;
9488 out.is_view = lit.value;
9489 } else {
9490 out.is_view = true;
9491 }
9492 return Ok(());
9493 }
9494 if meta.path.is_ident("managed") {
9495 // Who owns the table. Issue #321.
9496 // #[rustango(managed = false)] — operator-managed table
9497 // #[rustango(managed = true)] — rustango-managed (the default)
9498 // Bare-flag form is intentionally not accepted: writing
9499 // `#[rustango(managed)]` reads as "yes please manage it"
9500 // which is already the default. The opt-out is the only
9501 // useful state, so it must be explicit.
9502 let v = meta.value()?;
9503 let lit: syn::LitBool = v.parse()?;
9504 out.managed = lit.value;
9505 return Ok(());
9506 }
9507 if meta.path.is_ident("verbose_name") {
9508 let s: LitStr = meta.value()?.parse()?;
9509 out.verbose_name = Some(s.value());
9510 return Ok(());
9511 }
9512 if meta.path.is_ident("verbose_name_plural") {
9513 let s: LitStr = meta.value()?.parse()?;
9514 out.verbose_name_plural = Some(s.value());
9515 return Ok(());
9516 }
9517 if meta.path.is_ident("db_table_comment") {
9518 // Free-form table-level comment attached to the DB
9519 // catalog.
9520 let s: LitStr = meta.value()?.parse()?;
9521 out.db_table_comment = Some(s.value());
9522 return Ok(());
9523 }
9524 if meta.path.is_ident("proxy") {
9525 // Declarative flag
9526 // marking the struct as a proxy of another model that
9527 // shares its DB table. Stored on `ModelSchema::proxy`
9528 // so future codegen can skip `CreateTable` emission
9529 // for proxies (parent owns the table) and route
9530 // per-instance method resolution to the proxy class.
9531 //
9532 // Accepts `proxy` (bare → true) and `proxy = true/false`.
9533 let value = if meta.input.peek(syn::Token![=]) {
9534 meta.value()?.parse::<syn::LitBool>()?.value
9535 } else {
9536 true
9537 };
9538 out.proxy = value;
9539 return Ok(());
9540 }
9541 if meta.path.is_ident("order_with_respect_to") {
9542 // `order_with_respect_to = "parent_fk"` — the model's
9543 // instances are intrinsically ordered relative to
9544 // their parent FK.
9545 //
9546 // rustango stores the FK field name on
9547 // `ModelSchema::order_with_respect_to`. Declarative-only
9548 // today: the migration writer + admin surfaces still
9549 // treat every model identically. Future codegen will
9550 // key off the metadata to auto-emit the `_order`
9551 // column and reorder helpers.
9552 //
9553 // Validated as a Rust-shape identifier so the macro
9554 // can reject typos at derive time.
9555 let s: LitStr = meta.value()?.parse()?;
9556 let raw = s.value();
9557 if raw.is_empty() {
9558 return Err(syn::Error::new(
9559 s.span(),
9560 "`order_with_respect_to` must be a non-empty FK field name",
9561 ));
9562 }
9563 let valid = raw
9564 .chars()
9565 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9566 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9567 if !valid {
9568 return Err(syn::Error::new(
9569 s.span(),
9570 format!(
9571 "`order_with_respect_to` must be a valid Rust \
9572 identifier (letters / digits / underscores, \
9573 not starting with a digit); got `{raw}`"
9574 ),
9575 ));
9576 }
9577 out.order_with_respect_to = Some(raw);
9578 return Ok(());
9579 }
9580 if meta.path.is_ident("required_db_features") {
9581 // Capability tokens the model needs (e.g.
9582 // `"json_extract"`, `"window_functions"`,
9583 // `"row_security"`). Comma-separated.
9584 // `manage check --deploy` walks every model and warns
9585 // when the active backend doesn't advertise the
9586 // capability.
9587 //
9588 // rustango ships a small registry of capability tokens
9589 // each dialect supports — see `Dialect::supports`.
9590 // Unknown tokens still parse (they end up on the
9591 // schema and show up in the warning) so projects can
9592 // declare aspirational capabilities and the check
9593 // verb will keep nagging until the dialect implements
9594 // them.
9595 let s: LitStr = meta.value()?.parse()?;
9596 let raw = s.value();
9597 let features: Vec<String> = raw
9598 .split(',')
9599 .map(str::trim)
9600 .filter(|s| !s.is_empty())
9601 .map(str::to_owned)
9602 .collect();
9603 if features.is_empty() {
9604 return Err(syn::Error::new(
9605 s.span(),
9606 "`required_db_features` must list at least one \
9607 comma-separated capability token",
9608 ));
9609 }
9610 out.required_db_features = features;
9611 return Ok(());
9612 }
9613 if meta.path.is_ident("required_db_vendor") {
9614 // The model is only meant to run against the named DB
9615 // backend. `manage check --deploy` flags a mismatch so
9616 // ops catches "I forgot to switch DATABASE_URL" at
9617 // deploy time rather than runtime.
9618 //
9619 // Restricted to the three backends rustango ships
9620 // dialects for so the check verb can compare reliably.
9621 let s: LitStr = meta.value()?.parse()?;
9622 let raw = s.value().to_ascii_lowercase();
9623 match raw.as_str() {
9624 "postgresql" | "postgres" | "pg" => {
9625 out.required_db_vendor = Some("postgres".to_owned());
9626 }
9627 "mysql" | "mariadb" => {
9628 out.required_db_vendor = Some("mysql".to_owned());
9629 }
9630 "sqlite" | "sqlite3" => {
9631 out.required_db_vendor = Some("sqlite".to_owned());
9632 }
9633 _ => {
9634 return Err(syn::Error::new(
9635 s.span(),
9636 format!(
9637 "unknown required_db_vendor `{raw}` — \
9638 expected `postgres` (aliases: `postgresql`, `pg`), \
9639 `mysql` (alias: `mariadb`), or `sqlite` \
9640 (alias: `sqlite3`)"
9641 ),
9642 ));
9643 }
9644 }
9645 return Ok(());
9646 }
9647 if meta.path.is_ident("base_manager_name") {
9648 // Name of the manager that `<instance>.<relation>_set`
9649 // uses when resolving reverse-relation managers.
9650 // Distinct from `default_manager_name` (what
9651 // `Self::objects()` returns). Stored on
9652 // `ModelSchema::base_manager_name`.
9653 //
9654 // Validated as a Rust identifier so it stays safe to
9655 // re-emit as code in future reverse-manager codegen.
9656 let s: LitStr = meta.value()?.parse()?;
9657 let raw = s.value();
9658 if raw.is_empty() {
9659 return Err(syn::Error::new(
9660 s.span(),
9661 "`base_manager_name` must be a non-empty string",
9662 ));
9663 }
9664 let valid = raw
9665 .chars()
9666 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9667 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9668 if !valid {
9669 return Err(syn::Error::new(
9670 s.span(),
9671 format!(
9672 "`base_manager_name` must be a valid Rust \
9673 identifier (letters / digits / underscores, \
9674 not starting with a digit); got `{raw}`"
9675 ),
9676 ));
9677 }
9678 out.base_manager_name = Some(raw);
9679 return Ok(());
9680 }
9681 if meta.path.is_ident("default_related_name") {
9682 // The name reverse-relation accessors use when callers
9683 // don't override `related_name = "..."` on the FK / M2M
9684 // field. Stored on `ModelSchema::default_related_name`
9685 // so future reverse-manager codegen / schema emit /
9686 // admin templates can pick the right accessor name
9687 // (today rustango doesn't auto-emit reverse managers;
9688 // the metadata is the foundation for that work).
9689 //
9690 // Enforced non-empty + ASCII identifier-shape so the
9691 // string is safe to use as a Rust ident later.
9692 let s: LitStr = meta.value()?.parse()?;
9693 let raw = s.value();
9694 if raw.is_empty() {
9695 return Err(syn::Error::new(
9696 s.span(),
9697 "`default_related_name` must be a non-empty string",
9698 ));
9699 }
9700 let valid = raw
9701 .chars()
9702 .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9703 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9704 if !valid {
9705 return Err(syn::Error::new(
9706 s.span(),
9707 format!(
9708 "`default_related_name` must be snake_case ASCII \
9709 (lowercase letters / digits / underscores, not \
9710 starting with a digit); got `{raw}`"
9711 ),
9712 ));
9713 }
9714 out.default_related_name = Some(raw);
9715 return Ok(());
9716 }
9717 if meta.path.is_ident("extra_permissions") {
9718 // Comma-separated `codename:label` pairs.
9719 let s: LitStr = meta.value()?.parse()?;
9720 let raw = s.value();
9721 let mut pairs = Vec::new();
9722 for entry in raw.split(',') {
9723 let entry = entry.trim();
9724 if entry.is_empty() {
9725 continue;
9726 }
9727 let (codename, label) = match entry.split_once(':') {
9728 Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9729 None => (entry.to_owned(), entry.to_owned()),
9730 };
9731 if codename.is_empty() {
9732 return Err(meta.error(
9733 "`extra_permissions` entries must be `codename:label` pairs",
9734 ));
9735 }
9736 pairs.push((codename, label));
9737 }
9738 if pairs.is_empty() {
9739 return Err(meta
9740 .error("`extra_permissions = \"…\"` must list at least one pair"));
9741 }
9742 out.extra_permissions = pairs;
9743 return Ok(());
9744 }
9745 if meta.path.is_ident("default_permissions") {
9746 // Comma-separated subset of the CRUD action set.
9747 // Empty means all four (the framework default).
9748 let s: LitStr = meta.value()?.parse()?;
9749 let raw = s.value();
9750 let mut actions: Vec<String> = Vec::new();
9751 for entry in raw.split(',') {
9752 let action = entry.trim().to_ascii_lowercase();
9753 if action.is_empty() {
9754 continue;
9755 }
9756 match action.as_str() {
9757 "add" | "change" | "delete" | "view" => {}
9758 other => {
9759 return Err(syn::Error::new(
9760 s.span(),
9761 format!(
9762 "unknown default_permissions action `{other}` — \
9763 expected one of `add`, `change`, `delete`, `view`"
9764 ),
9765 ));
9766 }
9767 }
9768 if !actions.contains(&action) {
9769 actions.push(action);
9770 }
9771 }
9772 if actions.is_empty() {
9773 return Err(syn::Error::new(
9774 s.span(),
9775 "`default_permissions = \"…\"` must list at least one action; \
9776 use `permissions = false` on the container if you want NO \
9777 permissions seeded for this model.",
9778 ));
9779 }
9780 out.default_permissions = actions;
9781 return Ok(());
9782 }
9783 if meta.path.is_ident("get_latest_by") {
9784 // The `-` prefix selects descending order.
9785 let s: LitStr = meta.value()?.parse()?;
9786 let raw = s.value();
9787 let trimmed = raw.trim();
9788 if trimmed.is_empty() {
9789 return Err(meta.error("`get_latest_by` must name a column"));
9790 }
9791 let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9792 (stripped.to_owned(), true)
9793 } else if let Some(stripped) = trimmed.strip_prefix('+') {
9794 (stripped.to_owned(), false)
9795 } else {
9796 (trimmed.to_owned(), false)
9797 };
9798 if col.is_empty() {
9799 return Err(meta.error("`get_latest_by` must name a column"));
9800 }
9801 out.get_latest_by = Some((col, desc));
9802 return Ok(());
9803 }
9804 if meta.path.is_ident("unique_together") {
9805 // Composite UNIQUE index. Two syntaxes:
9806 //
9807 // #[rustango(unique_together = "org_id, user_id")] — auto-derived name
9808 // #[rustango(unique_together(columns = "org_id, user_id", name = "x"))] — explicit name
9809 //
9810 // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9811 // (col1, col2)`, where <name> defaults to
9812 // `<table>_<col1>_<col2>_uq` when not supplied.
9813 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9814 out.indexes.push(IndexAttr {
9815 name,
9816 columns,
9817 unique: true,
9818 method: "btree".to_owned(),
9819 where_clause: None,
9820 include: Vec::new(),
9821 });
9822 return Ok(());
9823 }
9824 if meta.path.is_ident("index_together") {
9825 // Composite (non-unique) index. Two syntaxes
9826 // mirroring `unique_together`.
9827 //
9828 // #[rustango(index_together = "created_at, status")]
9829 // #[rustango(index_together(columns = "created_at, status", name = "x"))]
9830 let (columns, name) = parse_together_attr(&meta, "index_together")?;
9831 out.indexes.push(IndexAttr {
9832 name,
9833 columns,
9834 unique: false,
9835 method: "btree".to_owned(),
9836 where_clause: None,
9837 include: Vec::new(),
9838 });
9839 return Ok(());
9840 }
9841 if meta.path.is_ident("unique_when") {
9842 // Partial unique index. Issue #265 / T1.3.
9843 //
9844 // #[rustango(unique_when(
9845 // columns = "email",
9846 // condition = "deleted_at IS NULL",
9847 // name = "unique_active_email"
9848 // ))]
9849 //
9850 // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9851 // on PG / SQLite (both ship partial indexes natively).
9852 // MySQL falls back to a plain UNIQUE index — the
9853 // condition is lost; document the limitation in the
9854 // generated migration.
9855 let mut columns: Option<Vec<String>> = None;
9856 let mut condition: Option<String> = None;
9857 let mut name: Option<String> = None;
9858 let mut include: Vec<String> = Vec::new();
9859 meta.parse_nested_meta(|inner| {
9860 if inner.path.is_ident("columns") {
9861 let s: LitStr = inner.value()?.parse()?;
9862 columns = Some(split_field_list(&s.value()));
9863 return Ok(());
9864 }
9865 if inner.path.is_ident("condition") {
9866 let s: LitStr = inner.value()?.parse()?;
9867 condition = Some(s.value());
9868 return Ok(());
9869 }
9870 if inner.path.is_ident("name") {
9871 let s: LitStr = inner.value()?.parse()?;
9872 name = Some(s.value());
9873 return Ok(());
9874 }
9875 if inner.path.is_ident("include") {
9876 // PG 11+ covering-index columns. Non-key columns
9877 // travel with the index leaf for index-only
9878 // scans. Dropped on MySQL/SQLite by the writer.
9879 let s: LitStr = inner.value()?.parse()?;
9880 include = split_field_list(&s.value());
9881 return Ok(());
9882 }
9883 Err(inner.error(
9884 "unknown unique_when attribute (supported: \
9885 `columns = \"...\"`, `condition = \"...\"`, \
9886 `name = \"...\"`, `include = \"...\"`)",
9887 ))
9888 })?;
9889 let columns = columns.ok_or_else(|| {
9890 meta.error("`unique_when(...)` requires `columns = \"...\"`")
9891 })?;
9892 let condition = condition.ok_or_else(|| {
9893 meta.error("`unique_when(...)` requires `condition = \"...\"`")
9894 })?;
9895 if columns.is_empty() {
9896 return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9897 }
9898 out.indexes.push(IndexAttr {
9899 name,
9900 columns,
9901 unique: true,
9902 method: "btree".to_owned(),
9903 where_clause: Some(condition),
9904 include,
9905 });
9906 return Ok(());
9907 }
9908 if meta.path.is_ident("index_when") {
9909 // Non-unique partial index. Sibling of `unique_when`
9910 // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9911 //
9912 // #[rustango(index_when(
9913 // columns = "status, created_at",
9914 // condition = "deleted_at IS NULL",
9915 // name = "active_status_created_idx"
9916 // ))]
9917 //
9918 // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9919 // on PG / SQLite (both ship partial indexes natively).
9920 // MySQL has no native partial-index support — the writer
9921 // emits a plain CREATE INDEX and the condition is lost;
9922 // operators wanting that selectivity on MySQL should
9923 // declare a covering index plus an application-level
9924 // filter.
9925 let mut columns: Option<Vec<String>> = None;
9926 let mut condition: Option<String> = None;
9927 let mut name: Option<String> = None;
9928 let mut method: String = "btree".to_owned();
9929 let mut include: Vec<String> = Vec::new();
9930 meta.parse_nested_meta(|inner| {
9931 if inner.path.is_ident("columns") {
9932 let s: LitStr = inner.value()?.parse()?;
9933 columns = Some(split_field_list(&s.value()));
9934 return Ok(());
9935 }
9936 if inner.path.is_ident("condition") {
9937 let s: LitStr = inner.value()?.parse()?;
9938 condition = Some(s.value());
9939 return Ok(());
9940 }
9941 if inner.path.is_ident("name") {
9942 let s: LitStr = inner.value()?.parse()?;
9943 name = Some(s.value());
9944 return Ok(());
9945 }
9946 if inner.path.is_ident("method") {
9947 let s: LitStr = inner.value()?.parse()?;
9948 method = s.value();
9949 return Ok(());
9950 }
9951 if inner.path.is_ident("include") {
9952 // PG 11+ covering-index columns; non-key
9953 // columns travel with the index leaf. Dropped
9954 // on MySQL/SQLite.
9955 let s: LitStr = inner.value()?.parse()?;
9956 include = split_field_list(&s.value());
9957 return Ok(());
9958 }
9959 Err(inner.error(
9960 "unknown index_when attribute (supported: \
9961 `columns = \"...\"`, `condition = \"...\"`, \
9962 `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9963 `include = \"...\"`)",
9964 ))
9965 })?;
9966 let columns = columns
9967 .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9968 let condition = condition.ok_or_else(|| {
9969 meta.error("`index_when(...)` requires `condition = \"...\"`")
9970 })?;
9971 if columns.is_empty() {
9972 return Err(meta.error("`index_when(columns = \"\")` is empty"));
9973 }
9974 out.indexes.push(IndexAttr {
9975 name,
9976 columns,
9977 unique: false,
9978 method,
9979 where_clause: Some(condition),
9980 include,
9981 });
9982 return Ok(());
9983 }
9984 if meta.path.is_ident("index") {
9985 // Container-level composite index. Two syntaxes:
9986 // #[rustango(index = "col1, col2")] — bare, non-unique btree
9987 // #[rustango(index("col1, col2"))] — call form (same result)
9988 // #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
9989 // The call form takes the column list as a leading string
9990 // literal, then optional `unique` / `name = "..."` /
9991 // `method = "..."` flags (a leading literal can't compose
9992 // under `parse_nested_meta`, so the paren body is parsed by
9993 // hand). `unique_together` / `index_together` remain
9994 // aliases for the same feature.
9995 let cols_lit: LitStr;
9996 let mut unique = false;
9997 let mut name: Option<String> = None;
9998 let mut method = "btree".to_owned();
9999 if meta.input.peek(syn::token::Paren) {
10000 let content;
10001 syn::parenthesized!(content in meta.input);
10002 cols_lit = content.parse()?;
10003 while content.peek(syn::Token![,]) {
10004 content.parse::<syn::Token![,]>()?;
10005 if content.is_empty() {
10006 break;
10007 }
10008 let flag: syn::Ident = content.parse()?;
10009 if flag == "unique" {
10010 unique = true;
10011 } else if flag == "name" {
10012 content.parse::<syn::Token![=]>()?;
10013 let s: LitStr = content.parse()?;
10014 name = Some(s.value());
10015 } else if flag == "method" {
10016 content.parse::<syn::Token![=]>()?;
10017 let s: LitStr = content.parse()?;
10018 let v = s.value();
10019 match v.as_str() {
10020 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
10021 | "bloom" => method = v,
10022 other => {
10023 return Err(syn::Error::new(
10024 s.span(),
10025 format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
10026 ));
10027 }
10028 }
10029 } else {
10030 return Err(syn::Error::new(
10031 flag.span(),
10032 "unknown index flag (supported: `unique`, `name`, `method`)",
10033 ));
10034 }
10035 }
10036 } else {
10037 cols_lit = meta.value()?.parse()?;
10038 }
10039 let columns = split_field_list(&cols_lit.value());
10040 out.indexes.push(IndexAttr {
10041 name,
10042 columns,
10043 unique,
10044 method,
10045 where_clause: None,
10046 include: Vec::new(),
10047 });
10048 return Ok(());
10049 }
10050 if meta.path.is_ident("check") {
10051 // #[rustango(check(name = "…", expr = "…"))]
10052 let mut name: Option<String> = None;
10053 let mut expr: Option<String> = None;
10054 meta.parse_nested_meta(|inner| {
10055 if inner.path.is_ident("name") {
10056 let s: LitStr = inner.value()?.parse()?;
10057 name = Some(s.value());
10058 return Ok(());
10059 }
10060 if inner.path.is_ident("expr") {
10061 let s: LitStr = inner.value()?.parse()?;
10062 expr = Some(s.value());
10063 return Ok(());
10064 }
10065 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
10066 })?;
10067 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
10068 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
10069 out.checks.push(CheckAttr { name, expr });
10070 return Ok(());
10071 }
10072 if meta.path.is_ident("exclude") {
10073 // #[rustango(exclude(name = "…", using = "gist",
10074 // elements = "col WITH op, col WITH op",
10075 // where = "…"))]
10076 let mut name: Option<String> = None;
10077 let mut using: Option<String> = None;
10078 let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
10079 let mut where_clause: Option<String> = None;
10080 meta.parse_nested_meta(|inner| {
10081 if inner.path.is_ident("name") {
10082 let s: LitStr = inner.value()?.parse()?;
10083 name = Some(s.value());
10084 return Ok(());
10085 }
10086 if inner.path.is_ident("using") {
10087 let s: LitStr = inner.value()?.parse()?;
10088 using = Some(s.value());
10089 return Ok(());
10090 }
10091 if inner.path.is_ident("elements") {
10092 let s: LitStr = inner.value()?.parse()?;
10093 elements_raw = Some((s.value(), s.span()));
10094 return Ok(());
10095 }
10096 if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
10097 let s: LitStr = inner.value()?.parse()?;
10098 where_clause = Some(s.value());
10099 return Ok(());
10100 }
10101 Err(inner.error(
10102 "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
10103 ))
10104 })?;
10105 let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
10106 let using = using.unwrap_or_else(|| "gist".to_owned());
10107 let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
10108 meta.error(
10109 "exclude requires `elements = \"col WITH op, col WITH op\"`",
10110 )
10111 })?;
10112 // Parse `col WITH op` pairs separated by commas.
10113 let mut elements: Vec<(String, String)> = Vec::new();
10114 for pair in elements_str.split(',') {
10115 let pair = pair.trim();
10116 if pair.is_empty() {
10117 continue;
10118 }
10119 let mut split = pair.splitn(2, |c: char| c.is_whitespace());
10120 let col = split.next().unwrap_or("").trim();
10121 let rest = split.next().unwrap_or("").trim();
10122 // `WITH op` — case-insensitive on `WITH`, then op.
10123 let rest_lc = rest.to_ascii_lowercase();
10124 let op = rest_lc
10125 .strip_prefix("with")
10126 .map(|r| r.trim_start())
10127 .filter(|r| !r.is_empty())
10128 .map(|_| {
10129 // Pull the original-case op from `rest` after the
10130 // `WITH ` token (5 chars).
10131 rest[4..].trim_start().to_owned()
10132 });
10133 let Some(op) = op else {
10134 return Err(syn::Error::new(
10135 elements_span,
10136 format!(
10137 "exclude elements: `{pair}` must be `<col> WITH <op>` \
10138 (e.g. `room_id WITH =` or `during WITH &&`)"
10139 ),
10140 ));
10141 };
10142 if col.is_empty() || op.is_empty() {
10143 return Err(syn::Error::new(
10144 elements_span,
10145 format!(
10146 "exclude elements: `{pair}` must be `<col> WITH <op>` \
10147 (both sides non-empty)"
10148 ),
10149 ));
10150 }
10151 elements.push((col.to_owned(), op));
10152 }
10153 if elements.is_empty() {
10154 return Err(syn::Error::new(
10155 elements_span,
10156 "exclude requires at least one `col WITH op` element",
10157 ));
10158 }
10159 out.excludes.push(ExcludeAttr {
10160 name,
10161 using,
10162 elements,
10163 where_clause,
10164 });
10165 return Ok(());
10166 }
10167 if meta.path.is_ident("generic_fk") {
10168 let mut gfk = GenericFkAttr {
10169 name: String::new(),
10170 ct_column: String::new(),
10171 pk_column: String::new(),
10172 };
10173 meta.parse_nested_meta(|inner| {
10174 if inner.path.is_ident("name") {
10175 let s: LitStr = inner.value()?.parse()?;
10176 gfk.name = s.value();
10177 return Ok(());
10178 }
10179 if inner.path.is_ident("ct_column") {
10180 let s: LitStr = inner.value()?.parse()?;
10181 gfk.ct_column = s.value();
10182 return Ok(());
10183 }
10184 if inner.path.is_ident("pk_column") {
10185 let s: LitStr = inner.value()?.parse()?;
10186 gfk.pk_column = s.value();
10187 return Ok(());
10188 }
10189 Err(inner.error(
10190 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10191 ))
10192 })?;
10193 if gfk.name.is_empty() {
10194 return Err(meta.error("generic_fk requires `name = \"...\"`"));
10195 }
10196 if gfk.ct_column.is_empty() {
10197 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10198 }
10199 if gfk.pk_column.is_empty() {
10200 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10201 }
10202 out.generic_fks.push(gfk);
10203 return Ok(());
10204 }
10205 if meta.path.is_ident("fk_composite") {
10206 let mut fk = CompositeFkAttr {
10207 name: String::new(),
10208 to: String::new(),
10209 from: Vec::new(),
10210 on: Vec::new(),
10211 };
10212 meta.parse_nested_meta(|inner| {
10213 if inner.path.is_ident("name") {
10214 let s: LitStr = inner.value()?.parse()?;
10215 fk.name = s.value();
10216 return Ok(());
10217 }
10218 if inner.path.is_ident("to") {
10219 let s: LitStr = inner.value()?.parse()?;
10220 fk.to = s.value();
10221 return Ok(());
10222 }
10223 // `on = ("col1", "col2", ...)` — parse a parenthesised
10224 // comma-list of string literals.
10225 if inner.path.is_ident("on") || inner.path.is_ident("from") {
10226 let value = inner.value()?;
10227 let content;
10228 syn::parenthesized!(content in value);
10229 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10230 content.parse_terminated(
10231 |p| p.parse::<syn::LitStr>(),
10232 syn::Token![,],
10233 )?;
10234 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10235 if inner.path.is_ident("on") {
10236 fk.on = cols;
10237 } else {
10238 fk.from = cols;
10239 }
10240 return Ok(());
10241 }
10242 Err(inner.error(
10243 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10244 ))
10245 })?;
10246 if fk.name.is_empty() {
10247 return Err(meta.error("fk_composite requires `name = \"...\"`"));
10248 }
10249 if fk.to.is_empty() {
10250 return Err(meta.error("fk_composite requires `to = \"...\"`"));
10251 }
10252 if fk.from.is_empty() || fk.on.is_empty() {
10253 return Err(meta.error(
10254 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10255 ));
10256 }
10257 if fk.from.len() != fk.on.len() {
10258 return Err(meta.error(format!(
10259 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10260 fk.from.len(),
10261 fk.on.len(),
10262 )));
10263 }
10264 out.composite_fks.push(fk);
10265 return Ok(());
10266 }
10267 if meta.path.is_ident("m2m") {
10268 let mut m2m = M2MAttr {
10269 name: String::new(),
10270 to: String::new(),
10271 through: String::new(),
10272 src: String::new(),
10273 dst: String::new(),
10274 auto_create: true,
10275 };
10276 meta.parse_nested_meta(|inner| {
10277 if inner.path.is_ident("name") {
10278 let s: LitStr = inner.value()?.parse()?;
10279 m2m.name = s.value();
10280 return Ok(());
10281 }
10282 if inner.path.is_ident("to") {
10283 let s: LitStr = inner.value()?.parse()?;
10284 m2m.to = s.value();
10285 return Ok(());
10286 }
10287 if inner.path.is_ident("through") {
10288 let s: LitStr = inner.value()?.parse()?;
10289 m2m.through = s.value();
10290 return Ok(());
10291 }
10292 if inner.path.is_ident("src") {
10293 let s: LitStr = inner.value()?.parse()?;
10294 m2m.src = s.value();
10295 return Ok(());
10296 }
10297 if inner.path.is_ident("dst") {
10298 let s: LitStr = inner.value()?.parse()?;
10299 m2m.dst = s.value();
10300 return Ok(());
10301 }
10302 if inner.path.is_ident("auto_create") {
10303 let lit: syn::LitBool = inner.value()?.parse()?;
10304 m2m.auto_create = lit.value;
10305 return Ok(());
10306 }
10307 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10308 })?;
10309 if m2m.name.is_empty() {
10310 return Err(meta.error("m2m requires `name = \"...\"`"));
10311 }
10312 if m2m.to.is_empty() {
10313 return Err(meta.error("m2m requires `to = \"...\"`"));
10314 }
10315 if m2m.through.is_empty() {
10316 return Err(meta.error("m2m requires `through = \"...\"`"));
10317 }
10318 if m2m.src.is_empty() {
10319 return Err(meta.error("m2m requires `src = \"...\"`"));
10320 }
10321 if m2m.dst.is_empty() {
10322 return Err(meta.error("m2m requires `dst = \"...\"`"));
10323 }
10324 out.m2m.push(m2m);
10325 return Ok(());
10326 }
10327 if meta.path.is_ident("generic_m2m") {
10328 let mut gm = GenericM2MAttr {
10329 name: String::new(),
10330 through: String::new(),
10331 pk_column: String::new(),
10332 ct_column: String::new(),
10333 related_column: String::new(),
10334 };
10335 meta.parse_nested_meta(|inner| {
10336 let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10337 let s: LitStr = inner.value()?.parse()?;
10338 Ok(s.value())
10339 };
10340 if inner.path.is_ident("name") {
10341 gm.name = field(&inner)?;
10342 return Ok(());
10343 }
10344 if inner.path.is_ident("through") {
10345 gm.through = field(&inner)?;
10346 return Ok(());
10347 }
10348 if inner.path.is_ident("pk_column") {
10349 gm.pk_column = field(&inner)?;
10350 return Ok(());
10351 }
10352 if inner.path.is_ident("ct_column") {
10353 gm.ct_column = field(&inner)?;
10354 return Ok(());
10355 }
10356 if inner.path.is_ident("related_column") {
10357 gm.related_column = field(&inner)?;
10358 return Ok(());
10359 }
10360 Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10361 })?;
10362 for (val, label) in [
10363 (&gm.name, "name"),
10364 (&gm.through, "through"),
10365 (&gm.pk_column, "pk_column"),
10366 (&gm.ct_column, "ct_column"),
10367 (&gm.related_column, "related_column"),
10368 ] {
10369 if val.is_empty() {
10370 return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10371 }
10372 }
10373 out.generic_m2m.push(gm);
10374 return Ok(());
10375 }
10376 Err(meta.error("unknown rustango container attribute"))
10377 })?;
10378 }
10379 Ok(out)
10380}
10381
10382/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10383/// owned field names, trimming whitespace and skipping empty entries.
10384/// Field-name validation against the model is done by the caller.
10385fn split_field_list(raw: &str) -> Vec<String> {
10386 raw.split(',')
10387 .map(str::trim)
10388 .filter(|s| !s.is_empty())
10389 .map(str::to_owned)
10390 .collect()
10391}
10392
10393/// Shared parser for `unique_together` and `index_together` container
10394/// attrs. Accepts both shapes:
10395///
10396/// * `attr = "col1, col2"` — auto-derived index name.
10397/// * `attr(columns = "col1, col2", name = "...")` — explicit name.
10398///
10399/// Returns `(columns, name)`.
10400fn parse_together_attr(
10401 meta: &syn::meta::ParseNestedMeta<'_>,
10402 attr: &str,
10403) -> syn::Result<(Vec<String>, Option<String>)> {
10404 // Disambiguate by whether the next token is `=` (key-value) or
10405 // `(` (parenthesized).
10406 if meta.input.peek(syn::Token![=]) {
10407 let cols_lit: LitStr = meta.value()?.parse()?;
10408 let columns = split_field_list(&cols_lit.value());
10409 check_together_columns(meta, attr, &columns)?;
10410 return Ok((columns, None));
10411 }
10412 let mut columns: Option<Vec<String>> = None;
10413 let mut name: Option<String> = None;
10414 meta.parse_nested_meta(|inner| {
10415 if inner.path.is_ident("columns") {
10416 let s: LitStr = inner.value()?.parse()?;
10417 columns = Some(split_field_list(&s.value()));
10418 return Ok(());
10419 }
10420 if inner.path.is_ident("name") {
10421 let s: LitStr = inner.value()?.parse()?;
10422 name = Some(s.value());
10423 return Ok(());
10424 }
10425 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10426 })?;
10427 let columns = columns.ok_or_else(|| {
10428 meta.error(format!(
10429 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10430 ))
10431 })?;
10432 check_together_columns(meta, attr, &columns)?;
10433 Ok((columns, name))
10434}
10435
10436fn check_together_columns(
10437 meta: &syn::meta::ParseNestedMeta<'_>,
10438 attr: &str,
10439 columns: &[String],
10440) -> syn::Result<()> {
10441 if columns.len() < 2 {
10442 let single = if attr == "unique_together" {
10443 "#[rustango(unique)] on the field"
10444 } else {
10445 "#[rustango(index)] on the field"
10446 };
10447 return Err(meta.error(format!(
10448 "{attr} expects two or more columns; for a single-column equivalent use {single}",
10449 )));
10450 }
10451 Ok(())
10452}
10453
10454/// Parse the fieldsets DSL: pipe-separated sections, optional
10455/// `"Title:"` prefix on each, comma-separated field names after.
10456/// Examples:
10457/// * `"name, office"` → one untitled section with two fields
10458/// * `"Identity: name, office | Metadata: created_at"` → two titled
10459/// sections
10460///
10461/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10462fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10463 raw.split('|')
10464 .map(str::trim)
10465 .filter(|s| !s.is_empty())
10466 .map(|section| {
10467 // Split off an optional `Title:` prefix (first colon).
10468 let (title, rest) = match section.split_once(':') {
10469 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10470 _ => (String::new(), section),
10471 };
10472 let fields = split_field_list(rest);
10473 (title, fields)
10474 })
10475 .collect()
10476}
10477
10478/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10479/// comma-separated entry maps a target field to one or more source
10480/// fields joined with `+`. Whitespace around tokens is trimmed.
10481/// Entries missing `:` or with empty target/source lists are dropped.
10482fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10483 raw.split(',')
10484 .map(str::trim)
10485 .filter(|s| !s.is_empty())
10486 .filter_map(|entry| {
10487 let (target, sources_raw) = entry.split_once(':')?;
10488 let target = target.trim().to_owned();
10489 if target.is_empty() {
10490 return None;
10491 }
10492 let sources: Vec<String> = sources_raw
10493 .split('+')
10494 .map(|s| s.trim().to_owned())
10495 .filter(|s| !s.is_empty())
10496 .collect();
10497 if sources.is_empty() {
10498 return None;
10499 }
10500 Some((target, sources))
10501 })
10502 .collect()
10503}
10504
10505/// Parse `formfield_overrides` — `"field:widget,field2:widget2"`
10506/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10507/// and empty halves drop silently — the macro layer only enforces shape,
10508/// not field-name vs. widget-name validity (those checks happen at
10509/// `AdminConfig` consumption time). Issue #359.
10510fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10511 raw.split(',')
10512 .map(str::trim)
10513 .filter(|s| !s.is_empty())
10514 .filter_map(|entry| {
10515 let (field, widget) = entry.split_once(':')?;
10516 let field = field.trim().to_owned();
10517 let widget = widget.trim().to_owned();
10518 if field.is_empty() || widget.is_empty() {
10519 return None;
10520 }
10521 Some((field, widget))
10522 })
10523 .collect()
10524}
10525
10526/// Parse an ordering list — `"name"` is ASC, `"-name"` is DESC.
10527/// Returns `(field_name, desc)` pairs in the same order as the input.
10528fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10529 raw.split(',')
10530 .map(str::trim)
10531 .filter(|s| !s.is_empty())
10532 .map(|spec| {
10533 spec.strip_prefix('-')
10534 .map_or((spec.to_owned(), false), |rest| {
10535 (rest.trim().to_owned(), true)
10536 })
10537 })
10538 .collect()
10539}
10540
10541struct FieldAttrs {
10542 column: Option<String>,
10543 primary_key: bool,
10544 fk: Option<String>,
10545 o2o: Option<String>,
10546 on: Option<String>,
10547 /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10548 /// "set_default" | "no_action")]` — what happens to this row when
10549 /// the row it points at is deleted.
10550 /// Only meaningful when `fk` / `o2o` is
10551 /// also set; the macro errors at compile time if applied to a
10552 /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10553 /// DDL writer renders `ON DELETE <action>` after the constraint
10554 /// clause when this is `Some`; `None` falls back to the database
10555 /// default (NO ACTION on every backend rustango supports).
10556 on_delete: Option<String>,
10557 /// `#[rustango(related_name = "...")]` — per-FK
10558 /// reverse-accessor override. When set, the derive emits
10559 /// `Parent::<related_name>[_pool]` instead of the container-level
10560 /// `default_related_name` or the `<child_snake>_set[_pool]`
10561 /// fallback. Only meaningful when `fk` / `o2o` is also set;
10562 /// silently ignored on non-FK fields. Follow-up to #816.
10563 related_name: Option<String>,
10564 max_length: Option<u32>,
10565 /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10566 /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10567 /// unconstrained `vector` column.
10568 vector_dims: Option<u32>,
10569 /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10570 /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10571 /// unconstrained `geometry(Point)` column (SRID 0).
10572 geometry_srid: Option<u32>,
10573 min: Option<i64>,
10574 max: Option<i64>,
10575 default: Option<String>,
10576 /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10577 /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10578 /// "gen_random_uuid()"`. The Rust field type must be
10579 /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10580 /// INSERTs so the DB DEFAULT fires.
10581 auto_uuid: bool,
10582 /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10583 /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10584 /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10585 /// the field is `Auto::Unset`, then bound as a normal parameter
10586 /// rather than relying on a per-dialect DB function. Issue #823
10587 /// (Eloquent `HasUuids`).
10588 ///
10589 /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10590 /// Composes with every backend (PG / MySQL / SQLite) — no
10591 /// `gen_random_uuid()` requirement on the database.
10592 default_uuid_v7: bool,
10593 /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10594 /// Server-set on insert, immutable from app code afterwards.
10595 /// Implies `auto + default = "now()"`. Field type must be
10596 /// `DateTime<Utc>`.
10597 auto_now_add: bool,
10598 /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10599 /// every insert AND every update. Implies `auto + default =
10600 /// "now()"`; the macro additionally rewrites `update_on` /
10601 /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10602 /// field value.
10603 auto_now: bool,
10604 /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10605 /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10606 /// `soft_delete_on(executor)` and `restore_on(executor)`
10607 /// methods on the model.
10608 soft_delete: bool,
10609 /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10610 /// the column in the generated DDL.
10611 unique: bool,
10612 /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10613 /// generates a `CREATE INDEX` for this column. `unique` here means
10614 /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10615 index: bool,
10616 index_unique: bool,
10617 index_name: Option<String>,
10618 /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10619 /// `"btree"`. Issue #34.
10620 index_method: String,
10621 /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10622 /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10623 /// the macro skips this column from every INSERT and UPDATE
10624 /// path, so the database always recomputes the value from
10625 /// `EXPR`. Backlog item #35.
10626 generated_as: Option<String>,
10627 /// `#[rustango(help_text = "…")]` — help text
10628 /// rendered below the admin form's input. Threaded into
10629 /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10630 /// layers can read it.
10631 help_text: Option<String>,
10632 /// `#[rustango(choices = "value:Label, value:Label")]` — the
10633 /// enumerated allowed values. Threaded into `FieldSchema::choices`
10634 /// as a `&'static [(&'static str, &'static str)]` slice. When
10635 /// present, the admin form renders a `<select>` instead of `<input>`
10636 /// and the validator rejects values not in the list. Only meaningful
10637 /// for `FieldType::String`; the macro errors at compile time if
10638 /// applied to a non-string field.
10639 choices: Option<Vec<(String, String)>>,
10640 /// `#[rustango(db_comment = "…")]` — DB-side column
10641 /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10642 /// the comment in CREATE TABLE; Postgres emits a separate
10643 /// `COMMENT ON COLUMN` statement after the table is created;
10644 /// SQLite silently drops the value (no native column comments).
10645 db_comment: Option<String>,
10646 /// `#[rustango(verbose_name = "…")]` — human-readable
10647 /// label for the field. Threaded into `FieldSchema::verbose_name`
10648 /// so admin column headers, form labels, and other display
10649 /// surfaces can prefer the friendly caption over the Rust
10650 /// identifier. `None` means renderers fall back to the field name.
10651 verbose_name: Option<String>,
10652 /// `#[rustango(editable = false)]` — opt out of
10653 /// auto-generated form rendering. Defaults to `true` so existing
10654 /// fields keep their current admin / form behavior; setting
10655 /// `false` removes the field from the admin change-form entirely
10656 /// (the value is still visible on detail / list views, just not
10657 /// editable).
10658 editable: bool,
10659 /// `#[rustango(blank)]` / `#[rustango(blank = true)]` —
10660 /// "form may submit empty even when DB is NOT NULL". Threaded into
10661 /// `FieldSchema::blank`. Defaults to `false`.
10662 blank: bool,
10663 /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10664 /// case-insensitive text column. Threaded into
10665 /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10666 /// fields; the macro errors at derive time if applied elsewhere.
10667 case_insensitive: bool,
10668 /// `#[rustango(validators = "email,url")]` — the
10669 /// model-level validator chain. Comma-separated names that
10670 /// dispatch to the `validators::*` family in `validate_value`.
10671 /// Empty by default; fires on every typed INSERT/UPDATE.
10672 validators: Vec<String>,
10673}
10674
10675fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10676 let mut out = FieldAttrs {
10677 column: None,
10678 primary_key: false,
10679 fk: None,
10680 o2o: None,
10681 on: None,
10682 on_delete: None,
10683 related_name: None,
10684 max_length: None,
10685 vector_dims: None,
10686 geometry_srid: None,
10687 min: None,
10688 max: None,
10689 default: None,
10690 auto_uuid: false,
10691 default_uuid_v7: false,
10692 auto_now_add: false,
10693 auto_now: false,
10694 soft_delete: false,
10695 unique: false,
10696 index: false,
10697 index_unique: false,
10698 index_name: None,
10699 index_method: "btree".to_owned(),
10700 generated_as: None,
10701 help_text: None,
10702 choices: None,
10703 db_comment: None,
10704 verbose_name: None,
10705 editable: true,
10706 blank: false,
10707 case_insensitive: false,
10708 validators: Vec::new(),
10709 };
10710 for attr in &field.attrs {
10711 if !attr.path().is_ident("rustango") {
10712 continue;
10713 }
10714 attr.parse_nested_meta(|meta| {
10715 if meta.path.is_ident("column") {
10716 let s: LitStr = meta.value()?.parse()?;
10717 let name = s.value();
10718 validate_sql_identifier(&name, "column", s.span())?;
10719 out.column = Some(name);
10720 return Ok(());
10721 }
10722 if meta.path.is_ident("primary_key") {
10723 out.primary_key = true;
10724 return Ok(());
10725 }
10726 if meta.path.is_ident("fk") {
10727 let s: LitStr = meta.value()?.parse()?;
10728 out.fk = Some(s.value());
10729 return Ok(());
10730 }
10731 if meta.path.is_ident("o2o") {
10732 let s: LitStr = meta.value()?.parse()?;
10733 out.o2o = Some(s.value());
10734 return Ok(());
10735 }
10736 if meta.path.is_ident("on") {
10737 let s: LitStr = meta.value()?.parse()?;
10738 out.on = Some(s.value());
10739 return Ok(());
10740 }
10741 if meta.path.is_ident("on_delete") {
10742 let s: LitStr = meta.value()?.parse()?;
10743 let raw = s.value();
10744 let normalized = raw.trim().to_ascii_lowercase();
10745 // Validate at parse time so the user gets a clear span
10746 // rather than a downstream compile error in the emit.
10747 match normalized.as_str() {
10748 "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10749 _ => {
10750 return Err(syn::Error::new(
10751 s.span(),
10752 format!(
10753 "unknown on_delete action `{raw}`; expected one of \
10754 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10755 ),
10756 ));
10757 }
10758 }
10759 out.on_delete = Some(normalized);
10760 return Ok(());
10761 }
10762 if meta.path.is_ident("related_name") {
10763 let s: LitStr = meta.value()?.parse()?;
10764 let raw = s.value();
10765 if raw.trim().is_empty() {
10766 return Err(syn::Error::new(
10767 s.span(),
10768 "`related_name` must be a non-empty identifier",
10769 ));
10770 }
10771 // Validate as a Rust-ident shape — the value becomes
10772 // a method name on the parent type. Same rule as
10773 // `default_related_name` so the two surfaces match.
10774 if !raw
10775 .chars()
10776 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10777 || raw.starts_with(char::is_numeric)
10778 {
10779 return Err(syn::Error::new(
10780 s.span(),
10781 "`related_name` must be snake_case ASCII (lowercase letters, \
10782 digits, underscores; no leading digit)",
10783 ));
10784 }
10785 out.related_name = Some(raw);
10786 return Ok(());
10787 }
10788 if meta.path.is_ident("max_length") {
10789 let lit: syn::LitInt = meta.value()?.parse()?;
10790 out.max_length = Some(lit.base10_parse::<u32>()?);
10791 return Ok(());
10792 }
10793 // `#[rustango(vector(dims = N))]` — pgvector column
10794 // dimension (#824). Nested-meta form so it reads like the
10795 // other typed-column attrs.
10796 if meta.path.is_ident("vector") {
10797 meta.parse_nested_meta(|inner| {
10798 if inner.path.is_ident("dims") {
10799 let lit: syn::LitInt = inner.value()?.parse()?;
10800 out.vector_dims = Some(lit.base10_parse::<u32>()?);
10801 return Ok(());
10802 }
10803 Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10804 })?;
10805 return Ok(());
10806 }
10807 // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10808 // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10809 if meta.path.is_ident("geometry") {
10810 meta.parse_nested_meta(|inner| {
10811 if inner.path.is_ident("srid") {
10812 let lit: syn::LitInt = inner.value()?.parse()?;
10813 out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10814 return Ok(());
10815 }
10816 Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10817 })?;
10818 return Ok(());
10819 }
10820 if meta.path.is_ident("min") {
10821 out.min = Some(parse_signed_i64(&meta)?);
10822 return Ok(());
10823 }
10824 if meta.path.is_ident("max") {
10825 out.max = Some(parse_signed_i64(&meta)?);
10826 return Ok(());
10827 }
10828 if meta.path.is_ident("default") {
10829 let s: LitStr = meta.value()?.parse()?;
10830 out.default = Some(s.value());
10831 return Ok(());
10832 }
10833 if meta.path.is_ident("generated_as") {
10834 let s: LitStr = meta.value()?.parse()?;
10835 out.generated_as = Some(s.value());
10836 return Ok(());
10837 }
10838 if meta.path.is_ident("help_text") {
10839 let s: LitStr = meta.value()?.parse()?;
10840 out.help_text = Some(s.value());
10841 return Ok(());
10842 }
10843 if meta.path.is_ident("choices") {
10844 let s: LitStr = meta.value()?.parse()?;
10845 let raw = s.value();
10846 let mut pairs: Vec<(String, String)> = Vec::new();
10847 for chunk in raw.split(',') {
10848 let chunk = chunk.trim();
10849 if chunk.is_empty() {
10850 continue;
10851 }
10852 let (value, label) = match chunk.split_once(':') {
10853 Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10854 None => (chunk.to_owned(), chunk.to_owned()),
10855 };
10856 if value.is_empty() {
10857 return Err(syn::Error::new(
10858 s.span(),
10859 "`choices` entry has empty value before `:`",
10860 ));
10861 }
10862 pairs.push((value, label));
10863 }
10864 if pairs.is_empty() {
10865 return Err(syn::Error::new(
10866 s.span(),
10867 "`choices = \"…\"` must contain at least one value",
10868 ));
10869 }
10870 out.choices = Some(pairs);
10871 return Ok(());
10872 }
10873 if meta.path.is_ident("db_comment") {
10874 let s: LitStr = meta.value()?.parse()?;
10875 out.db_comment = Some(s.value());
10876 return Ok(());
10877 }
10878 if meta.path.is_ident("verbose_name") {
10879 let s: LitStr = meta.value()?.parse()?;
10880 out.verbose_name = Some(s.value());
10881 return Ok(());
10882 }
10883 if meta.path.is_ident("editable") {
10884 // Two forms accepted:
10885 // #[rustango(editable = false)] / true — explicit
10886 // #[rustango(editable)] — flag form (= true, the
10887 // default, so harmless; included for symmetry)
10888 if let Ok(v) = meta.value() {
10889 let lit: syn::LitBool = v.parse()?;
10890 out.editable = lit.value;
10891 } else {
10892 out.editable = true;
10893 }
10894 return Ok(());
10895 }
10896 if meta.path.is_ident("blank") {
10897 // Two forms accepted:
10898 // #[rustango(blank)] — flag form, true
10899 // #[rustango(blank = false)] / true — explicit
10900 if let Ok(v) = meta.value() {
10901 let lit: syn::LitBool = v.parse()?;
10902 out.blank = lit.value;
10903 } else {
10904 out.blank = true;
10905 }
10906 return Ok(());
10907 }
10908 if meta.path.is_ident("citext") {
10909 // Case-insensitive text column (#344). Two forms:
10910 // #[rustango(citext)] — flag form, true
10911 // #[rustango(citext = true)] — explicit
10912 // #[rustango(citext = false)] — explicit opt-out
10913 // String-only validation lives in the field-type
10914 // emitter (the FieldType discriminant is computed in
10915 // `detect_type`); the macro records the flag and the
10916 // DDL writer emits dialect-specific COLLATE / CITEXT.
10917 if let Ok(v) = meta.value() {
10918 let lit: syn::LitBool = v.parse()?;
10919 out.case_insensitive = lit.value;
10920 } else {
10921 out.case_insensitive = true;
10922 }
10923 return Ok(());
10924 }
10925 if meta.path.is_ident("validators") {
10926 let s: LitStr = meta.value()?.parse()?;
10927 let raw = s.value();
10928 out.validators = raw
10929 .split(',')
10930 .map(str::trim)
10931 .filter(|s| !s.is_empty())
10932 .map(str::to_owned)
10933 .collect();
10934 if out.validators.is_empty() {
10935 return Err(syn::Error::new(
10936 s.span(),
10937 "`validators = \"…\"` must list at least one name",
10938 ));
10939 }
10940 return Ok(());
10941 }
10942 if meta.path.is_ident("auto_uuid") {
10943 out.auto_uuid = true;
10944 // Implied: PK + auto + DEFAULT gen_random_uuid().
10945 // Each is also explicitly settable; the explicit
10946 // value wins if conflicting.
10947 out.primary_key = true;
10948 if out.default.is_none() {
10949 out.default = Some("gen_random_uuid()".into());
10950 }
10951 return Ok(());
10952 }
10953 if meta.path.is_ident("default_uuid_v7") {
10954 // Backend-neutral counterpart of `auto_uuid` — issue #823.
10955 // No SQL DEFAULT (the macro fills the value Rust-side
10956 // before binding); just mark the field as PK + Auto
10957 // so the insert path is the `Auto::Unset → generate`
10958 // branch.
10959 out.default_uuid_v7 = true;
10960 out.primary_key = true;
10961 return Ok(());
10962 }
10963 if meta.path.is_ident("auto_now_add") {
10964 out.auto_now_add = true;
10965 if out.default.is_none() {
10966 out.default = Some("now()".into());
10967 }
10968 return Ok(());
10969 }
10970 if meta.path.is_ident("auto_now") {
10971 out.auto_now = true;
10972 if out.default.is_none() {
10973 out.default = Some("now()".into());
10974 }
10975 return Ok(());
10976 }
10977 if meta.path.is_ident("soft_delete") {
10978 out.soft_delete = true;
10979 return Ok(());
10980 }
10981 if meta.path.is_ident("unique") {
10982 out.unique = true;
10983 return Ok(());
10984 }
10985 if meta.path.is_ident("index") {
10986 out.index = true;
10987 // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
10988 if meta.input.peek(syn::token::Paren) {
10989 meta.parse_nested_meta(|inner| {
10990 if inner.path.is_ident("unique") {
10991 out.index_unique = true;
10992 return Ok(());
10993 }
10994 if inner.path.is_ident("name") {
10995 let s: LitStr = inner.value()?.parse()?;
10996 out.index_name = Some(s.value());
10997 return Ok(());
10998 }
10999 if inner.path.is_ident("method") {
11000 let s: LitStr = inner.value()?.parse()?;
11001 let v = s.value();
11002 match v.as_str() {
11003 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
11004 out.index_method = v;
11005 }
11006 other => {
11007 return Err(inner.error(format!(
11008 "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
11009 )));
11010 }
11011 }
11012 return Ok(());
11013 }
11014 Err(inner.error(
11015 "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
11016 ))
11017 })?;
11018 }
11019 return Ok(());
11020 }
11021 Err(meta.error("unknown rustango field attribute"))
11022 })?;
11023 }
11024 Ok(out)
11025}
11026
11027/// Parse a signed integer literal, accepting optional leading `-`.
11028fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
11029 let expr: syn::Expr = meta.value()?.parse()?;
11030 match expr {
11031 syn::Expr::Lit(syn::ExprLit {
11032 lit: syn::Lit::Int(lit),
11033 ..
11034 }) => lit.base10_parse::<i64>(),
11035 syn::Expr::Unary(syn::ExprUnary {
11036 op: syn::UnOp::Neg(_),
11037 expr,
11038 ..
11039 }) => {
11040 if let syn::Expr::Lit(syn::ExprLit {
11041 lit: syn::Lit::Int(lit),
11042 ..
11043 }) = *expr
11044 {
11045 let v: i64 = lit.base10_parse()?;
11046 Ok(-v)
11047 } else {
11048 Err(syn::Error::new_spanned(expr, "expected integer literal"))
11049 }
11050 }
11051 other => Err(syn::Error::new_spanned(
11052 other,
11053 "expected integer literal (signed)",
11054 )),
11055 }
11056}
11057
11058struct FieldInfo<'a> {
11059 ident: &'a syn::Ident,
11060 column: String,
11061 primary_key: bool,
11062 /// `true` when the Rust type was `Auto<T>` — the INSERT path will
11063 /// skip this column when `Auto::Unset` and emit it under
11064 /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
11065 auto: bool,
11066 /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
11067 /// the `Column::Value` associated type for typed-column tokens.
11068 value_ty: &'a Type,
11069 /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
11070 field_type_tokens: TokenStream2,
11071 schema: TokenStream2,
11072 from_row_init: TokenStream2,
11073 /// Variant of [`Self::from_row_init`] that reads the column via
11074 /// `format!("{prefix}__{col}")` so a model can be decoded out of
11075 /// the aliased columns of a JOINed row. Drives slice 9.0d's
11076 /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
11077 /// helper that `select_related` calls when stitching loaded FKs.
11078 from_aliased_row_init: TokenStream2,
11079 /// MySQL twins of the two inits above. They differ only for a UUID
11080 /// column, which MySQL keeps as `CHAR(36)` text (#1733).
11081 from_row_init_my: TokenStream2,
11082 from_aliased_row_init_my: TokenStream2,
11083 /// A bare or optional `Uuid` field.
11084 uuid_column: bool,
11085 /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
11086 /// relation helper emit (`Author::<child>_set`) needs to know `T`
11087 /// to point the generated method at the right child model.
11088 fk_inner: Option<Type>,
11089 /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
11090 /// `kind` (since ForeignKey detection sets `kind` to K's
11091 /// underlying type) but stored separately for clarity at the
11092 /// `FkRelation` construction site, which only sees the FK's
11093 /// surface fields.
11094 fk_pk_kind: DetectedKind,
11095 /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
11096 /// the bare `ForeignKey<T, K>`. Routes the load_related and
11097 /// fk_pk_access emitters to wrap assignments / accessors in
11098 /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
11099 /// FK column compiles end-to-end. The DDL writer reads this off
11100 /// the field schema (`nullable` flag); the macro just needs to
11101 /// keep the Rust-side codegen consistent.
11102 nullable: bool,
11103 /// `true` when this column was marked `#[rustango(auto_now)]` —
11104 /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
11105 /// column instead of the user-supplied value, so `updated_at`
11106 /// always reflects the latest write without the caller having
11107 /// to remember to set it.
11108 auto_now: bool,
11109 /// `true` when this column was marked `#[rustango(auto_now_add)]`
11110 /// — the column is server-set on INSERT (DB DEFAULT) and
11111 /// **immutable** afterwards. `update_on` / `save_on` skip the
11112 /// column entirely so a stale `created_at` value in memory never
11113 /// rewrites the persisted timestamp.
11114 auto_now_add: bool,
11115 /// `true` when this column was marked `#[rustango(soft_delete)]`.
11116 /// Triggers emission of `soft_delete_on(executor)` and
11117 /// `restore_on(executor)` on the model's inherent impl. There is
11118 /// at most one such column per model — emission asserts this.
11119 soft_delete: bool,
11120 /// `Some` when this column was marked
11121 /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
11122 /// every INSERT and UPDATE path; the database recomputes the
11123 /// value from `EXPR`. Backlog item #35.
11124 generated_as: Option<String>,
11125 /// `true` when this column was marked
11126 /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
11127 /// emit an `insert_push` that auto-fills an `Auto::Unset` value
11128 /// with `Uuid::now_v7()` before binding, so the PK is generated
11129 /// Rust-side and the column is always present in the INSERT
11130 /// statement (no DB DEFAULT requirement). Issue #823.
11131 default_uuid_v7: bool,
11132 /// `Some` when this FK field carried `#[rustango(related_name =
11133 /// "...")]`. Threaded into [`FkRelation::related_name`] and
11134 /// consumed by [`reverse_helper_tokens`] to override the default
11135 /// `<child_snake>_set` accessor name. Follow-up to #816.
11136 related_name: Option<String>,
11137}
11138
11139/// Reject table names that won't survive SQL identifier
11140/// derivation downstream. Postgres' regular-identifier rule
11141/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
11142/// through the framework's unquoted FK / index / constraint name
11143/// emission without surprises. We also disallow leading-digit and
11144/// the empty string for clarity.
11145///
11146/// Reserved-word collisions (`select`, `from`, …) aren't flagged
11147/// here — those produce a runtime error from the SQL parser,
11148/// which is loud enough; statically enumerating reserved words
11149/// across the three supported dialects is more friction than help.
11150///
11151/// Backlog item #65.
11152fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11153 validate_sql_identifier(name, "table", span)
11154}
11155
11156/// Reject SQL identifiers that compile but break downstream SQL
11157/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11158/// `kind` is "table" / "column" — used for the error message so users
11159/// see which attribute caused the failure.
11160fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11161 if name.is_empty() {
11162 return Err(syn::Error::new(
11163 span,
11164 format!("`{kind} = \"\"` is not a valid SQL identifier"),
11165 ));
11166 }
11167 let mut chars = name.chars();
11168 let first = chars.next().unwrap();
11169 if !(first.is_ascii_alphabetic() || first == '_') {
11170 return Err(syn::Error::new(
11171 span,
11172 format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11173 ));
11174 }
11175 for c in chars {
11176 if !(c.is_ascii_alphanumeric() || c == '_') {
11177 return Err(syn::Error::new(
11178 span,
11179 format!(
11180 "{kind} name `{name}` contains invalid character {c:?} — \
11181 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11182 Hyphens in particular break FK / index name derivation \
11183 downstream; use underscores instead (e.g. `{}`)",
11184 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11185 ),
11186 ));
11187 }
11188 }
11189 Ok(())
11190}
11191
11192fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11193 let root = rustango_root();
11194 let attrs = parse_field_attrs(field)?;
11195 let ident = field
11196 .ident
11197 .as_ref()
11198 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11199 let name = ident_name(ident);
11200 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11201 let primary_key = attrs.primary_key;
11202 let DetectedType {
11203 kind,
11204 nullable,
11205 auto: detected_auto,
11206 fk_inner,
11207 } = detect_type(&field.ty)?;
11208 check_bound_compatibility(field, &attrs, kind)?;
11209 let auto = detected_auto;
11210 // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11211 // INSERT path: the user must wrap the field in `Auto<T>`, which
11212 // marks the column as DB-default-supplied. The mixin attrs then
11213 // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11214 // for `auto_now`, force the value on UPDATE too.
11215 if attrs.auto_uuid {
11216 if kind != DetectedKind::Uuid {
11217 return Err(syn::Error::new_spanned(
11218 field,
11219 "`#[rustango(auto_uuid)]` requires the field type to be \
11220 `Auto<uuid::Uuid>`",
11221 ));
11222 }
11223 if !detected_auto {
11224 return Err(syn::Error::new_spanned(
11225 field,
11226 "`#[rustango(auto_uuid)]` requires the field type to be \
11227 wrapped in `Auto<...>` so the macro skips the column on \
11228 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11229 ));
11230 }
11231 }
11232 if attrs.default_uuid_v7 {
11233 if kind != DetectedKind::Uuid {
11234 return Err(syn::Error::new_spanned(
11235 field,
11236 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11237 `Auto<uuid::Uuid>`",
11238 ));
11239 }
11240 if !detected_auto {
11241 return Err(syn::Error::new_spanned(
11242 field,
11243 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11244 wrapped in `Auto<...>` so the macro can detect the \
11245 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11246 ));
11247 }
11248 if attrs.auto_uuid {
11249 return Err(syn::Error::new_spanned(
11250 field,
11251 "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11252 `#[rustango(auto_uuid)]` — the former generates the UUID \
11253 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11254 Pick one.",
11255 ));
11256 }
11257 }
11258 if attrs.auto_now_add || attrs.auto_now {
11259 if kind != DetectedKind::DateTime {
11260 return Err(syn::Error::new_spanned(
11261 field,
11262 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11263 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11264 ));
11265 }
11266 if !detected_auto {
11267 return Err(syn::Error::new_spanned(
11268 field,
11269 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11270 the field type to be wrapped in `Auto<...>` so the macro skips \
11271 the column on INSERT and the DB DEFAULT (`now()`) fires",
11272 ));
11273 }
11274 }
11275 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11276 return Err(syn::Error::new_spanned(
11277 field,
11278 "`#[rustango(soft_delete)]` requires the field type to be \
11279 `Option<chrono::DateTime<chrono::Utc>>`",
11280 ));
11281 }
11282 let is_mixin_auto =
11283 attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11284 if detected_auto && !primary_key && !is_mixin_auto {
11285 return Err(syn::Error::new_spanned(
11286 field,
11287 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11288 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11289 `auto_now`",
11290 ));
11291 }
11292 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11293 return Err(syn::Error::new_spanned(
11294 field,
11295 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11296 SERIAL / BIGSERIAL already supplies a default sequence.",
11297 ));
11298 }
11299 if fk_inner.is_some() && primary_key {
11300 return Err(syn::Error::new_spanned(
11301 field,
11302 "`ForeignKey<T>` is not allowed on a primary-key field — \
11303 a row's PK is its own identity, not a reference to a parent.",
11304 ));
11305 }
11306 if attrs.generated_as.is_some() {
11307 if primary_key {
11308 return Err(syn::Error::new_spanned(
11309 field,
11310 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11311 primary-key field — a PK must be writable so the row \
11312 has an identity at INSERT time.",
11313 ));
11314 }
11315 if attrs.default.is_some() {
11316 return Err(syn::Error::new_spanned(
11317 field,
11318 "`#[rustango(generated_as = \"…\")]` cannot combine with \
11319 `default = \"…\"` — Postgres rejects DEFAULT on \
11320 generated columns. The expression IS the default.",
11321 ));
11322 }
11323 if detected_auto {
11324 return Err(syn::Error::new_spanned(
11325 field,
11326 "`#[rustango(generated_as = \"…\")]` is not allowed on \
11327 an `Auto<T>` field — generated columns are computed \
11328 by the DB, not server-assigned via a sequence. Use a \
11329 plain Rust type (e.g. `f64`).",
11330 ));
11331 }
11332 if fk_inner.is_some() {
11333 return Err(syn::Error::new_spanned(
11334 field,
11335 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11336 ForeignKey field.",
11337 ));
11338 }
11339 }
11340 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11341 let column_lit = column.as_str();
11342 // pgvector (#824): the `vector(dims = N)` attribute supplies the
11343 // dimension that the bare `Vector` Rust type can't carry, so emit
11344 // `FieldType::Vector(N)` here rather than the `variant_tokens`
11345 // fallback of `Vector(0)`.
11346 let field_type_tokens = if kind == DetectedKind::Vector {
11347 let root = rustango_root();
11348 let dims = attrs.vector_dims.unwrap_or(0);
11349 quote!(#root::core::FieldType::Vector(#dims))
11350 } else if kind == DetectedKind::Geometry {
11351 // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11352 // SRID that the bare `Point` Rust type can't carry, so emit
11353 // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11354 let root = rustango_root();
11355 let srid = attrs.geometry_srid.unwrap_or(0);
11356 quote!(#root::core::FieldType::Geometry(#srid))
11357 } else {
11358 kind.variant_tokens()
11359 };
11360 let max_length = optional_u32(attrs.max_length);
11361 let min = optional_i64(attrs.min);
11362 let max = optional_i64(attrs.max);
11363 let default = optional_str(attrs.default.as_deref());
11364
11365 let unique = attrs.unique;
11366 let generated_as = optional_str(attrs.generated_as.as_deref());
11367 let help_text = optional_str(attrs.help_text.as_deref());
11368 let choices = optional_choices(attrs.choices.as_deref());
11369 let db_comment = optional_str(attrs.db_comment.as_deref());
11370 let verbose_name = optional_str(attrs.verbose_name.as_deref());
11371 let editable = attrs.editable;
11372 let blank = attrs.blank;
11373 let case_insensitive = attrs.case_insensitive;
11374 let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11375 if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() {
11376 return Err(syn::Error::new_spanned(
11377 field,
11378 "`#[rustango(on_delete = \"…\")]` requires either `fk = \"<table>\"` \
11379 or `o2o = \"<table>\"` on the same field — it has no meaning on a \
11380 non-FK column.",
11381 ));
11382 }
11383 let fk_on_delete = match attrs.on_delete.as_deref() {
11384 None => quote!(::core::option::Option::None),
11385 Some(action) => {
11386 let variant = match action {
11387 "cascade" => quote!(Cascade),
11388 "restrict" => quote!(Restrict),
11389 "set_null" => quote!(SetNull),
11390 "set_default" => quote!(SetDefault),
11391 "no_action" => quote!(NoAction),
11392 // parse_field_attrs already validated this — guard against future drift.
11393 other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11394 };
11395 quote!(::core::option::Option::Some(
11396 #root::core::OnDeleteAction::#variant
11397 ))
11398 }
11399 };
11400 let auto_now_flag = attrs.auto_now;
11401 let schema = quote! {{
11402 #root::core::FieldSchema::from_parts(#root::core::FieldSchemaParts {
11403 name: #name,
11404 column: #column_lit,
11405 ty: #field_type_tokens,
11406 nullable: #nullable,
11407 primary_key: #primary_key,
11408 relation: #relation,
11409 max_length: #max_length,
11410 min: #min,
11411 max: #max,
11412 default: #default,
11413 auto: #auto,
11414 auto_now: #auto_now_flag,
11415 unique: #unique,
11416 generated_as: #generated_as,
11417 help_text: #help_text,
11418 choices: #choices,
11419 db_comment: #db_comment,
11420 verbose_name: #verbose_name,
11421 editable: #editable,
11422 blank: #blank,
11423 case_insensitive: #case_insensitive,
11424 fk_on_delete: #fk_on_delete,
11425 validators: &[ #(#validators_lits),* ],
11426 })
11427 }};
11428
11429 let from_row_init = quote! {
11430 #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11431 };
11432 let from_aliased_row_init = quote! {
11433 #ident: #root::sql::sqlx::Row::try_get(
11434 row,
11435 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11436 )?
11437 };
11438 // `Auto<Uuid>` and `ForeignKey<_, Uuid>` decode through their own
11439 // MySQL impls; a bare or optional `Uuid` needs `FlatScalar`'s cell.
11440 let uuid_column = kind == DetectedKind::Uuid && !detected_auto && fk_inner.is_none();
11441 let (from_row_init_my, from_aliased_row_init_my) = if uuid_column {
11442 (
11443 quote! { #ident: #root::sql::try_get_flat_my(row, #column_lit)? },
11444 quote! {
11445 #ident: #root::sql::try_get_flat_my(
11446 row,
11447 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11448 )?
11449 },
11450 )
11451 } else {
11452 (from_row_init.clone(), from_aliased_row_init.clone())
11453 };
11454
11455 Ok(FieldInfo {
11456 ident,
11457 column,
11458 primary_key,
11459 auto,
11460 value_ty: &field.ty,
11461 field_type_tokens,
11462 schema,
11463 from_row_init,
11464 from_aliased_row_init,
11465 from_row_init_my,
11466 from_aliased_row_init_my,
11467 uuid_column,
11468 fk_inner: fk_inner.cloned(),
11469 fk_pk_kind: kind,
11470 nullable,
11471 auto_now: attrs.auto_now,
11472 auto_now_add: attrs.auto_now_add,
11473 soft_delete: attrs.soft_delete,
11474 generated_as: attrs.generated_as.clone(),
11475 default_uuid_v7: attrs.default_uuid_v7,
11476 related_name: attrs.related_name.clone(),
11477 })
11478}
11479
11480fn check_bound_compatibility(
11481 field: &syn::Field,
11482 attrs: &FieldAttrs,
11483 kind: DetectedKind,
11484) -> syn::Result<()> {
11485 if attrs.max_length.is_some() && kind != DetectedKind::String {
11486 return Err(syn::Error::new_spanned(
11487 field,
11488 "`max_length` is only valid on `String` fields (or `Option<String>`)",
11489 ));
11490 }
11491 // These were silently dropped on the wrong type (#1937).
11492 if attrs.case_insensitive && kind != DetectedKind::String {
11493 return Err(syn::Error::new_spanned(
11494 field,
11495 "`citext` is only valid on `String` fields (or `Option<String>`)",
11496 ));
11497 }
11498 if attrs.vector_dims.is_some() && kind != DetectedKind::Vector {
11499 return Err(syn::Error::new_spanned(
11500 field,
11501 "`vector(dims = …)` is only valid on a pgvector `Vector` field",
11502 ));
11503 }
11504 if attrs.geometry_srid.is_some() && kind != DetectedKind::Geometry {
11505 return Err(syn::Error::new_spanned(
11506 field,
11507 "`geometry(srid = …)` is only valid on a PostGIS `Point` field",
11508 ));
11509 }
11510 if attrs.choices.is_some() && kind != DetectedKind::String {
11511 return Err(syn::Error::new_spanned(
11512 field,
11513 "`choices` is only valid on `String` fields (or `Option<String>`) — \
11514 integer-valued enumerations should be modeled with a Rust enum and \
11515 custom (de)serializer for now",
11516 ));
11517 }
11518 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11519 return Err(syn::Error::new_spanned(
11520 field,
11521 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11522 ));
11523 }
11524 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11525 if min > max {
11526 return Err(syn::Error::new_spanned(
11527 field,
11528 format!("`min` ({min}) is greater than `max` ({max})"),
11529 ));
11530 }
11531 }
11532 Ok(())
11533}
11534
11535/// A `&'static [ty]` of `const fn` results. The slice needs its own
11536/// `const`: a `&[..]` assigned in a statement is not extended to `'static`.
11537fn const_slice(ty: &TokenStream2, items: impl Iterator<Item = TokenStream2>) -> TokenStream2 {
11538 quote!({
11539 const ITEMS: &[#ty] = &[ #(#items),* ];
11540 ITEMS
11541 })
11542}
11543
11544fn optional_u32(value: Option<u32>) -> TokenStream2 {
11545 if let Some(v) = value {
11546 quote!(::core::option::Option::Some(#v))
11547 } else {
11548 quote!(::core::option::Option::None)
11549 }
11550}
11551
11552fn optional_i64(value: Option<i64>) -> TokenStream2 {
11553 if let Some(v) = value {
11554 quote!(::core::option::Option::Some(#v))
11555 } else {
11556 quote!(::core::option::Option::None)
11557 }
11558}
11559
11560fn optional_str(value: Option<&str>) -> TokenStream2 {
11561 if let Some(v) = value {
11562 quote!(::core::option::Option::Some(#v))
11563 } else {
11564 quote!(::core::option::Option::None)
11565 }
11566}
11567
11568fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11569 let Some(pairs) = pairs else {
11570 return quote!(::core::option::Option::None);
11571 };
11572 let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11573 quote!(::core::option::Option::Some(&[#(#entries),*]))
11574}
11575
11576fn relation_tokens(
11577 field: &syn::Field,
11578 attrs: &FieldAttrs,
11579 fk_inner: Option<&syn::Type>,
11580 table: &str,
11581) -> syn::Result<TokenStream2> {
11582 let root = rustango_root();
11583 if let Some(inner) = fk_inner {
11584 if attrs.fk.is_some() || attrs.o2o.is_some() {
11585 return Err(syn::Error::new_spanned(
11586 field,
11587 "`ForeignKey<T>` already declares the FK target via the type parameter — \
11588 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11589 ));
11590 }
11591 let on = attrs.on.as_deref().unwrap_or("id");
11592 return Ok(quote! {
11593 ::core::option::Option::Some(#root::core::Relation::fk(
11594 <#inner as #root::core::Model>::SCHEMA.table,
11595 #on,
11596 ))
11597 });
11598 }
11599 match (&attrs.fk, &attrs.o2o) {
11600 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11601 field,
11602 "`fk` and `o2o` are mutually exclusive",
11603 )),
11604 (Some(to), None) => {
11605 let on = attrs.on.as_deref().unwrap_or("id");
11606 // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11607 // the model's own table. Threaded as a literal string at
11608 // macro-expansion time to sidestep the const-eval cycle
11609 // that `Self::SCHEMA.table` would create when referenced
11610 // inside Self::SCHEMA's own initializer.
11611 let resolved = if to == "self" { table } else { to };
11612 Ok(quote! {
11613 ::core::option::Option::Some(#root::core::Relation::fk(#resolved, #on))
11614 })
11615 }
11616 (None, Some(to)) => {
11617 let on = attrs.on.as_deref().unwrap_or("id");
11618 let resolved = if to == "self" { table } else { to };
11619 Ok(quote! {
11620 ::core::option::Option::Some(#root::core::Relation::o2o(#resolved, #on))
11621 })
11622 }
11623 (None, None) => {
11624 if attrs.on.is_some() {
11625 return Err(syn::Error::new_spanned(
11626 field,
11627 "`on` requires `fk` or `o2o`",
11628 ));
11629 }
11630 Ok(quote!(::core::option::Option::None))
11631 }
11632 }
11633}
11634
11635/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11636/// about kinds without depending on `rustango-core` (which would require a
11637/// proc-macro/normal split it doesn't have today).
11638#[derive(Clone, Copy, PartialEq, Eq)]
11639enum DetectedKind {
11640 I16,
11641 I32,
11642 I64,
11643 F32,
11644 F64,
11645 Bool,
11646 String,
11647 DateTime,
11648 Date,
11649 Time,
11650 Uuid,
11651 Json,
11652 Decimal,
11653 Binary,
11654 /// `Array<String>` → PG `text[]` (#341).
11655 ArrayText,
11656 /// `Array<i32>` → PG `integer[]` (#341).
11657 ArrayInt,
11658 /// `Array<i64>` → PG `bigint[]` (#341).
11659 ArrayBigInt,
11660 /// `Range<i32>` → PG `int4range` (#343).
11661 RangeInt,
11662 /// `Range<i64>` → PG `int8range` (#343).
11663 RangeBigInt,
11664 /// `Range<Decimal>` → PG `numrange` (#343).
11665 RangeNumeric,
11666 /// `Range<NaiveDate>` → PG `daterange` (#343).
11667 RangeDate,
11668 /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11669 RangeDateTime,
11670 /// `HStore` → PG `hstore` (#342).
11671 HStore,
11672 /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11673 /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11674 /// in at the `FieldType` emission site (not carried on this enum).
11675 Vector,
11676 /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11677 /// from the `#[rustango(geometry(srid = N))]` field attribute,
11678 /// threaded in at the `FieldType` emission site.
11679 Geometry,
11680}
11681
11682impl DetectedKind {
11683 fn variant_tokens(self) -> TokenStream2 {
11684 let root = rustango_root();
11685 match self {
11686 Self::I16 => quote!(#root::core::FieldType::I16),
11687 Self::I32 => quote!(#root::core::FieldType::I32),
11688 Self::I64 => quote!(#root::core::FieldType::I64),
11689 Self::F32 => quote!(#root::core::FieldType::F32),
11690 Self::F64 => quote!(#root::core::FieldType::F64),
11691 Self::Bool => quote!(#root::core::FieldType::Bool),
11692 Self::String => quote!(#root::core::FieldType::String),
11693 Self::DateTime => quote!(#root::core::FieldType::DateTime),
11694 Self::Date => quote!(#root::core::FieldType::Date),
11695 Self::Time => quote!(#root::core::FieldType::Time),
11696 Self::Uuid => quote!(#root::core::FieldType::Uuid),
11697 Self::Json => quote!(#root::core::FieldType::Json),
11698 Self::Decimal => quote!(#root::core::FieldType::Decimal),
11699 Self::Binary => quote!(#root::core::FieldType::Binary),
11700 Self::ArrayText => {
11701 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11702 }
11703 Self::ArrayInt => {
11704 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11705 }
11706 Self::ArrayBigInt => {
11707 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11708 }
11709 Self::RangeInt => {
11710 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11711 }
11712 Self::RangeBigInt => {
11713 quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11714 }
11715 Self::RangeNumeric => {
11716 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11717 }
11718 Self::RangeDate => {
11719 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11720 }
11721 Self::RangeDateTime => {
11722 quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11723 }
11724 Self::HStore => quote!(#root::core::FieldType::HStore),
11725 // Dimension comes from the `vector(dims = N)` attribute,
11726 // applied at the emission site; `0` here is just a fallback.
11727 Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11728 // SRID comes from the `geometry(srid = N)` attribute, applied
11729 // at the emission site; `0` here is just a fallback.
11730 Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11731 }
11732 }
11733
11734 fn is_integer(self) -> bool {
11735 matches!(self, Self::I16 | Self::I32 | Self::I64)
11736 }
11737
11738 /// `(SqlValue::<Variant>, default expr)` for emitting the
11739 /// `match SqlValue { … }` arm in `LoadRelated::__rustango_load_related`
11740 /// for a `ForeignKey<T, K>` FK whose K maps to `self`. The default
11741 /// fires only when the parent's `__rustango_pk_value` returns a
11742 /// different variant than expected, which is a compile-time bug —
11743 /// but we still need a value-typed fallback to keep the match
11744 /// total.
11745 fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
11746 let root = rustango_root();
11747 match self {
11748 Self::I16 => (quote!(I16), quote!(0i16)),
11749 Self::I32 => (quote!(I32), quote!(0i32)),
11750 Self::I64 => (quote!(I64), quote!(0i64)),
11751 Self::F32 => (quote!(F32), quote!(0f32)),
11752 Self::F64 => (quote!(F64), quote!(0f64)),
11753 Self::Bool => (quote!(Bool), quote!(false)),
11754 Self::String => (quote!(String), quote!(::std::string::String::new())),
11755 Self::DateTime => (
11756 quote!(DateTime),
11757 quote!(<#root::__chrono::DateTime<#root::__chrono::Utc> as ::std::default::Default>::default()),
11758 ),
11759 Self::Date => (
11760 quote!(Date),
11761 quote!(<#root::__chrono::NaiveDate as ::std::default::Default>::default()),
11762 ),
11763 Self::Time => (
11764 quote!(Time),
11765 quote!(<#root::__chrono::NaiveTime as ::std::default::Default>::default()),
11766 ),
11767 Self::Uuid => (quote!(Uuid), quote!(#root::__uuid::Uuid::nil())),
11768 Self::Json => (quote!(Json), quote!(#root::__serde_json::Value::Null)),
11769 Self::Decimal => (
11770 quote!(Decimal),
11771 quote!(<#root::__rust_decimal::Decimal as ::std::default::Default>::default()),
11772 ),
11773 Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
11774 // Arrays (#341) can never be a foreign-key primary key, so
11775 // this arm is never reached at runtime — but the match must
11776 // stay total. A bare empty `SqlValue::Array` is the dummy.
11777 Self::ArrayText | Self::ArrayInt | Self::ArrayBigInt => {
11778 (quote!(Array), quote!(::std::vec::Vec::new()))
11779 }
11780 // Ranges (#343) likewise can't be a FK PK — never reached.
11781 Self::RangeInt
11782 | Self::RangeBigInt
11783 | Self::RangeNumeric
11784 | Self::RangeDate
11785 | Self::RangeDateTime => (quote!(RangeLiteral), quote!(::std::string::String::new())),
11786 // HStore (#342) can't be a FK PK — never reached.
11787 Self::HStore => (quote!(HStore), quote!(::std::vec::Vec::new())),
11788 // Vector (#824) can't be a FK PK — never reached.
11789 Self::Vector => (quote!(Vector), quote!(::std::vec::Vec::new())),
11790 // Geometry (#443) can't be a FK PK — never reached (arm is
11791 // exhaustiveness-only; never interpolated into emitted code).
11792 Self::Geometry => (quote!(Geometry), quote!(::std::vec::Vec::new())),
11793 }
11794 }
11795}
11796
11797/// Result of walking a field's Rust type. `kind` is the underlying
11798/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11799/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11800/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11801/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11802#[derive(Clone, Copy)]
11803struct DetectedType<'a> {
11804 kind: DetectedKind,
11805 nullable: bool,
11806 auto: bool,
11807 fk_inner: Option<&'a syn::Type>,
11808}
11809
11810/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11811/// non-`Auto` types — the caller should already have routed Auto-only
11812/// codegen through this helper, so a `None` indicates a macro-internal
11813/// invariant break.
11814fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11815 let Type::Path(TypePath { path, qself: None }) = ty else {
11816 return None;
11817 };
11818 let last = path.segments.last()?;
11819 if last.ident != "Auto" {
11820 return None;
11821 }
11822 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11823 return None;
11824 };
11825 args.args.iter().find_map(|a| match a {
11826 syn::GenericArgument::Type(t) => Some(t),
11827 _ => None,
11828 })
11829}
11830
11831fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11832 let Type::Path(TypePath { path, qself: None }) = ty else {
11833 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11834 };
11835 let last = path
11836 .segments
11837 .last()
11838 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11839
11840 if last.ident == "Option" {
11841 let inner = generic_inner(ty, &last.arguments, "Option")?;
11842 let inner_det = detect_type(inner)?;
11843 if inner_det.nullable {
11844 return Err(syn::Error::new_spanned(
11845 ty,
11846 "nested Option is not supported",
11847 ));
11848 }
11849 if inner_det.auto {
11850 return Err(syn::Error::new_spanned(
11851 ty,
11852 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11853 ));
11854 }
11855 return Ok(DetectedType {
11856 nullable: true,
11857 ..inner_det
11858 });
11859 }
11860
11861 if last.ident == "Auto" {
11862 let inner = generic_inner(ty, &last.arguments, "Auto")?;
11863 let inner_det = detect_type(inner)?;
11864 if inner_det.auto {
11865 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11866 }
11867 if inner_det.nullable {
11868 return Err(syn::Error::new_spanned(
11869 ty,
11870 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11871 ));
11872 }
11873 if inner_det.fk_inner.is_some() {
11874 return Err(syn::Error::new_spanned(
11875 ty,
11876 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11877 ));
11878 }
11879 if !matches!(
11880 inner_det.kind,
11881 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11882 ) {
11883 return Err(syn::Error::new_spanned(
11884 ty,
11885 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11886 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11887 (DEFAULT now())",
11888 ));
11889 }
11890 return Ok(DetectedType {
11891 auto: true,
11892 ..inner_det
11893 });
11894 }
11895
11896 if last.ident == "ForeignKey" {
11897 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11898 // Resolve the FK column's underlying SQL type from `K`. When the
11899 // user wrote `ForeignKey<T>` without a key parameter, the type
11900 // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11901 // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11902 // recurse into K so the column DDL emits the right SQL type
11903 // (VARCHAR for String, UUID for Uuid, …) and the load_related
11904 // emitter knows which `SqlValue` variant to match.
11905 let kind = match key_ty {
11906 Some(k) => detect_type(k)?.kind,
11907 None => DetectedKind::I64,
11908 };
11909 return Ok(DetectedType {
11910 kind,
11911 nullable: false,
11912 auto: false,
11913 fk_inner: Some(inner),
11914 });
11915 }
11916
11917 let kind = match last.ident.to_string().as_str() {
11918 "i16" => DetectedKind::I16,
11919 "i32" => DetectedKind::I32,
11920 "i64" => DetectedKind::I64,
11921 "f32" => DetectedKind::F32,
11922 "f64" => DetectedKind::F64,
11923 "bool" => DetectedKind::Bool,
11924 "String" => DetectedKind::String,
11925 "DateTime" => DetectedKind::DateTime,
11926 "NaiveDate" => DetectedKind::Date,
11927 "NaiveTime" => DetectedKind::Time,
11928 "Uuid" => DetectedKind::Uuid,
11929 "Value" => DetectedKind::Json,
11930 "Decimal" => DetectedKind::Decimal,
11931 // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11932 // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11933 // — that would emit Binary DDL and decode-fail at runtime.
11934 "Vec" => {
11935 let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11936 if let Type::Path(TypePath { path, qself: None }) = inner {
11937 if let Some(seg) = path.segments.last() {
11938 if seg.ident == "u8" && seg.arguments.is_empty() {
11939 return Ok(DetectedType {
11940 kind: DetectedKind::Binary,
11941 nullable: false,
11942 auto: false,
11943 fk_inner: None,
11944 });
11945 }
11946 }
11947 }
11948 return Err(syn::Error::new_spanned(
11949 ty,
11950 "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11951 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11952 ));
11953 }
11954 // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11955 // `integer[]` / `bigint[]` (#341).
11956 "Array" => {
11957 let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11958 let elem = match inner {
11959 Type::Path(TypePath { path, qself: None }) => {
11960 path.segments.last().map(|s| s.ident.to_string())
11961 }
11962 _ => None,
11963 };
11964 let kind = match elem.as_deref() {
11965 Some("String") => DetectedKind::ArrayText,
11966 Some("i32") => DetectedKind::ArrayInt,
11967 Some("i64") => DetectedKind::ArrayBigInt,
11968 _ => {
11969 return Err(syn::Error::new_spanned(
11970 ty,
11971 "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
11972 `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
11973 ));
11974 }
11975 };
11976 return Ok(DetectedType {
11977 kind,
11978 nullable: false,
11979 auto: false,
11980 fk_inner: None,
11981 });
11982 }
11983 // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
11984 // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
11985 // `int8range` / `numrange` / `daterange` / `tstzrange` (#343).
11986 "Range" => {
11987 let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
11988 let elem = match inner {
11989 Type::Path(TypePath { path, qself: None }) => {
11990 path.segments.last().map(|s| s.ident.to_string())
11991 }
11992 _ => None,
11993 };
11994 let kind = match elem.as_deref() {
11995 Some("i32") => DetectedKind::RangeInt,
11996 Some("i64") => DetectedKind::RangeBigInt,
11997 Some("Decimal") => DetectedKind::RangeNumeric,
11998 Some("NaiveDate") => DetectedKind::RangeDate,
11999 Some("DateTime") => DetectedKind::RangeDateTime,
12000 _ => {
12001 return Err(syn::Error::new_spanned(
12002 ty,
12003 "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
12004 `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
12005 `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
12006 (→ tstzrange) are supported (#343)",
12007 ));
12008 }
12009 };
12010 return Ok(DetectedType {
12011 kind,
12012 nullable: false,
12013 auto: false,
12014 fk_inner: None,
12015 });
12016 }
12017 // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
12018 // (the `CastValue` impl bridges logical↔stored); the field's own
12019 // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
12020 // schema just needs `FieldType::String`.
12021 "Cast" => {
12022 return Ok(DetectedType {
12023 kind: DetectedKind::String,
12024 nullable: false,
12025 auto: false,
12026 fk_inner: None,
12027 });
12028 }
12029 // `HStore` → PG `hstore` (#342). No generic
12030 // parameter — always a string→string map.
12031 "HStore" => {
12032 return Ok(DetectedType {
12033 kind: DetectedKind::HStore,
12034 nullable: false,
12035 auto: false,
12036 fk_inner: None,
12037 });
12038 }
12039 // `Vector` → pgvector `vector(N)` (#824). The dimension is
12040 // supplied by `#[rustango(vector(dims = N))]`, not the type.
12041 "Vector" => {
12042 return Ok(DetectedType {
12043 kind: DetectedKind::Vector,
12044 nullable: false,
12045 auto: false,
12046 fk_inner: None,
12047 });
12048 }
12049 // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
12050 // supplied by `#[rustango(geometry(srid = N))]`, not the type.
12051 "Point" => {
12052 return Ok(DetectedType {
12053 kind: DetectedKind::Geometry,
12054 nullable: false,
12055 auto: false,
12056 fk_inner: None,
12057 });
12058 }
12059 other => {
12060 return Err(syn::Error::new_spanned(
12061 ty,
12062 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)"),
12063 ));
12064 }
12065 };
12066 Ok(DetectedType {
12067 kind,
12068 nullable: false,
12069 auto: false,
12070 fk_inner: None,
12071 })
12072}
12073
12074fn generic_inner<'a>(
12075 ty: &'a Type,
12076 arguments: &'a PathArguments,
12077 wrapper: &str,
12078) -> syn::Result<&'a Type> {
12079 let PathArguments::AngleBracketed(args) = arguments else {
12080 return Err(syn::Error::new_spanned(
12081 ty,
12082 format!("{wrapper} requires a generic argument"),
12083 ));
12084 };
12085 args.args
12086 .iter()
12087 .find_map(|a| match a {
12088 GenericArgument::Type(t) => Some(t),
12089 _ => None,
12090 })
12091 .ok_or_else(|| {
12092 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12093 })
12094}
12095
12096/// Like [`generic_inner`] but pulls *two* type args — the first is
12097/// required, the second is optional. Used by the `ForeignKey<T, K>`
12098/// detection where K defaults to `i64` when omitted.
12099fn generic_pair<'a>(
12100 ty: &'a Type,
12101 arguments: &'a PathArguments,
12102 wrapper: &str,
12103) -> syn::Result<(&'a Type, Option<&'a Type>)> {
12104 let PathArguments::AngleBracketed(args) = arguments else {
12105 return Err(syn::Error::new_spanned(
12106 ty,
12107 format!("{wrapper} requires a generic argument"),
12108 ));
12109 };
12110 let mut types = args.args.iter().filter_map(|a| match a {
12111 GenericArgument::Type(t) => Some(t),
12112 _ => None,
12113 });
12114 let first = types.next().ok_or_else(|| {
12115 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12116 })?;
12117 let second = types.next();
12118 Ok((first, second))
12119}
12120
12121fn to_snake_case(s: &str) -> String {
12122 let mut out = String::with_capacity(s.len() + 4);
12123 for (i, ch) in s.chars().enumerate() {
12124 if ch.is_ascii_uppercase() {
12125 if i > 0 {
12126 out.push('_');
12127 }
12128 out.push(ch.to_ascii_lowercase());
12129 } else {
12130 out.push(ch);
12131 }
12132 }
12133 out
12134}
12135
12136// ============================================================
12137// #[derive(Form)] — slice 8.4B
12138// ============================================================
12139
12140/// Per-field `#[form(...)]` attributes recognised by the derive.
12141#[derive(Default)]
12142struct FormFieldAttrs {
12143 min: Option<i64>,
12144 max: Option<i64>,
12145 min_length: Option<u32>,
12146 max_length: Option<u32>,
12147 /// `#[form(clean = "fn_name")]` — per-field clean hook.
12148 /// The named static method on the form struct is called after the
12149 /// field's typed parse + length/range checks; it gets the parsed
12150 /// value by reference and returns `Result<<FieldType>, String>`.
12151 /// On Ok, the returned value replaces the parsed one; on Err, the
12152 /// message is attached to the field error list. Issue #372.
12153 clean: Option<syn::Ident>,
12154}
12155
12156/// Container-level `#[form(...)]` attributes. Currently only the
12157/// cross-field `validate` hook (issue #373).
12158#[derive(Default)]
12159struct FormContainerAttrs {
12160 /// `#[form(validate = "fn_name")]` — whole-form clean hook.
12161 /// After every per-field parse succeeds, the named method on the
12162 /// form struct is called with `&self` and may return
12163 /// `Result<(), FormErrors>`. Errors merge into the field error
12164 /// list. Issue #373.
12165 validate: Option<syn::Ident>,
12166}
12167
12168/// Detected shape of a form field's Rust type.
12169#[derive(Clone, Copy)]
12170enum FormFieldKind {
12171 String,
12172 I16,
12173 I32,
12174 I64,
12175 F32,
12176 F64,
12177 Bool,
12178}
12179
12180impl FormFieldKind {
12181 fn parse_method(self) -> &'static str {
12182 match self {
12183 Self::I16 => "i16",
12184 Self::I32 => "i32",
12185 Self::I64 => "i64",
12186 Self::F32 => "f32",
12187 Self::F64 => "f64",
12188 // String + Bool don't go through `str::parse`; the codegen
12189 // handles them inline.
12190 Self::String | Self::Bool => "",
12191 }
12192 }
12193}
12194
12195fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
12196 let root = rustango_root();
12197 let struct_name = &input.ident;
12198
12199 let Data::Struct(data) = &input.data else {
12200 return Err(syn::Error::new_spanned(
12201 struct_name,
12202 "Form can only be derived on structs",
12203 ));
12204 };
12205 let Fields::Named(named) = &data.fields else {
12206 return Err(syn::Error::new_spanned(
12207 struct_name,
12208 "Form requires a struct with named fields",
12209 ));
12210 };
12211
12212 // #373 — container-level `#[form(validate = "fn")]` hook.
12213 let container = parse_form_container_attrs(input)?;
12214 let post_field_clean: Vec<TokenStream2> = Vec::new();
12215 let _ = post_field_clean;
12216
12217 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12218 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12219
12220 for field in &named.named {
12221 let ident = field
12222 .ident
12223 .as_ref()
12224 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12225 let attrs = parse_form_field_attrs(field)?;
12226 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12227
12228 let name_lit = ident_name(ident);
12229 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12230 // #372 — append the per-field `clean_<field>` call right after
12231 // the parse block when the attribute is set. The clean fn
12232 // takes &T and returns Result<T, String>; on Err we attach
12233 // the message to the field error list without aborting, so
12234 // the caller sees every field error at once.
12235 let clean_block = if let Some(clean_fn) = &attrs.clean {
12236 quote! {
12237 if __errors.fields().get(#name_lit).is_none() {
12238 match Self::#clean_fn(&#ident) {
12239 ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
12240 ::core::result::Result::Err(__msg) => {
12241 __errors.add(#name_lit, __msg);
12242 }
12243 }
12244 }
12245 }
12246 } else {
12247 quote! {}
12248 };
12249 field_blocks.push(quote! {
12250 #parse_block
12251 #clean_block
12252 });
12253 field_idents.push(ident);
12254 }
12255
12256 // #373 — after every per-field parse + clean succeeds, call the
12257 // cross-field validator if declared. Errors merge into the
12258 // outgoing FormErrors via the existing `FormErrors::merge` helper
12259 // (same primitive the serializer cross-field hook uses).
12260 let cross_field_call = if let Some(validate_fn) = &container.validate {
12261 quote! {
12262 if __errors.is_empty() {
12263 let __candidate = Self { #( #field_idents ),* };
12264 if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12265 __errors.merge(__other);
12266 }
12267 if !__errors.is_empty() {
12268 return ::core::result::Result::Err(__errors);
12269 }
12270 return ::core::result::Result::Ok(__candidate);
12271 }
12272 }
12273 } else {
12274 quote! {}
12275 };
12276
12277 Ok(quote! {
12278 impl #root::forms::Form for #struct_name {
12279 fn parse(
12280 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12281 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12282 let mut __errors = #root::forms::FormErrors::default();
12283 #( #field_blocks )*
12284 #cross_field_call
12285 if !__errors.is_empty() {
12286 return ::core::result::Result::Err(__errors);
12287 }
12288 ::core::result::Result::Ok(Self {
12289 #( #field_idents ),*
12290 })
12291 }
12292 }
12293 })
12294}
12295
12296fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12297 let mut out = FormContainerAttrs::default();
12298 for attr in &input.attrs {
12299 if !attr.path().is_ident("form") {
12300 continue;
12301 }
12302 attr.parse_nested_meta(|meta| {
12303 if meta.path.is_ident("validate") {
12304 let s: LitStr = meta.value()?.parse()?;
12305 out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12306 return Ok(());
12307 }
12308 Err(meta.error("unknown form container attribute (supported: `validate`)"))
12309 })?;
12310 }
12311 Ok(out)
12312}
12313
12314fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12315 let mut out = FormFieldAttrs::default();
12316 for attr in &field.attrs {
12317 if !attr.path().is_ident("form") {
12318 continue;
12319 }
12320 attr.parse_nested_meta(|meta| {
12321 if meta.path.is_ident("min") {
12322 let lit: syn::LitInt = meta.value()?.parse()?;
12323 out.min = Some(lit.base10_parse::<i64>()?);
12324 return Ok(());
12325 }
12326 if meta.path.is_ident("max") {
12327 let lit: syn::LitInt = meta.value()?.parse()?;
12328 out.max = Some(lit.base10_parse::<i64>()?);
12329 return Ok(());
12330 }
12331 if meta.path.is_ident("min_length") {
12332 let lit: syn::LitInt = meta.value()?.parse()?;
12333 out.min_length = Some(lit.base10_parse::<u32>()?);
12334 return Ok(());
12335 }
12336 if meta.path.is_ident("max_length") {
12337 let lit: syn::LitInt = meta.value()?.parse()?;
12338 out.max_length = Some(lit.base10_parse::<u32>()?);
12339 return Ok(());
12340 }
12341 if meta.path.is_ident("clean") {
12342 let s: LitStr = meta.value()?.parse()?;
12343 out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12344 return Ok(());
12345 }
12346 Err(meta.error(
12347 "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12348 ))
12349 })?;
12350 }
12351 Ok(out)
12352}
12353
12354fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12355 let Type::Path(TypePath { path, qself: None }) = ty else {
12356 return Err(syn::Error::new(
12357 span,
12358 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12359 ));
12360 };
12361 let last = path
12362 .segments
12363 .last()
12364 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12365
12366 if last.ident == "Option" {
12367 let inner = generic_inner(ty, &last.arguments, "Option")?;
12368 let (kind, nested) = detect_form_field(inner, span)?;
12369 if nested {
12370 return Err(syn::Error::new(
12371 span,
12372 "nested Option in Form fields is not supported",
12373 ));
12374 }
12375 return Ok((kind, true));
12376 }
12377
12378 let kind = match last.ident.to_string().as_str() {
12379 "String" => FormFieldKind::String,
12380 "i16" => FormFieldKind::I16,
12381 "i32" => FormFieldKind::I32,
12382 "i64" => FormFieldKind::I64,
12383 "f32" => FormFieldKind::F32,
12384 "f64" => FormFieldKind::F64,
12385 "bool" => FormFieldKind::Bool,
12386 other => {
12387 return Err(syn::Error::new(
12388 span,
12389 format!(
12390 "Form field type `{other}` is not supported in v0.8 — use String / \
12391 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12392 ),
12393 ));
12394 }
12395 };
12396 Ok((kind, false))
12397}
12398
12399#[allow(clippy::too_many_lines)]
12400fn render_form_field_parse(
12401 ident: &syn::Ident,
12402 name_lit: &str,
12403 kind: FormFieldKind,
12404 nullable: bool,
12405 attrs: &FormFieldAttrs,
12406) -> TokenStream2 {
12407 // Pull the raw &str from the payload. Uses variable name `data` to
12408 // match the new `Form::parse(data: &HashMap<…>)` signature.
12409 let lookup = quote! {
12410 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12411 };
12412
12413 let parsed_value = match kind {
12414 FormFieldKind::Bool => quote! {
12415 let __v: bool = match __raw {
12416 ::core::option::Option::None => false,
12417 ::core::option::Option::Some(__s) => !matches!(
12418 __s.to_ascii_lowercase().as_str(),
12419 "" | "false" | "0" | "off" | "no"
12420 ),
12421 };
12422 },
12423 FormFieldKind::String => {
12424 if nullable {
12425 quote! {
12426 let __v: ::core::option::Option<::std::string::String> = match __raw {
12427 ::core::option::Option::None => ::core::option::Option::None,
12428 ::core::option::Option::Some(__s) if __s.is_empty() => {
12429 ::core::option::Option::None
12430 }
12431 ::core::option::Option::Some(__s) => {
12432 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12433 }
12434 };
12435 }
12436 } else {
12437 quote! {
12438 let __v: ::std::string::String = match __raw {
12439 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12440 ::core::clone::Clone::clone(__s)
12441 }
12442 _ => {
12443 __errors.add(#name_lit, "This field is required.");
12444 ::std::string::String::new()
12445 }
12446 };
12447 }
12448 }
12449 }
12450 FormFieldKind::I16
12451 | FormFieldKind::I32
12452 | FormFieldKind::I64
12453 | FormFieldKind::F32
12454 | FormFieldKind::F64 => {
12455 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12456 let ty_lit = kind.parse_method();
12457 let default_val = match kind {
12458 FormFieldKind::I16 => quote! { 0i16 },
12459 FormFieldKind::I32 => quote! { 0i32 },
12460 FormFieldKind::I64 => quote! { 0i64 },
12461 FormFieldKind::F32 => quote! { 0f32 },
12462 FormFieldKind::F64 => quote! { 0f64 },
12463 _ => quote! { Default::default() },
12464 };
12465 if nullable {
12466 quote! {
12467 let __v: ::core::option::Option<#parse_ty> = match __raw {
12468 ::core::option::Option::None => ::core::option::Option::None,
12469 ::core::option::Option::Some(__s) if __s.is_empty() => {
12470 ::core::option::Option::None
12471 }
12472 ::core::option::Option::Some(__s) => {
12473 match __s.parse::<#parse_ty>() {
12474 ::core::result::Result::Ok(__n) => {
12475 ::core::option::Option::Some(__n)
12476 }
12477 ::core::result::Result::Err(__e) => {
12478 __errors.add(
12479 #name_lit,
12480 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12481 );
12482 ::core::option::Option::None
12483 }
12484 }
12485 }
12486 };
12487 }
12488 } else {
12489 quote! {
12490 let __v: #parse_ty = match __raw {
12491 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12492 match __s.parse::<#parse_ty>() {
12493 ::core::result::Result::Ok(__n) => __n,
12494 ::core::result::Result::Err(__e) => {
12495 __errors.add(
12496 #name_lit,
12497 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12498 );
12499 #default_val
12500 }
12501 }
12502 }
12503 _ => {
12504 __errors.add(#name_lit, "This field is required.");
12505 #default_val
12506 }
12507 };
12508 }
12509 }
12510 }
12511 };
12512
12513 let validators = render_form_validators(name_lit, kind, nullable, attrs);
12514
12515 quote! {
12516 // `mut` so the per-field `clean` hook (#372) can rewrite the
12517 // parsed value in-place when it returns Ok with a normalized
12518 // form (e.g. trim / lowercase).
12519 let mut #ident = {
12520 #lookup
12521 #parsed_value
12522 #validators
12523 __v
12524 };
12525 }
12526}
12527
12528fn render_form_validators(
12529 name_lit: &str,
12530 kind: FormFieldKind,
12531 nullable: bool,
12532 attrs: &FormFieldAttrs,
12533) -> TokenStream2 {
12534 let mut checks: Vec<TokenStream2> = Vec::new();
12535
12536 let val_ref = if nullable {
12537 quote! { __v.as_ref() }
12538 } else {
12539 quote! { ::core::option::Option::Some(&__v) }
12540 };
12541
12542 let is_string = matches!(kind, FormFieldKind::String);
12543 let is_numeric = matches!(
12544 kind,
12545 FormFieldKind::I16
12546 | FormFieldKind::I32
12547 | FormFieldKind::I64
12548 | FormFieldKind::F32
12549 | FormFieldKind::F64
12550 );
12551
12552 if is_string {
12553 if let Some(min_len) = attrs.min_length {
12554 let min_len_usize = min_len as usize;
12555 checks.push(quote! {
12556 if let ::core::option::Option::Some(__s) = #val_ref {
12557 if __s.chars().count() < #min_len_usize {
12558 __errors.add(
12559 #name_lit,
12560 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12561 );
12562 }
12563 }
12564 });
12565 }
12566 if let Some(max_len) = attrs.max_length {
12567 let max_len_usize = max_len as usize;
12568 checks.push(quote! {
12569 if let ::core::option::Option::Some(__s) = #val_ref {
12570 if __s.chars().count() > #max_len_usize {
12571 __errors.add(
12572 #name_lit,
12573 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12574 );
12575 }
12576 }
12577 });
12578 }
12579 }
12580
12581 if is_numeric {
12582 if let Some(min) = attrs.min {
12583 checks.push(quote! {
12584 if let ::core::option::Option::Some(__n) = #val_ref {
12585 if (*__n as f64) < (#min as f64) {
12586 __errors.add(
12587 #name_lit,
12588 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12589 );
12590 }
12591 }
12592 });
12593 }
12594 if let Some(max) = attrs.max {
12595 checks.push(quote! {
12596 if let ::core::option::Option::Some(__n) = #val_ref {
12597 if (*__n as f64) > (#max as f64) {
12598 __errors.add(
12599 #name_lit,
12600 ::std::format!("Ensure this value is less than or equal to {}.", #max),
12601 );
12602 }
12603 }
12604 });
12605 }
12606 }
12607
12608 quote! { #( #checks )* }
12609}
12610
12611// ============================================================
12612// #[derive(ViewSet)]
12613// ============================================================
12614
12615struct ViewSetAttrs {
12616 model: syn::Path,
12617 fields: Option<Vec<String>>,
12618 filter_fields: Vec<String>,
12619 search_fields: Vec<String>,
12620 /// (field_name, desc)
12621 ordering: Vec<(String, bool)>,
12622 page_size: Option<usize>,
12623 read_only: bool,
12624 allow_anonymous: bool,
12625 perms: ViewSetPermsAttrs,
12626 /// `#[viewset(serializer = SomeSerializer)]` — render list /
12627 /// retrieve / create responses through this `#[derive(Serializer)]`
12628 /// type instead of the default field-level projection (requires the
12629 /// `serializer` feature). Tri-dialect.
12630 serializer: Option<syn::Path>,
12631}
12632
12633#[derive(Default)]
12634struct ViewSetPermsAttrs {
12635 list: Vec<String>,
12636 retrieve: Vec<String>,
12637 create: Vec<String>,
12638 update: Vec<String>,
12639 destroy: Vec<String>,
12640}
12641
12642fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12643 let root = rustango_root();
12644 let struct_name = &input.ident;
12645
12646 // Must be a unit struct or an empty named struct.
12647 match &input.data {
12648 Data::Struct(s) => match &s.fields {
12649 Fields::Unit | Fields::Named(_) => {}
12650 Fields::Unnamed(_) => {
12651 return Err(syn::Error::new_spanned(
12652 struct_name,
12653 "ViewSet can only be derived on a unit struct or an empty named struct",
12654 ));
12655 }
12656 },
12657 _ => {
12658 return Err(syn::Error::new_spanned(
12659 struct_name,
12660 "ViewSet can only be derived on a struct",
12661 ));
12662 }
12663 }
12664
12665 let attrs = parse_viewset_attrs(input)?;
12666 let model_path = &attrs.model;
12667
12668 // `.fields(&[...])` call — None means skip (use all scalar fields).
12669 let fields_call = if let Some(ref fields) = attrs.fields {
12670 let lits = fields.iter().map(|f| f.as_str());
12671 quote!(.fields(&[ #(#lits),* ]))
12672 } else {
12673 quote!()
12674 };
12675
12676 let filter_fields_call = if attrs.filter_fields.is_empty() {
12677 quote!()
12678 } else {
12679 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12680 quote!(.filter_fields(&[ #(#lits),* ]))
12681 };
12682
12683 let search_fields_call = if attrs.search_fields.is_empty() {
12684 quote!()
12685 } else {
12686 let lits = attrs.search_fields.iter().map(|f| f.as_str());
12687 quote!(.search_fields(&[ #(#lits),* ]))
12688 };
12689
12690 let ordering_call = if attrs.ordering.is_empty() {
12691 quote!()
12692 } else {
12693 let pairs = attrs.ordering.iter().map(|(f, desc)| {
12694 let f = f.as_str();
12695 quote!((#f, #desc))
12696 });
12697 quote!(.ordering(&[ #(#pairs),* ]))
12698 };
12699
12700 let page_size_call = if let Some(n) = attrs.page_size {
12701 quote!(.page_size(#n))
12702 } else {
12703 quote!()
12704 };
12705
12706 let read_only_call = if attrs.read_only {
12707 quote!(.read_only())
12708 } else {
12709 quote!()
12710 };
12711 let allow_anonymous_call = if attrs.allow_anonymous {
12712 quote!(.allow_anonymous())
12713 } else {
12714 quote!()
12715 };
12716
12717 // `.serializer::<S>()` — reshape responses through a derived
12718 // serializer. Requires the downstream crate to enable the
12719 // `serializer` feature (the method is gated on it).
12720 let serializer_call = if let Some(ref ser) = attrs.serializer {
12721 quote!(.serializer::<#ser>())
12722 } else {
12723 quote!()
12724 };
12725
12726 let perms = &attrs.perms;
12727 let perms_call = if perms.list.is_empty()
12728 && perms.retrieve.is_empty()
12729 && perms.create.is_empty()
12730 && perms.update.is_empty()
12731 && perms.destroy.is_empty()
12732 {
12733 quote!()
12734 } else {
12735 let list_lits = perms.list.iter().map(|s| s.as_str());
12736 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12737 let create_lits = perms.create.iter().map(|s| s.as_str());
12738 let update_lits = perms.update.iter().map(|s| s.as_str());
12739 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12740 quote! {
12741 .permissions(#root::viewset::ViewSetPerms {
12742 list: ::std::vec![ #(#list_lits.to_owned()),* ],
12743 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12744 create: ::std::vec![ #(#create_lits.to_owned()),* ],
12745 update: ::std::vec![ #(#update_lits.to_owned()),* ],
12746 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
12747 })
12748 }
12749 };
12750
12751 Ok(quote! {
12752 impl #struct_name {
12753 /// Build an `axum::Router` with the six standard REST endpoints
12754 /// for this ViewSet, mounted at `prefix`.
12755 ///
12756 /// Accepts any backend's pool — a `sqlx::PgPool`,
12757 /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
12758 /// enum — because they all `Into<Pool>` (#1273). This is why
12759 /// the derive no longer names `PgPool`, which would not
12760 /// exist on a project built without the `postgres` feature.
12761 pub fn router(
12762 prefix: &str,
12763 pool: impl ::core::convert::Into<#root::sql::Pool>,
12764 ) -> #root::__axum::Router {
12765 #root::viewset::ViewSet::for_model(
12766 <#model_path as #root::core::Model>::SCHEMA
12767 )
12768 #fields_call
12769 #filter_fields_call
12770 #search_fields_call
12771 #ordering_call
12772 #page_size_call
12773 #perms_call
12774 #read_only_call
12775 #allow_anonymous_call
12776 #serializer_call
12777 .router_pool(prefix, pool.into())
12778 }
12779 }
12780 })
12781}
12782
12783fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
12784 let mut model: Option<syn::Path> = None;
12785 let mut fields: Option<Vec<String>> = None;
12786 let mut filter_fields: Vec<String> = Vec::new();
12787 let mut search_fields: Vec<String> = Vec::new();
12788 let mut ordering: Vec<(String, bool)> = Vec::new();
12789 let mut page_size: Option<usize> = None;
12790 let mut read_only = false;
12791 let mut allow_anonymous = false;
12792 let mut perms = ViewSetPermsAttrs::default();
12793 let mut serializer: Option<syn::Path> = None;
12794
12795 for attr in &input.attrs {
12796 if !attr.path().is_ident("viewset") {
12797 continue;
12798 }
12799 attr.parse_nested_meta(|meta| {
12800 if meta.path.is_ident("model") {
12801 let path: syn::Path = meta.value()?.parse()?;
12802 model = Some(path);
12803 return Ok(());
12804 }
12805 if meta.path.is_ident("serializer") {
12806 let path: syn::Path = meta.value()?.parse()?;
12807 serializer = Some(path);
12808 return Ok(());
12809 }
12810 if meta.path.is_ident("fields") {
12811 let s: LitStr = meta.value()?.parse()?;
12812 fields = Some(split_field_list(&s.value()));
12813 return Ok(());
12814 }
12815 if meta.path.is_ident("filter_fields") {
12816 let s: LitStr = meta.value()?.parse()?;
12817 filter_fields = split_field_list(&s.value());
12818 return Ok(());
12819 }
12820 if meta.path.is_ident("search_fields") {
12821 let s: LitStr = meta.value()?.parse()?;
12822 search_fields = split_field_list(&s.value());
12823 return Ok(());
12824 }
12825 if meta.path.is_ident("ordering") {
12826 let s: LitStr = meta.value()?.parse()?;
12827 ordering = parse_ordering_list(&s.value());
12828 return Ok(());
12829 }
12830 if meta.path.is_ident("page_size") {
12831 let lit: syn::LitInt = meta.value()?.parse()?;
12832 page_size = Some(lit.base10_parse::<usize>()?);
12833 return Ok(());
12834 }
12835 if meta.path.is_ident("read_only") {
12836 read_only = true;
12837 return Ok(());
12838 }
12839 if meta.path.is_ident("allow_anonymous") {
12840 allow_anonymous = true;
12841 return Ok(());
12842 }
12843 if meta.path.is_ident("permissions") {
12844 meta.parse_nested_meta(|inner| {
12845 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
12846 let s: LitStr = inner.value()?.parse()?;
12847 Ok(split_field_list(&s.value()))
12848 };
12849 if inner.path.is_ident("list") {
12850 perms.list = parse_codenames(&inner)?;
12851 } else if inner.path.is_ident("retrieve") {
12852 perms.retrieve = parse_codenames(&inner)?;
12853 } else if inner.path.is_ident("create") {
12854 perms.create = parse_codenames(&inner)?;
12855 } else if inner.path.is_ident("update") {
12856 perms.update = parse_codenames(&inner)?;
12857 } else if inner.path.is_ident("destroy") {
12858 perms.destroy = parse_codenames(&inner)?;
12859 } else {
12860 return Err(inner.error(
12861 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
12862 ));
12863 }
12864 Ok(())
12865 })?;
12866 return Ok(());
12867 }
12868 Err(meta.error(
12869 "unknown viewset attribute (supported: model, fields, filter_fields, \
12870 search_fields, ordering, page_size, read_only, allow_anonymous, serializer, \
12871 permissions(...))",
12872 ))
12873 })?;
12874 }
12875
12876 let model = model.ok_or_else(|| {
12877 syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
12878 })?;
12879
12880 Ok(ViewSetAttrs {
12881 model,
12882 fields,
12883 filter_fields,
12884 search_fields,
12885 ordering,
12886 page_size,
12887 read_only,
12888 allow_anonymous,
12889 perms,
12890 serializer,
12891 })
12892}
12893
12894// ============================================================ #[derive(Serializer)]
12895
12896struct SerializerContainerAttrs {
12897 model: syn::Path,
12898 /// `#[serializer(validate = "fn_name")]` on the struct — the
12899 /// cross-field validation hook (#436). The named inherent method
12900 /// must take `&self` and return
12901 /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
12902 /// `validate()` runs every per-field validator first; then calls
12903 /// the cross-field method if declared; aggregates all errors into
12904 /// one `FormErrors`.
12905 cross_validate: Option<syn::Ident>,
12906}
12907
12908#[derive(Default)]
12909struct SerializerFieldAttrs {
12910 read_only: bool,
12911 write_only: bool,
12912 source: Option<String>,
12913 skip: bool,
12914 /// `#[serializer(method = "fn_name")]` — computed field. The macro
12915 /// emits a `from_model` initializer that calls
12916 /// `Self::fn_name(&model)` and stores the return value.
12917 method: Option<String>,
12918 /// `#[serializer(validate = "fn_name")]` — per-field validator
12919 /// callable run by `Self::validate(&self)`. Must return
12920 /// `Result<(), String>`. Errors land in `FormErrors` keyed by
12921 /// the field name.
12922 validate: Option<String>,
12923 /// `#[serializer(nested)]` on a field whose type is another
12924 /// `Serializer` — the macro emits `from_model` initializer that
12925 /// reads the parent via `model.<source>.value()` then calls the
12926 /// child serializer's `from_model(parent)`. When the FK is
12927 /// unloaded the field falls back to `Default::default()` (does
12928 /// NOT panic) so a missing prefetch in prod degrades gracefully.
12929 /// Source field on the model defaults to the field name; override
12930 /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
12931 /// panic-on-unloaded behavior for tests.
12932 nested: bool,
12933 /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
12934 /// strict behavior: panic when the FK isn't loaded. Useful in
12935 /// test code where forgetting select_related must trip a hard
12936 /// failure rather than render a blank nested object.
12937 nested_strict: bool,
12938 /// `#[serializer(many = TagSerializer)]` — declare the field as
12939 /// a list of nested serializers. Field type must be `Vec<S>`
12940 /// where `S` is the inner serializer. The macro initializes the
12941 /// field to `Vec::new()` in `from_model` and emits a typed
12942 /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
12943 /// maps each model row through `S::from_model`. Auto-load isn't
12944 /// possible (the M2M / one-to-many accessor is async); callers
12945 /// fetch the children + call the setter post-from_model.
12946 many: Option<syn::Type>,
12947 /// `#[serializer(slug = "name")]` — render a relation as one of
12948 /// its fields.
12949 /// Source field on the model must be a `ForeignKey<T>`; the
12950 /// macro emits `from_model` glue that walks
12951 /// `model.<source>.value()?.<slug>` and clones it. Field type on
12952 /// the serializer is typically `String` (whatever type the slug
12953 /// column has). When the FK is unloaded the field falls back to
12954 /// `Default::default()`, same graceful-degrade contract as
12955 /// `nested`. Source defaults to the field name; override with
12956 /// `source = "..."`. v0.44.
12957 slug: Option<String>,
12958 /// `#[serializer(max_length = N)]` — caps
12959 /// the character count of a string field on write. Overrides the
12960 /// model's `max_length`; when absent the model value is inherited.
12961 max_length: Option<u64>,
12962 /// `#[serializer(min_length = N)]` — minimum character count.
12963 /// Serializer-only (the model has no `min_length` column).
12964 min_length: Option<u64>,
12965 /// `#[serializer(min = N)]` — inclusive
12966 /// integer lower bound; overrides the model's `min` when given.
12967 min: Option<i64>,
12968 /// `#[serializer(max = N)]` — inclusive
12969 /// integer upper bound; overrides the model's `max` when given.
12970 max: Option<i64>,
12971}
12972
12973fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
12974 let mut model: Option<syn::Path> = None;
12975 let mut cross_validate: Option<syn::Ident> = None;
12976 for attr in &input.attrs {
12977 if !attr.path().is_ident("serializer") {
12978 continue;
12979 }
12980 attr.parse_nested_meta(|meta| {
12981 if meta.path.is_ident("model") {
12982 let _eq: syn::Token![=] = meta.input.parse()?;
12983 model = Some(meta.input.parse()?);
12984 return Ok(());
12985 }
12986 if meta.path.is_ident("validate") {
12987 // #436 — container-level `validate = "fn_name"` for
12988 // cross-field validation. Field-level
12989 // `#[serializer(validate = "...")]` on a field is
12990 // parsed separately in `parse_serializer_field_attrs`.
12991 let s: LitStr = meta.value()?.parse()?;
12992 cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
12993 return Ok(());
12994 }
12995 Err(meta.error(
12996 "unknown serializer container attribute \
12997 (supported: `model`, `validate`)",
12998 ))
12999 })?;
13000 }
13001 let model = model.ok_or_else(|| {
13002 syn::Error::new_spanned(
13003 &input.ident,
13004 "`#[serializer(model = SomeModel)]` is required",
13005 )
13006 })?;
13007 Ok(SerializerContainerAttrs {
13008 model,
13009 cross_validate,
13010 })
13011}
13012
13013fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
13014 let mut out = SerializerFieldAttrs::default();
13015 for attr in &field.attrs {
13016 if !attr.path().is_ident("serializer") {
13017 continue;
13018 }
13019 attr.parse_nested_meta(|meta| {
13020 if meta.path.is_ident("read_only") {
13021 out.read_only = true;
13022 return Ok(());
13023 }
13024 if meta.path.is_ident("write_only") {
13025 out.write_only = true;
13026 return Ok(());
13027 }
13028 if meta.path.is_ident("skip") {
13029 out.skip = true;
13030 return Ok(());
13031 }
13032 if meta.path.is_ident("source") {
13033 let s: LitStr = meta.value()?.parse()?;
13034 out.source = Some(s.value());
13035 return Ok(());
13036 }
13037 if meta.path.is_ident("method") {
13038 let s: LitStr = meta.value()?.parse()?;
13039 out.method = Some(s.value());
13040 return Ok(());
13041 }
13042 if meta.path.is_ident("validate") {
13043 let s: LitStr = meta.value()?.parse()?;
13044 out.validate = Some(s.value());
13045 return Ok(());
13046 }
13047 if meta.path.is_ident("many") {
13048 let _eq: syn::Token![=] = meta.input.parse()?;
13049 out.many = Some(meta.input.parse()?);
13050 return Ok(());
13051 }
13052 if meta.path.is_ident("nested") {
13053 out.nested = true;
13054 // Optional strict flag inside parentheses:
13055 // #[serializer(nested(strict))]
13056 if meta.input.peek(syn::token::Paren) {
13057 meta.parse_nested_meta(|inner| {
13058 if inner.path.is_ident("strict") {
13059 out.nested_strict = true;
13060 return Ok(());
13061 }
13062 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
13063 })?;
13064 }
13065 return Ok(());
13066 }
13067 if meta.path.is_ident("slug") {
13068 let s: LitStr = meta.value()?.parse()?;
13069 out.slug = Some(s.value());
13070 return Ok(());
13071 }
13072 if meta.path.is_ident("max_length") {
13073 let lit: syn::LitInt = meta.value()?.parse()?;
13074 out.max_length = Some(lit.base10_parse::<u64>()?);
13075 return Ok(());
13076 }
13077 if meta.path.is_ident("min_length") {
13078 let lit: syn::LitInt = meta.value()?.parse()?;
13079 out.min_length = Some(lit.base10_parse::<u64>()?);
13080 return Ok(());
13081 }
13082 if meta.path.is_ident("min") {
13083 let lit: syn::LitInt = meta.value()?.parse()?;
13084 out.min = Some(lit.base10_parse::<i64>()?);
13085 return Ok(());
13086 }
13087 if meta.path.is_ident("max") {
13088 let lit: syn::LitInt = meta.value()?.parse()?;
13089 out.max = Some(lit.base10_parse::<i64>()?);
13090 return Ok(());
13091 }
13092 Err(meta.error(
13093 "unknown serializer field attribute (supported: \
13094 `read_only`, `write_only`, `source`, `skip`, `method`, \
13095 `validate`, `nested`, `many`, `slug`, `max_length`, \
13096 `min_length`, `min`, `max`)",
13097 ))
13098 })?;
13099 }
13100 // Validate: read_only + write_only is nonsensical
13101 if out.read_only && out.write_only {
13102 return Err(syn::Error::new_spanned(
13103 field,
13104 "a field cannot be both `read_only` and `write_only`",
13105 ));
13106 }
13107 if out.method.is_some() && out.source.is_some() {
13108 return Err(syn::Error::new_spanned(
13109 field,
13110 "`method` and `source` are mutually exclusive — `method` computes \
13111 the value from a method, `source` reads it from a different model field",
13112 ));
13113 }
13114 if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
13115 return Err(syn::Error::new_spanned(
13116 field,
13117 "`slug` is mutually exclusive with `method`, `nested`, and `many` \
13118 — pick one strategy for populating the field",
13119 ));
13120 }
13121 Ok(out)
13122}
13123
13124fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
13125 let root = rustango_root();
13126 let struct_name = &input.ident;
13127 let struct_name_lit = struct_name.to_string();
13128
13129 let Data::Struct(data) = &input.data else {
13130 return Err(syn::Error::new_spanned(
13131 struct_name,
13132 "Serializer can only be derived on structs",
13133 ));
13134 };
13135 let Fields::Named(named) = &data.fields else {
13136 return Err(syn::Error::new_spanned(
13137 struct_name,
13138 "Serializer requires a struct with named fields",
13139 ));
13140 };
13141
13142 let container = parse_serializer_container_attrs(input)?;
13143 let model_path = &container.model;
13144
13145 // Classify each field. `ty` is only consumed by the
13146 // `#[cfg(feature = "openapi")]` block below, but we always
13147 // capture it to keep the field-info build a single pass.
13148 #[allow(dead_code)]
13149 struct FieldInfo {
13150 ident: syn::Ident,
13151 ty: syn::Type,
13152 attrs: SerializerFieldAttrs,
13153 }
13154 let mut fields_info: Vec<FieldInfo> = Vec::new();
13155 for field in &named.named {
13156 let ident = field.ident.clone().expect("named field has ident");
13157 let attrs = parse_serializer_field_attrs(field)?;
13158 fields_info.push(FieldInfo {
13159 ident,
13160 ty: field.ty.clone(),
13161 attrs,
13162 });
13163 }
13164
13165 // Generate from_model body: struct literal with each field assigned.
13166 let from_model_fields = fields_info.iter().map(|fi| {
13167 let ident = &fi.ident;
13168 let ty = &fi.ty;
13169 if let Some(_inner) = &fi.attrs.many {
13170 // Many — collection field. Initialize empty; caller
13171 // populates via the macro-emitted set_<field> helper
13172 // after fetching the M2M children.
13173 quote! { #ident: ::std::vec::Vec::new() }
13174 } else if let Some(method) = &fi.attrs.method {
13175 // SerializerMethodField: call Self::<method>(&model) to
13176 // compute the value. Method signature must be
13177 // `fn <method>(model: &T) -> <field type>`.
13178 let method_ident = syn::Ident::new(method, ident.span());
13179 quote! { #ident: Self::#method_ident(model) }
13180 } else if let Some(slug_field) = &fi.attrs.slug {
13181 // v0.44 — SlugRelatedField. Source defaults to the field
13182 // name on this struct; override via `source = "..."`. The
13183 // source field on the model is expected to be a
13184 // `ForeignKey<T>`; the slug field on the parent is named
13185 // by the attribute value. When the FK is unloaded the
13186 // field falls back to `Default::default()` — same
13187 // graceful-degrade contract as `nested`.
13188 let src_name = fi
13189 .attrs
13190 .source
13191 .as_deref()
13192 .unwrap_or(&ident_name(&fi.ident))
13193 .to_owned();
13194 let src_ident = ident_from_name(&src_name, ident.span());
13195 let slug_ident = syn::Ident::new(slug_field, ident.span());
13196 quote! {
13197 #ident: match model.#src_ident.value() {
13198 ::core::option::Option::Some(__loaded) =>
13199 ::core::clone::Clone::clone(&__loaded.#slug_ident),
13200 ::core::option::Option::None =>
13201 ::core::default::Default::default(),
13202 }
13203 }
13204 } else if fi.attrs.nested {
13205 // Nested serializer. Source defaults to the field name on
13206 // this struct; override via `source = "..."`. The source
13207 // field on the model is expected to be a `ForeignKey<T>`
13208 // whose `.value()` returns `Option<&T>` after lazy-load.
13209 //
13210 // Behavior matrix (tweakable per-field):
13211 // * FK loaded → nested object materializes via
13212 // ChildSerializer::from_model(parent).
13213 // * FK unloaded → fall back to ChildSerializer::default()
13214 // (so prod doesn't crash on a missing
13215 // prefetch — just renders a blank nested
13216 // object). Add `#[serializer(nested,
13217 // strict)]` to keep the v0.18.1
13218 // panic-on-unloaded behavior for tests
13219 // that want hard guardrails.
13220 let src_name = fi.attrs.source.as_deref().unwrap_or(&ident_name(&fi.ident)).to_owned();
13221 let src_ident = ident_from_name(&src_name, ident.span());
13222 if fi.attrs.nested_strict {
13223 let panic_msg = format!(
13224 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
13225 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
13226 );
13227 quote! {
13228 #ident: <#ty as #root::serializer::ModelSerializer>::from_model(
13229 model.#src_ident.value().expect(#panic_msg),
13230 )
13231 }
13232 } else {
13233 quote! {
13234 #ident: match model.#src_ident.value() {
13235 ::core::option::Option::Some(__loaded) =>
13236 <#ty as #root::serializer::ModelSerializer>::from_model(__loaded),
13237 ::core::option::Option::None =>
13238 ::core::default::Default::default(),
13239 }
13240 }
13241 }
13242 } else if fi.attrs.write_only || fi.attrs.skip {
13243 // Not read from model — use default
13244 quote! { #ident: ::core::default::Default::default() }
13245 } else if let Some(src) = &fi.attrs.source {
13246 let src_ident = ident_from_name(src, ident.span());
13247 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13248 } else {
13249 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13250 }
13251 });
13252
13253 // is_writable predicate (also used by writable_lits /
13254 // writable_source_lits below).
13255 let is_writable = |fi: &&FieldInfo| {
13256 !fi.attrs.read_only
13257 && !fi.attrs.skip
13258 && fi.attrs.method.is_none()
13259 && !fi.attrs.nested
13260 && fi.attrs.many.is_none()
13261 && fi.attrs.slug.is_none()
13262 };
13263
13264 // Declarative field constraints: one block
13265 // per writable field, run inside `validate()`. Each resolves its
13266 // bounds as the serializer attr when given (`#[serializer(max_length
13267 // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13268 // `max` / `choices`), then dispatches on the field's JSON value via
13269 // `forms::validators::check_value` (string → length/choices, integer
13270 // → min/max).
13271 let opt_usize = |v: Option<u64>| match v {
13272 Some(n) => {
13273 let n = n as usize;
13274 quote!(::core::option::Option::Some(#n))
13275 }
13276 None => quote!(::core::option::Option::None),
13277 };
13278 let opt_i64 = |v: Option<i64>| match v {
13279 Some(n) => quote!(::core::option::Option::Some(#n)),
13280 None => quote!(::core::option::Option::None),
13281 };
13282 let constraint_blocks: Vec<_> = fields_info
13283 .iter()
13284 .filter(is_writable)
13285 .map(|fi| {
13286 let ident = &fi.ident;
13287 let fname = ident_name(ident);
13288 let mname = fi
13289 .attrs
13290 .source
13291 .clone()
13292 .unwrap_or_else(|| ident_name(&fi.ident));
13293 let attr_max_len = opt_usize(fi.attrs.max_length);
13294 let attr_min_len = opt_usize(fi.attrs.min_length);
13295 let attr_min = opt_i64(fi.attrs.min);
13296 let attr_max = opt_i64(fi.attrs.max);
13297 quote! {
13298 if __sent(#fname) {
13299 let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13300 let __max_length: ::core::option::Option<usize> = #attr_max_len
13301 .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13302 let __min_length: ::core::option::Option<usize> = #attr_min_len;
13303 let __min: ::core::option::Option<i64> =
13304 #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13305 let __max: ::core::option::Option<i64> =
13306 #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13307 let __choices = __sf.and_then(|__f| __f.choices);
13308 let __v = #root::__serde_json::to_value(&self.#ident)
13309 .unwrap_or(#root::__serde_json::Value::Null);
13310 #root::forms::validators::check_value(
13311 #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13312 &mut __errors,
13313 );
13314 }
13315 }
13316 })
13317 .collect();
13318 let has_constraints = !constraint_blocks.is_empty();
13319
13320 // Per-field validators. Emit a
13321 // `validate(&self)` method that runs each user-defined validator
13322 // and aggregates errors into `FormErrors`.
13323 let validator_calls: Vec<_> = fields_info
13324 .iter()
13325 .filter_map(|fi| {
13326 let ident = &fi.ident;
13327 let name_lit = ident_name(ident);
13328 let method = fi.attrs.validate.as_ref()?;
13329 let method_ident = syn::Ident::new(method, ident.span());
13330 Some(quote! {
13331 if __sent(#name_lit) {
13332 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13333 __errors.add(#name_lit.to_owned(), __e);
13334 }
13335 }
13336 })
13337 })
13338 .collect();
13339 // #436 — cross-field `validate(&self)`. If the
13340 // container declared `#[serializer(validate = "fn_name")]`,
13341 // the macro-generated `validate(&self)` runs every per-field
13342 // validator first, then calls the user's cross-field method,
13343 // merging its `FormErrors` into the per-field errors. Either
13344 // alone is enough to emit the wrapper.
13345 let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13346 quote! {
13347 // Merge cross-field errors into the per-field bucket so
13348 // a single .validate() call surfaces both layers.
13349 if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13350 __errors.merge(__cross);
13351 }
13352 }
13353 });
13354 let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13355 // Anything to run? Declarative constraints (for any writable field) +
13356 // per-field validators + cross-field hook.
13357 let has_run_validations = has_validators || has_constraints;
13358 // Shared body: declarative field constraints first (length / range /
13359 // choices, serializer-attr-or-model), then per-field validators, then
13360 // the cross-field hook. Field rules run only where `__sent(name)` (#1995).
13361 let checks = quote! {
13362 let mut __errors = #root::forms::FormErrors::default();
13363 #( #constraint_blocks )*
13364 #( #validator_calls )*
13365 #cross_validate_call
13366 if __errors.is_empty() {
13367 ::core::result::Result::Ok(())
13368 } else {
13369 ::core::result::Result::Err(__errors)
13370 }
13371 };
13372 let validate_body = quote! {
13373 let __sent = |_: &str| true;
13374 #checks
13375 };
13376 // PATCH: only the keys the body carries.
13377 let validate_patch_body = quote! {
13378 let __sent = |__n: &str| __obj.is_some_and(|__o| __o.contains_key(__n));
13379 #checks
13380 };
13381 // Inherent `validate(&self)` — kept for back-compat with direct
13382 // `serializer.validate()` calls that don't import `ModelSerializer`.
13383 // Emitted only when the serializer declares per-field/cross-field
13384 // validators (unchanged rule) so it never collides with a
13385 // hand-written inherent `validate`.
13386 let validate_method = if has_validators {
13387 quote! {
13388 impl #struct_name {
13389 /// Run the declarative field constraints, every
13390 /// `#[serializer(validate = "...")]` per-field validator,
13391 /// and (when declared) the container-level cross-field
13392 /// validator. Aggregates errors into `FormErrors` keyed by
13393 /// the field name. Returns `Ok(())` when all pass.
13394 pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13395 #validate_body
13396 }
13397 }
13398 }
13399 } else {
13400 quote! {}
13401 };
13402 // Trait-method override so the type-erased ViewSet write path can
13403 // dispatch `ModelSerializer::validate` generically. Emitted whenever
13404 // there's anything to run (declarative constraints inherited from the
13405 // model count); otherwise the trait's default no-op is used.
13406 let trait_validate_override = if has_run_validations {
13407 quote! {
13408 fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13409 #validate_body
13410 }
13411 }
13412 } else {
13413 quote! {}
13414 };
13415
13416 // For every `#[serializer(many = S)]` field, emit a
13417 // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13418 // helper that maps the parents through `S::from_model`.
13419 let many_setters: Vec<_> = fields_info
13420 .iter()
13421 .filter_map(|fi| {
13422 let many_ty = fi.attrs.many.as_ref()?;
13423 let ident = &fi.ident;
13424 let setter = syn::Ident::new(&format!("set_{}", ident_name(ident)), ident.span());
13425 Some(quote! {
13426 /// Populate this `many` field by mapping each parent model
13427 /// through the inner serializer's `from_model`. Call after
13428 /// fetching the M2M / one-to-many children since
13429 /// `from_model` itself can't await an SQL query.
13430 pub fn #setter(
13431 &mut self,
13432 models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13433 ) -> &mut Self {
13434 self.#ident = models.iter()
13435 .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13436 .collect();
13437 self
13438 }
13439 })
13440 })
13441 .collect();
13442 let many_setters_impl = if many_setters.is_empty() {
13443 quote! {}
13444 } else {
13445 quote! {
13446 impl #struct_name {
13447 #( #many_setters )*
13448 }
13449 }
13450 };
13451
13452 // Generate custom Serialize: skip write_only fields
13453 let output_fields: Vec<_> = fields_info
13454 .iter()
13455 .filter(|fi| !fi.attrs.write_only)
13456 .collect();
13457 let output_field_count = output_fields.len();
13458 let serialize_fields = output_fields.iter().map(|fi| {
13459 let ident = &fi.ident;
13460 let name_lit = ident_name(ident);
13461 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13462 });
13463
13464 // writable_fields: normal + write_only.
13465 // Exclude:
13466 // - `read_only` — server-computed.
13467 // - `skip` — caller sets manually post-from_model.
13468 // - `method` — computed from a Self::fn(&model) call; accepting
13469 // it on write is meaningless.
13470 // - `nested` / `many` — populated from related-model data, not
13471 // from a field on the wire body.
13472 // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13473 // `many` in `writable_fields()`, which made the ViewSet write
13474 // path accept those fields from the JSON body and try to bind
13475 // them to the SQL UPDATE — a silent no-op at best, a type
13476 // mismatch at worst.
13477 // (`is_writable` is defined above, near the constraint codegen.)
13478 let writable_lits: Vec<_> = fields_info
13479 .iter()
13480 .filter(is_writable)
13481 .map(|fi| ident_name(&fi.ident))
13482 .collect();
13483
13484 // `writable_source_fields`: the MODEL field names of writable
13485 // serializer fields (source-resolved). The ViewSet write path skips
13486 // every model column NOT in this set, so `read_only` / `method` /
13487 // computed fields a client posts are ignored instead of written.
13488 let writable_source_lits: Vec<String> = fields_info
13489 .iter()
13490 .filter(is_writable)
13491 .map(|fi| {
13492 fi.attrs
13493 .source
13494 .clone()
13495 .unwrap_or_else(|| ident_name(&fi.ident))
13496 })
13497 .collect();
13498
13499 // `readable_source_fields`: model columns the JSON output copies
13500 // straight from the model, so `?ordering=` cannot sort on a column
13501 // the API never shows (#1845).
13502 let readable_source_lits: Vec<String> = fields_info
13503 .iter()
13504 .filter(|fi| {
13505 !fi.attrs.write_only
13506 && !fi.attrs.skip
13507 && fi.attrs.method.is_none()
13508 && !fi.attrs.nested
13509 && fi.attrs.many.is_none()
13510 && fi.attrs.slug.is_none()
13511 })
13512 .map(|fi| {
13513 fi.attrs
13514 .source
13515 .clone()
13516 .unwrap_or_else(|| ident_name(&fi.ident))
13517 })
13518 .collect();
13519
13520 // `from_writable_json`: build a partial instance for input
13521 // validation. Writable fields are parsed from the JSON body (keyed
13522 // by serializer field name); every other field defaults. Per-field
13523 // type errors collect into `FormErrors` keyed by the field name.
13524 // Construction is field-by-field (not `Self::default()`) so it works
13525 // for serializers regardless of a struct-level `Default`.
13526 let from_writable_json_inits: Vec<_> = fields_info
13527 .iter()
13528 .map(|fi| {
13529 let ident = &fi.ident;
13530 let fname = ident_name(ident);
13531 let ty = &fi.ty;
13532 if is_writable(&fi) {
13533 quote! {
13534 #ident: match __obj.and_then(|__o| __o.get(#fname)) {
13535 ::core::option::Option::Some(__v) => {
13536 match #root::__serde_json::from_value::<#ty>(
13537 ::core::clone::Clone::clone(__v),
13538 ) {
13539 ::core::result::Result::Ok(__x) => __x,
13540 ::core::result::Result::Err(__e) => {
13541 __errors.add(#fname.to_owned(), __e.to_string());
13542 ::core::default::Default::default()
13543 }
13544 }
13545 }
13546 ::core::option::Option::None => ::core::default::Default::default(),
13547 }
13548 }
13549 } else {
13550 quote! { #ident: ::core::default::Default::default() }
13551 }
13552 })
13553 .collect();
13554 // `validate_patch`: overlay each sent writable field on the stored row.
13555 let patch_overlays: Vec<_> = fields_info
13556 .iter()
13557 .filter(is_writable)
13558 .map(|fi| {
13559 let ident = &fi.ident;
13560 let fname = ident_name(ident);
13561 let ty = &fi.ty;
13562 quote! {
13563 if let ::core::option::Option::Some(__v) = __obj.and_then(|__o| __o.get(#fname)) {
13564 match #root::__serde_json::from_value::<#ty>(::core::clone::Clone::clone(__v)) {
13565 ::core::result::Result::Ok(__x) => __merged.#ident = __x,
13566 ::core::result::Result::Err(__e) => {
13567 __errors.add(#fname.to_owned(), __e.to_string());
13568 }
13569 }
13570 }
13571 }
13572 })
13573 .collect();
13574 let patch_validate_call = if has_run_validations {
13575 quote! { __merged.__rustango_validate_patch(__obj) }
13576 } else {
13577 quote! { let _ = &__merged; ::core::result::Result::Ok(()) }
13578 };
13579 let patch_validate_helper = if has_run_validations {
13580 quote! {
13581 impl #struct_name {
13582 #[doc(hidden)]
13583 fn __rustango_validate_patch(
13584 &self,
13585 __obj: ::core::option::Option<&#root::__serde_json::Map<::std::string::String, #root::__serde_json::Value>>,
13586 ) -> ::core::result::Result<(), #root::forms::FormErrors> {
13587 #validate_patch_body
13588 }
13589 }
13590 }
13591 } else {
13592 quote! {}
13593 };
13594
13595 // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
13596 // Only includes fields shown in JSON output (skips write_only). For each
13597 // `Option<T>` field, omit from `required` and add `.nullable()`.
13598 let openapi_impl = {
13599 #[cfg(feature = "openapi")]
13600 {
13601 let property_calls = output_fields.iter().map(|fi| {
13602 let ident = &fi.ident;
13603 let name_lit = ident_name(ident);
13604 let ty = &fi.ty;
13605 let nullable_call = if is_option(ty) {
13606 quote! { .nullable() }
13607 } else {
13608 quote! {}
13609 };
13610 quote! {
13611 .property(
13612 #name_lit,
13613 <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13614 #nullable_call,
13615 )
13616 }
13617 });
13618 let required_lits: Vec<_> = output_fields
13619 .iter()
13620 .filter(|fi| !is_option(&fi.ty))
13621 .map(|fi| ident_name(&fi.ident))
13622 .collect();
13623 quote! {
13624 impl #root::openapi::OpenApiSchema for #struct_name {
13625 fn openapi_schema() -> #root::openapi::Schema {
13626 #root::openapi::Schema::object()
13627 #( #property_calls )*
13628 .required([ #( #required_lits ),* ])
13629 }
13630 }
13631 }
13632 }
13633 #[cfg(not(feature = "openapi"))]
13634 {
13635 quote! {}
13636 }
13637 };
13638
13639 Ok(quote! {
13640 impl #root::serializer::ModelSerializer for #struct_name {
13641 type Model = #model_path;
13642
13643 fn from_model(model: &Self::Model) -> Self {
13644 Self {
13645 #( #from_model_fields ),*
13646 }
13647 }
13648
13649 fn writable_fields() -> &'static [&'static str] {
13650 &[ #( #writable_lits ),* ]
13651 }
13652
13653 fn writable_source_fields() -> &'static [&'static str] {
13654 &[ #( #writable_source_lits ),* ]
13655 }
13656
13657 fn readable_source_fields() -> &'static [&'static str] {
13658 &[ #( #readable_source_lits ),* ]
13659 }
13660
13661 fn from_writable_json(
13662 __body: &#root::__serde_json::Value,
13663 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
13664 let mut __errors = #root::forms::FormErrors::default();
13665 let __obj = __body.as_object();
13666 let __out = Self {
13667 #( #from_writable_json_inits ),*
13668 };
13669 if __errors.is_empty() {
13670 ::core::result::Result::Ok(__out)
13671 } else {
13672 ::core::result::Result::Err(__errors)
13673 }
13674 }
13675
13676 fn validate_patch(
13677 __stored: &Self::Model,
13678 __body: &#root::__serde_json::Value,
13679 ) -> ::core::result::Result<(), #root::forms::FormErrors> {
13680 let mut __errors = #root::forms::FormErrors::default();
13681 let __obj = __body.as_object();
13682 #[allow(unused_mut)]
13683 let mut __merged = <Self as #root::serializer::ModelSerializer>::from_model(__stored);
13684 #( #patch_overlays )*
13685 if !__errors.is_empty() {
13686 return ::core::result::Result::Err(__errors);
13687 }
13688 #patch_validate_call
13689 }
13690
13691 #trait_validate_override
13692 }
13693
13694 #patch_validate_helper
13695
13696 impl #root::__serde::Serialize for #struct_name {
13697 fn serialize<S>(&self, serializer: S)
13698 -> ::core::result::Result<S::Ok, S::Error>
13699 where
13700 S: #root::__serde::Serializer,
13701 {
13702 use #root::__serde::ser::SerializeStruct;
13703 let mut __state = serializer.serialize_struct(
13704 #struct_name_lit,
13705 #output_field_count,
13706 )?;
13707 #( #serialize_fields )*
13708 __state.end()
13709 }
13710 }
13711
13712 #openapi_impl
13713
13714 #validate_method
13715
13716 #many_setters_impl
13717 })
13718}
13719
13720/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
13721/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
13722/// when the feature is off.
13723#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
13724fn is_option(ty: &syn::Type) -> bool {
13725 if let syn::Type::Path(p) = ty {
13726 if let Some(last) = p.path.segments.last() {
13727 return last.ident == "Option";
13728 }
13729 }
13730 false
13731}
13732
13733#[cfg(test)]
13734mod main_attr_tests {
13735 use super::*;
13736
13737 fn expand(args: &str) -> String {
13738 let body = quote! { async fn main() { run().await } };
13739 expand_main(args.parse().unwrap(), body)
13740 .expect("expansion failed")
13741 .to_string()
13742 }
13743
13744 /// Asserts on the expansion rather than on `parse_logging`,
13745 /// because the defect was a subscriber reaching the output, not
13746 /// an argument being misread (#1465).
13747 #[test]
13748 fn logging_false_emits_no_subscriber() {
13749 let out = expand("logging = false");
13750 assert!(
13751 !out.contains("tracing_subscriber"),
13752 "`logging = false` must not install a subscriber:\n{out}",
13753 );
13754 assert!(
13755 out.contains("block_on"),
13756 "the runtime must still be built:\n{out}",
13757 );
13758 }
13759
13760 #[test]
13761 fn default_still_installs_a_subscriber() {
13762 // The control. Without it, an `expand_main` that dropped the
13763 // prologue unconditionally would pass the test above while
13764 // leaving every default project with no logging at all.
13765 let out = expand("");
13766 assert!(
13767 out.contains("tracing_subscriber"),
13768 "the default entrypoint must still install one:\n{out}",
13769 );
13770 }
13771
13772 #[test]
13773 fn logging_false_composes_with_flavor() {
13774 let out = expand("flavor = \"current_thread\", logging = false");
13775 assert!(!out.contains("tracing_subscriber"), "{out}");
13776 assert!(out.contains("new_current_thread"), "{out}");
13777 }
13778
13779 #[test]
13780 fn logging_true_is_the_default_shape() {
13781 // `logging = true` is accepted and means the default, so the
13782 // match must not fire on the substring `logging`.
13783 assert!(parse_logging(&"logging = true".parse().unwrap()));
13784 assert!(!parse_logging(&"logging=false".parse().unwrap()));
13785 }
13786}
13787
13788#[cfg(test)]
13789mod field_attr_tests {
13790 use super::*;
13791
13792 fn error_for(field: syn::Field) -> Option<String> {
13793 let attrs = parse_field_attrs(&field).unwrap();
13794 let kind = detect_type(&field.ty).unwrap().kind;
13795 check_bound_compatibility(&field, &attrs, kind)
13796 .err()
13797 .map(|e| e.to_string())
13798 }
13799
13800 /// `citext`, `vector` and `geometry` on the wrong type are errors, not no-ops (#1937).
13801 #[test]
13802 fn type_specific_attrs_reject_other_types() {
13803 let f: syn::FieldsNamed = syn::parse_quote!({
13804 #[rustango(citext)] a: i64,
13805 #[rustango(vector(dims = 3))] b: String,
13806 #[rustango(geometry(srid = 4326))] c: String,
13807 #[rustango(citext)] d: String,
13808 });
13809 let errs: Vec<_> = f.named.into_iter().map(error_for).collect();
13810 assert!(errs[0].as_deref().unwrap().contains("citext"), "{errs:?}");
13811 assert!(errs[1].as_deref().unwrap().contains("vector"), "{errs:?}");
13812 assert!(errs[2].as_deref().unwrap().contains("geometry"), "{errs:?}");
13813 assert_eq!(errs[3], None);
13814 }
13815
13816 /// A raw ident derives names without `r#`; this used to panic (#1937).
13817 #[test]
13818 fn raw_idents_derive_plain_names() {
13819 let ident: syn::Ident = syn::parse_quote!(r#type);
13820 assert_eq!(column_type_ident(&ident).to_string(), "type_col");
13821 assert_eq!(ident_from_name("type", ident.span()).to_string(), "r#type");
13822 }
13823}