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. `RUST_LOG` and
272/// `RUSTANGO__LOGGING__FORMAT` (e.g. `json`) come from the real
273/// environment, else from `./.env` (only those keys are read):
274///
275/// ```ignore
276/// #[rustango::main]
277/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
278/// rustango::server::Builder::from_env().await?
279/// .migrate("migrations").await?
280/// .api(my_app::urls::api())
281/// .seed_with(my_app::seed::run).await?
282/// .serve("0.0.0.0:8080").await
283/// }
284/// ```
285///
286/// Optional `flavor = "current_thread"` passes through to
287/// `#[tokio::main]`; default is the multi-threaded runtime.
288///
289/// `logging = false` suppresses the default subscriber, for apps
290/// that install their own. Needed whenever the body reaches
291/// `Cli::with_logging()` or `logging::Setup`: the subscriber this
292/// macro installs gets there first, and `tracing` keeps the first
293/// one, so the `[logging]` section would otherwise be read and
294/// discarded (#1465).
295///
296/// ```ignore
297/// #[rustango::main(logging = false)]
298/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
299/// rustango::manage::Cli::new()
300/// .with_settings_from_env()
301/// .with_logging() // now the first installer
302/// .run().await
303/// }
304/// ```
305///
306/// Pulls `tracing-subscriber` into the rustango crate behind the
307/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
308/// out get plain `#[tokio::main]` ergonomics without the dependency.
309#[proc_macro_attribute]
310pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
311 expand_main(args.into(), item.into())
312 .unwrap_or_else(syn::Error::into_compile_error)
313 .into()
314}
315
316fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
317 let mut input: syn::ItemFn = syn::parse2(item)?;
318 if input.sig.asyncness.is_none() {
319 return Err(syn::Error::new(
320 input.sig.ident.span(),
321 "`#[rustango::main]` must wrap an `async fn`",
322 ));
323 }
324
325 // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
326 // to `#[tokio::main]`. Tokio's proc-macro internally emits
327 // `::tokio::*` paths that resolve against the user crate's deps,
328 // so calling it through the rustango re-export still requires the
329 // user to add tokio to their own Cargo.toml. Building the
330 // runtime ourselves keeps the dep transitive through the
331 // `runtime` feature on rustango.
332 //
333 // Parse optional `flavor = "current_thread"` / `flavor =
334 // "multi_thread"` from the attribute args. Unknown args are
335 // tolerated (forward-compat with tokio's own arg surface).
336 let root = rustango_root();
337 let flavor = parse_flavor(&args);
338 let builder_call = match flavor {
339 Flavor::CurrentThread => quote! {
340 #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
341 },
342 Flavor::MultiThread => quote! {
343 #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
344 },
345 };
346
347 // Detach the user body and rewrite `main` as a sync fn that
348 // builds the runtime and blocks on the async body.
349 let user_body = input.block.clone();
350 input.sig.asyncness = None;
351
352 // `logging = false` drops the block entirely, so a later
353 // `Cli::with_logging()` is the first installer and its
354 // `[logging]` settings actually take effect (#1465).
355 let logging_prologue = if parse_logging(&args) {
356 quote! {
357 // `Setup`'s defaults, not a copy of them: filter, format and
358 // the `Color::Auto` rule live in one place (#1493).
359 // `logging` and this macro share the `runtime` gate.
360 // The real `RUST_LOG` still wins over `./.env`'s (#2204).
361 let _ = #root::logging::Setup::new()
362 .with_default_env_filter(#root::__private_runtime::main_default_filter())
363 .with_format(#root::__private_runtime::main_format())
364 .install();
365 }
366 } else {
367 quote! {}
368 };
369
370 input.block = syn::parse2(quote! {{
371 { #logging_prologue }
372 let __rt = #builder_call
373 .enable_all()
374 .build()
375 .expect("failed to build tokio runtime");
376 __rt.block_on(async move #user_body)
377 }})?;
378
379 Ok(quote! {
380 #input
381 })
382}
383
384enum Flavor {
385 MultiThread,
386 CurrentThread,
387}
388
389fn parse_flavor(args: &TokenStream2) -> Flavor {
390 // Cheap parser: look for the literal token sequence
391 // `flavor = "current_thread"`. Everything else (including
392 // bare `multi_thread` or no args) defaults to multi-thread.
393 let s = args.to_string();
394 if s.contains("current_thread") {
395 Flavor::CurrentThread
396 } else {
397 Flavor::MultiThread
398 }
399}
400
401/// `false` when the attribute carries `logging = false` — the caller
402/// installs their own subscriber, usually via `Cli::with_logging`
403/// (#1465). Same cheap token match as `parse_flavor`.
404fn parse_logging(args: &TokenStream2) -> bool {
405 !args.to_string().replace(' ', "").contains("logging=false")
406}
407
408/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
409struct QInput {
410 base_path: syn::Path,
411 field: syn::Ident,
412 value: syn::Expr,
413}
414
415impl syn::parse::Parse for QInput {
416 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
417 let base_path: syn::Path = input.parse()?;
418 input.parse::<syn::Token![.]>()?;
419 let field: syn::Ident = input.parse()?;
420 input.parse::<syn::Token![=]>()?;
421 let value: syn::Expr = input.parse()?;
422 Ok(QInput {
423 base_path,
424 field,
425 value,
426 })
427 }
428}
429
430fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
431 let q: QInput = syn::parse2(input)?;
432 let root = rustango_root();
433 let field_str = q.field.to_string();
434 let field_span = q.field.span();
435 let (base, suffix) = match field_str.find("__") {
436 Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
437 None => (field_str.as_str(), ""),
438 };
439 if base.is_empty() {
440 return Err(syn::Error::new(
441 field_span,
442 "Q!(): field name is empty before `__` suffix",
443 ));
444 }
445 let base_ident = syn::Ident::new(base, field_span);
446 let value = &q.value;
447 let path = &q.base_path;
448
449 // Most suffixes map directly to a Column method with the value
450 // forwarded unchanged. Some need value-shape massaging (wildcards
451 // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
452 // ISNULL). Unknown suffixes fail the build.
453 let expanded = match suffix {
454 "" | "exact" => quote! {
455 #root::core::Column::eq(#path::#base_ident, #value)
456 },
457 "ne" => quote! {
458 #root::core::Column::ne(#path::#base_ident, #value)
459 },
460 "gt" => quote! {
461 #root::core::Column::gt(#path::#base_ident, #value)
462 },
463 "gte" => quote! {
464 #root::core::Column::gte(#path::#base_ident, #value)
465 },
466 "lt" => quote! {
467 #root::core::Column::lt(#path::#base_ident, #value)
468 },
469 "lte" => quote! {
470 #root::core::Column::lte(#path::#base_ident, #value)
471 },
472 // The escaped-LIKE lookups delegate to the typed Column
473 // methods, which escape the value and pair it with the
474 // *Escaped ops so `%` / `_` match literally on every dialect
475 // (#1257) — the macro no longer owns any pattern wrapping.
476 "iexact" => quote! {
477 #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
478 },
479 "contains" => quote! {
480 #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
481 },
482 "icontains" => quote! {
483 #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
484 },
485 "startswith" => quote! {
486 #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
487 },
488 "istartswith" => quote! {
489 #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
490 },
491 "endswith" => quote! {
492 #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
493 },
494 "iendswith" => quote! {
495 #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
496 },
497 "in" => quote! {
498 #root::core::Column::is_in(#path::#base_ident, #value)
499 },
500 "not_in" => quote! {
501 #root::core::Column::not_in(#path::#base_ident, #value)
502 },
503 "isnull" => {
504 // Must be a bool literal at macro time so we can route to
505 // is_null vs is_not_null without a runtime branch.
506 let b = match value {
507 syn::Expr::Lit(syn::ExprLit {
508 lit: syn::Lit::Bool(b),
509 ..
510 }) => b.value(),
511 _ => {
512 return Err(syn::Error::new_spanned(
513 value,
514 "Q!(): `__isnull` requires a `true` or `false` literal",
515 ));
516 }
517 };
518 if b {
519 quote! { #root::core::Column::is_null(#path::#base_ident) }
520 } else {
521 quote! { #root::core::Column::is_not_null(#path::#base_ident) }
522 }
523 }
524 "between" => {
525 // Accept a tuple literal `(lo, hi)`.
526 let tuple = match value {
527 syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
528 _ => {
529 return Err(syn::Error::new_spanned(
530 value,
531 "Q!(): `__between` requires a tuple literal `(lo, hi)`",
532 ));
533 }
534 };
535 let lo = &tuple.elems[0];
536 let hi = &tuple.elems[1];
537 quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
538 }
539 "regex" => quote! {
540 #root::core::Column::regex(#path::#base_ident, #value)
541 },
542 "iregex" => quote! {
543 #root::core::Column::iregex(#path::#base_ident, #value)
544 },
545 _ => {
546 return Err(syn::Error::new(
547 field_span,
548 format!(
549 "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",
550 suffix
551 ),
552 ));
553 }
554 };
555 Ok(expanded)
556}
557
558fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
559 // Default to "./migrations" if invoked without args.
560 let path_str = if input.is_empty() {
561 "./migrations".to_string()
562 } else {
563 let lit: LitStr = syn::parse2(input)?;
564 lit.value()
565 };
566
567 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
568 syn::Error::new(
569 proc_macro2::Span::call_site(),
570 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
571 )
572 })?;
573 let abs = std::path::Path::new(&manifest).join(&path_str);
574
575 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
576 if abs.is_dir() {
577 let read = std::fs::read_dir(&abs).map_err(|e| {
578 syn::Error::new(
579 proc_macro2::Span::call_site(),
580 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
581 )
582 })?;
583 for entry in read.flatten() {
584 let path = entry.path();
585 if !path.is_file() {
586 continue;
587 }
588 if path.extension().and_then(|s| s.to_str()) != Some("json") {
589 continue;
590 }
591 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
592 continue;
593 };
594 entries.push((stem.to_owned(), path));
595 }
596 }
597 entries.sort_by(|a, b| a.0.cmp(&b.0));
598
599 // Compile-time chain validation: read each migration's JSON,
600 // pull `name` and `prev` (file-stem-keyed for the chain check),
601 // and verify every `prev` points to another migration in the
602 // slice. Mismatches between the file stem and the embedded
603 // `name` field — and broken `prev` chains — fail at MACRO
604 // EXPANSION time so a misshapen migration set never compiles.
605 //
606 // This is the v0.4 Slice 5 distinguisher: rustango's JSON
607 // migrations + a Rust proc-macro that reads them is a combo no
608 // other Rust web framework matches (Cot's are imperative Rust
609 // code, Loco's are SeaORM up/down, Rwf's are raw SQL — none have
610 // a static chain to validate).
611 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
612 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
613 for (stem, path) in &entries {
614 let raw = std::fs::read_to_string(path).map_err(|e| {
615 syn::Error::new(
616 proc_macro2::Span::call_site(),
617 format!(
618 "embed_migrations!: cannot read {} for chain validation: {e}",
619 path.display()
620 ),
621 )
622 })?;
623 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
624 syn::Error::new(
625 proc_macro2::Span::call_site(),
626 format!(
627 "embed_migrations!: {} is not valid JSON: {e}",
628 path.display()
629 ),
630 )
631 })?;
632 let name = json
633 .get("name")
634 .and_then(|v| v.as_str())
635 .ok_or_else(|| {
636 syn::Error::new(
637 proc_macro2::Span::call_site(),
638 format!(
639 "embed_migrations!: {} is missing the `name` field",
640 path.display()
641 ),
642 )
643 })?
644 .to_owned();
645 if name != *stem {
646 return Err(syn::Error::new(
647 proc_macro2::Span::call_site(),
648 format!(
649 "embed_migrations!: file stem `{stem}` does not match the migration's \
650 `name` field `{name}` — rename the file or fix the JSON",
651 ),
652 ));
653 }
654 let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
655 chain_names.push(name.clone());
656 prev_refs.push((name, prev));
657 }
658
659 let name_set: std::collections::HashSet<&str> =
660 chain_names.iter().map(String::as_str).collect();
661 for (name, prev) in &prev_refs {
662 if let Some(p) = prev {
663 if !name_set.contains(p.as_str()) {
664 return Err(syn::Error::new(
665 proc_macro2::Span::call_site(),
666 format!(
667 "embed_migrations!: broken migration chain — `{name}` declares \
668 prev=`{p}` but no migration with that name exists in {}",
669 abs.display()
670 ),
671 ));
672 }
673 }
674 }
675
676 let pairs: Vec<TokenStream2> = entries
677 .iter()
678 .map(|(name, path)| {
679 let path_lit = path.display().to_string();
680 quote! { (#name, ::core::include_str!(#path_lit)) }
681 })
682 .collect();
683
684 Ok(quote! {
685 {
686 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
687 __RUSTANGO_EMBEDDED
688 }
689 })
690}
691
692fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
693 let root = rustango_root();
694 let struct_name = &input.ident;
695
696 let Data::Struct(data) = &input.data else {
697 return Err(syn::Error::new_spanned(
698 struct_name,
699 "Model can only be derived on structs",
700 ));
701 };
702 let Fields::Named(named) = &data.fields else {
703 return Err(syn::Error::new_spanned(
704 struct_name,
705 "Model requires a struct with named fields",
706 ));
707 };
708
709 let container = parse_container_attrs(input)?;
710 let table = container
711 .table
712 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
713 let model_name = struct_name.to_string();
714
715 let collected = collect_fields(named, &table)?;
716
717 // Validate that #[rustango(display = "…")] names a real field.
718 if let Some((ref display, span)) = container.display {
719 if !collected.field_names.iter().any(|n| n == display) {
720 return Err(syn::Error::new(
721 span,
722 format!("`display = \"{display}\"` does not match any field on this struct"),
723 ));
724 }
725 }
726 let display = container.display.map(|(name, _)| name);
727 let app_label = container.app.clone();
728
729 // Validate admin field-name lists against declared field names.
730 // Note: `list_display` is intentionally NOT validated here. As of
731 // v0.32 it may also reference inventory-registered computed
732 // fields (via `register_admin_computed!`) whose existence the
733 // macro can't see at compile time — they're submitted from any
734 // crate that depends on rustango. The runtime list-view resolves
735 // unknown names against the inventory + silently drops the
736 // truly-bogus ones, which is the cheaper trade-off versus
737 // forcing a per-Model attr to opt out.
738 if let Some(admin) = &container.admin {
739 for (label, list) in [
740 ("search_fields", &admin.search_fields),
741 ("readonly_fields", &admin.readonly_fields),
742 ("list_filter", &admin.list_filter),
743 ] {
744 if let Some((names, span)) = list {
745 for name in names {
746 if !collected.field_names.iter().any(|n| n == name) {
747 return Err(syn::Error::new(
748 *span,
749 format!(
750 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
751 ),
752 ));
753 }
754 }
755 }
756 }
757 if let Some((pairs, span)) = &admin.ordering {
758 for (name, _) in pairs {
759 if !collected.field_names.iter().any(|n| n == name) {
760 return Err(syn::Error::new(
761 *span,
762 format!(
763 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
764 ),
765 ));
766 }
767 }
768 }
769 if let Some((groups, span)) = &admin.fieldsets {
770 for (_, fields) in groups {
771 for name in fields {
772 if !collected.field_names.iter().any(|n| n == name) {
773 return Err(syn::Error::new(
774 *span,
775 format!(
776 "`fieldsets`: \"{name}\" is not a declared field on this struct"
777 ),
778 ));
779 }
780 }
781 }
782 }
783 }
784 if let Some(audit) = &container.audit {
785 if let Some((names, span)) = &audit.track {
786 for name in names {
787 if !collected.field_names.iter().any(|n| n == name) {
788 return Err(syn::Error::new(
789 *span,
790 format!(
791 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
792 ),
793 ));
794 }
795 }
796 }
797 }
798
799 // Issue #291 / T2.5 — validate each `default_order` column name
800 // against the model's collected fields. Typos fail at macro-expand
801 // time, not at the database.
802 for (col, _desc, span) in &container.default_order {
803 if !collected.field_names.iter().any(|n| n == col) {
804 return Err(syn::Error::new(
805 *span,
806 format!(
807 "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
808 ),
809 ));
810 }
811 }
812
813 // Build the audit_track list for ModelSchema: None when no audit attr,
814 // Some(empty) when audit present without track, Some(names) when explicit.
815 let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
816 audit
817 .track
818 .as_ref()
819 .map(|(names, _)| names.clone())
820 .unwrap_or_default()
821 });
822
823 // The upsert conflict target: a container-level composite unique,
824 // never a field `index(unique)` or a partial index PG can't target.
825 let upsert_unique: Option<Vec<String>> = container
826 .indexes
827 .iter()
828 .find(|i| i.unique && i.where_clause.is_none() && !i.columns.is_empty())
829 .map(|i| i.columns.clone());
830
831 // Merge field-level indexes into the container's index list.
832 let mut all_indexes: Vec<IndexAttr> = container.indexes;
833 for field in &named.named {
834 let ident = field.ident.as_ref().expect("named");
835 // The same default column as `process_field` (#1937).
836 let col = ident_name(ident);
837 // Re-parse field attrs to check for index flag
838 if let Ok(fa) = parse_field_attrs(field) {
839 if fa.index {
840 let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
841 let auto_name = if fa.index_unique {
842 format!("{table}_{col_name}_uq_idx")
843 } else {
844 format!("{table}_{col_name}_idx")
845 };
846 all_indexes.push(IndexAttr {
847 name: fa.index_name.or(Some(auto_name)),
848 columns: vec![col_name],
849 unique: fa.index_unique,
850 method: fa.index_method,
851 where_clause: None,
852 include: Vec::new(),
853 });
854 }
855 }
856 }
857 // An unbounded String is LONGTEXT on MySQL, which no key can hold (1170, #2575).
858 for field in &named.named {
859 let (Ok(fa), Ok(det)) = (parse_field_attrs(field), detect_type(&field.ty)) else {
860 continue;
861 };
862 if det.kind != DetectedKind::String || fa.max_length.is_some() {
863 continue;
864 }
865 let col = fa
866 .column
867 .clone()
868 .unwrap_or_else(|| ident_name(field.ident.as_ref().expect("named")));
869 // MySQL indexes every foreign-key column too.
870 let relation = det.fk_inner.is_some() || fa.fk.is_some() || fa.o2o.is_some();
871 if fa.primary_key
872 || fa.unique
873 || relation
874 || all_indexes.iter().any(|i| i.columns.contains(&col))
875 {
876 return Err(syn::Error::new_spanned(
877 field,
878 "a primary-key, unique, indexed or foreign-key `String` needs \
879 `max_length`: MySQL cannot index an unbounded (LONGTEXT) column",
880 ));
881 }
882 }
883
884 let model_impl = model_impl_tokens(
885 struct_name,
886 &model_name,
887 &table,
888 display.as_deref(),
889 app_label.as_deref(),
890 container.admin.as_ref(),
891 &container.default_order,
892 &collected.field_schemas,
893 collected.soft_delete_column.as_deref(),
894 container.permissions,
895 audit_track_names.as_deref(),
896 &container.m2m,
897 &all_indexes,
898 &container.checks,
899 &container.excludes,
900 &container.composite_fks,
901 &container.generic_fks,
902 container.scope.as_deref(),
903 container.is_view,
904 container.verbose_name.as_deref(),
905 container.verbose_name_plural.as_deref(),
906 container.managed,
907 container.base_manager_name.as_deref(),
908 container.order_with_respect_to.as_deref(),
909 container.proxy,
910 &container.required_db_features,
911 container.required_db_vendor.as_deref(),
912 container.default_related_name.as_deref(),
913 container.db_table_comment.as_deref(),
914 container
915 .get_latest_by
916 .as_ref()
917 .map(|(c, d)| (c.as_str(), *d)),
918 &container.extra_permissions,
919 &container.default_permissions,
920 &container.global_scopes,
921 &container.reverse_has_relations,
922 &container.generic_has_relations,
923 );
924 let module_ident = column_module_ident(struct_name);
925 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
926 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
927 let track_set: Option<std::collections::HashSet<&str>> = audit
928 .track
929 .as_ref()
930 .map(|(names, _)| names.iter().map(String::as_str).collect());
931 collected
932 .column_entries
933 .iter()
934 .filter(|c| {
935 track_set
936 .as_ref()
937 .map_or(true, |s| s.contains(c.name.as_str()))
938 })
939 .collect()
940 });
941 let inherent_impl = inherent_impl_tokens(
942 struct_name,
943 &collected,
944 collected.primary_key.as_ref(),
945 &column_consts,
946 audited_fields.as_deref(),
947 upsert_unique.as_deref(),
948 &container.manager_fns,
949 );
950 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
951 let from_row_impl = from_row_impl_tokens(
952 struct_name,
953 &collected.from_row_inits,
954 &collected.from_row_inits_my,
955 );
956 let reverse_helpers = reverse_helper_tokens(
957 struct_name,
958 &collected.fk_relations,
959 container.default_related_name.as_deref(),
960 );
961 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
962 let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
963 // Issue #817 — `#[rustango(through(...))]` accessors.
964 let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
965 // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
966 let reverse_has_accessors =
967 reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
968 let generic_fk_accessors = generic_fk_accessor_tokens(
969 struct_name,
970 &container.generic_fks,
971 &collected.column_entries,
972 );
973
974 Ok(quote! {
975 #model_impl
976 #inherent_impl
977 #from_row_impl
978 #column_module
979 #reverse_helpers
980 #m2m_accessors
981 #generic_m2m_accessors
982 #through_accessors
983 #reverse_has_accessors
984 #generic_fk_accessors
985
986 #root::core::inventory::submit! {
987 // `module_path!()` evaluates at the registration site,
988 // so a Model declared in `crate::blog::models` records
989 // `"<crate>::blog::models"` and `resolved_app_label()`
990 // can infer "blog" without an explicit attribute.
991 #root::core::ModelEntry::new(
992 <#struct_name as #root::core::Model>::SCHEMA,
993 ::core::module_path!(),
994 )
995 .with_audited(
996 <#struct_name as #root::core::Model>::__rustango_audited_update,
997 <#struct_name as #root::core::Model>::__rustango_audited_delete,
998 )
999 .with_audited_create(<#struct_name as #root::core::Model>::__rustango_audited_create)
1000 .with_audited_update_record(
1001 <#struct_name as #root::core::Model>::__rustango_audited_update_record,
1002 )
1003 }
1004 })
1005}
1006
1007/// Guard before decoding a `select_related` target (#2293): a NULL nullable
1008/// FK leaves the relation unloaded; a set FK whose row is missing is an error.
1009fn join_guard_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1010 let root = rustango_root();
1011 let parent_ty = &rel.parent_type;
1012 let field_ident = &rel.field_ident;
1013 let null_fk = if rel.nullable {
1014 quote! {
1015 if #receiver.#field_ident.is_none() {
1016 return ::core::result::Result::Ok(false);
1017 }
1018 }
1019 } else {
1020 quote! {}
1021 };
1022 quote! {
1023 #null_fk
1024 #root::sql::__rustango_require_join(
1025 row, <#parent_ty as #root::core::Model>::SCHEMA, alias,
1026 )?;
1027 }
1028}
1029
1030/// A target an earlier chain already loaded (`a__b` before `a__c`) is
1031/// reused, and the deeper chain stitched into it; re-decoding it would
1032/// drop what that chain loaded.
1033fn loaded_reuse_tokens(
1034 rel: &FkRelation,
1035 receiver: &TokenStream2,
1036 load: &TokenStream2,
1037) -> TokenStream2 {
1038 let root = rustango_root();
1039 let field_ident = &rel.field_ident;
1040 let pattern = if rel.nullable {
1041 quote!(::core::option::Option::Some(#root::sql::ForeignKey::Loaded { value: __loaded, .. }))
1042 } else {
1043 quote!(#root::sql::ForeignKey::Loaded { value: __loaded, .. })
1044 };
1045 quote! {
1046 if let #pattern = &mut #receiver.#field_ident {
1047 if let ::core::option::Option::Some(__r) = __rest {
1048 let _ = #load(&mut **__loaded, row, __r, &__next_alias)?;
1049 }
1050 return ::core::result::Result::Ok(true);
1051 }
1052 }
1053}
1054
1055/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
1056/// matches `field_name` against the model's FK fields and, for a
1057/// match, decodes the FK target via the parent's macro-generated
1058/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
1059/// `ForeignKey::Loaded` on `self`.
1060///
1061/// Always emitted (with empty arms for FK-less models, which
1062/// return `Ok(false)` for any field name) so the `T: LoadRelated`
1063/// trait bound on `fetch_on` is universally satisfied — users
1064/// never have to think about implementing it.
1065fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1066 let root = rustango_root();
1067 let arms = fk_relations.iter().map(|rel| {
1068 let parent_ty = &rel.parent_type;
1069 let field_ident = &rel.field_ident;
1070 let fk_name = ident_name(field_ident);
1071 let fk_col = fk_name.as_str();
1072 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1073 let guard = join_guard_tokens(rel, "e!(self));
1074 let reuse = loaded_reuse_tokens(
1075 rel,
1076 "e!(self),
1077 "e!(#root::sql::LoadRelated::__rustango_load_related),
1078 );
1079 let assign = if rel.nullable {
1080 quote! {
1081 self.#field_ident = ::core::option::Option::Some(
1082 #root::sql::ForeignKey::loaded(_pk, _parent),
1083 );
1084 }
1085 } else {
1086 quote! {
1087 self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1088 }
1089 };
1090 quote! {
1091 #fk_col => {
1092 #guard
1093 #reuse
1094 let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
1095 // Audit #451 — multi-hop `select_related("a__b__c")`:
1096 // stitch the deeper relation onto this parent first,
1097 // decoding it at the accumulated `__next_alias`. The
1098 // parent type also impls `LoadRelated`, so this recurses
1099 // the FK chain to arbitrary depth.
1100 if let ::core::option::Option::Some(__r) = __rest {
1101 let _ = #root::sql::LoadRelated::__rustango_load_related(
1102 &mut _parent, row, __r, &__next_alias,
1103 )?;
1104 }
1105 // Loud-in-debug, default-in-release: a divergence
1106 // between the FK field's declared `K` (drives the
1107 // expected `SqlValue::<Variant>`) and the parent's
1108 // `__rustango_pk_value` output is a macro-internal
1109 // invariant break — surfacing the panic in dev
1110 // catches it before users hit silent PK=0 corruption.
1111 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1112 #root::core::SqlValue::#variant_ident(v) => v,
1113 _other => {
1114 ::core::debug_assert!(
1115 false,
1116 "rustango macro bug: load_related on FK `{}` expected \
1117 SqlValue::{} from parent's __rustango_pk_value but got \
1118 {:?} — file a bug at https://github.com/ujeenet/rustango",
1119 #fk_col,
1120 ::core::stringify!(#variant_ident),
1121 _other,
1122 );
1123 #default_expr
1124 }
1125 };
1126 #assign
1127 ::core::result::Result::Ok(true)
1128 }
1129 }
1130 });
1131 quote! {
1132 #[cfg(feature = "postgres")]
1133 impl #root::sql::LoadRelated for #struct_name {
1134 #[allow(unused_variables)]
1135 fn __rustango_load_related(
1136 &mut self,
1137 row: &#root::sql::sqlx::postgres::PgRow,
1138 field_name: &str,
1139 alias: &str,
1140 ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1141 // Audit #451 — split the multi-hop path: `base` is the FK
1142 // on THIS model, `__rest` (if any) is the remaining chain
1143 // to stitch onto the loaded parent. `__next_alias` is the
1144 // accumulated join alias the parent's columns live under
1145 // (`{alias}__{next-hop}`), matching `lower_select_related`.
1146 let (__base, __rest): (&str, ::core::option::Option<&str>) =
1147 match field_name.split_once("__") {
1148 ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1149 ::core::option::Option::None => (field_name, ::core::option::Option::None),
1150 };
1151 let __next_alias: ::std::string::String = match __rest {
1152 ::core::option::Option::Some(__r) => {
1153 let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1154 ::std::format!("{}__{}", alias, __rb)
1155 }
1156 ::core::option::Option::None => ::std::string::String::new(),
1157 };
1158 match __base {
1159 #( #arms )*
1160 _ => ::core::result::Result::Ok(false),
1161 }
1162 }
1163 }
1164 }
1165}
1166
1167/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1168/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1169/// which expands to a `LoadRelatedMy` impl when rustango is built with
1170/// the `mysql` feature, and to nothing otherwise. The decoded parent
1171/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1172/// decoder, also batch8) so the dual emission is symmetric across
1173/// backends.
1174fn load_related_impl_my_tokens(
1175 struct_name: &syn::Ident,
1176 fk_relations: &[FkRelation],
1177) -> TokenStream2 {
1178 let root = rustango_root();
1179 let arms = fk_relations.iter().map(|rel| {
1180 let parent_ty = &rel.parent_type;
1181 let field_ident = &rel.field_ident;
1182 let fk_name = ident_name(field_ident);
1183 let fk_col = fk_name.as_str();
1184 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1185 let guard = join_guard_tokens(rel, "e!(__self));
1186 let reuse = loaded_reuse_tokens(
1187 rel,
1188 "e!(__self),
1189 "e!(#root::sql::LoadRelatedMy::__rustango_load_related_my),
1190 );
1191 let assign = if rel.nullable {
1192 quote! {
1193 __self.#field_ident = ::core::option::Option::Some(
1194 #root::sql::ForeignKey::loaded(_pk, _parent),
1195 );
1196 }
1197 } else {
1198 quote! {
1199 __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1200 }
1201 };
1202 // `self` IS hygiene-tracked through macro_rules — emitted from
1203 // a different context than the `&mut self` parameter inside
1204 // the macro_rules-expanded fn. Pass it through as `__self`
1205 // and let the macro_rules rebind it to the receiver.
1206 quote! {
1207 #fk_col => {
1208 #guard
1209 #reuse
1210 let mut _parent: #parent_ty =
1211 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1212 // Audit #451 — multi-hop: stitch the deeper relation onto
1213 // the parent at the accumulated alias (see PG twin).
1214 if let ::core::option::Option::Some(__r) = __rest {
1215 let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1216 &mut _parent, row, __r, &__next_alias,
1217 )?;
1218 }
1219 // See note in `load_related_impl_tokens` (PG twin) —
1220 // the same loud-in-debug invariant guard.
1221 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1222 #root::core::SqlValue::#variant_ident(v) => v,
1223 _other => {
1224 ::core::debug_assert!(
1225 false,
1226 "rustango macro bug: load_related on FK `{}` expected \
1227 SqlValue::{} from parent's __rustango_pk_value but got \
1228 {:?} — file a bug at https://github.com/ujeenet/rustango",
1229 #fk_col,
1230 ::core::stringify!(#variant_ident),
1231 _other,
1232 );
1233 #default_expr
1234 }
1235 };
1236 #assign
1237 ::core::result::Result::Ok(true)
1238 }
1239 }
1240 });
1241 quote! {
1242 #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1243 #( #arms )*
1244 });
1245 }
1246}
1247
1248/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1249/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1250/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1251fn load_related_impl_sqlite_tokens(
1252 struct_name: &syn::Ident,
1253 fk_relations: &[FkRelation],
1254) -> TokenStream2 {
1255 let root = rustango_root();
1256 let arms = fk_relations.iter().map(|rel| {
1257 let parent_ty = &rel.parent_type;
1258 let field_ident = &rel.field_ident;
1259 let fk_name = ident_name(field_ident);
1260 let fk_col = fk_name.as_str();
1261 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1262 let guard = join_guard_tokens(rel, "e!(__self));
1263 let reuse = loaded_reuse_tokens(
1264 rel,
1265 "e!(__self),
1266 "e!(#root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite),
1267 );
1268 let assign = if rel.nullable {
1269 quote! {
1270 __self.#field_ident = ::core::option::Option::Some(
1271 #root::sql::ForeignKey::loaded(_pk, _parent),
1272 );
1273 }
1274 } else {
1275 quote! {
1276 __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1277 }
1278 };
1279 quote! {
1280 #fk_col => {
1281 #guard
1282 #reuse
1283 let mut _parent: #parent_ty =
1284 <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1285 // Audit #451 — multi-hop: stitch the deeper relation onto
1286 // the parent at the accumulated alias (see PG twin).
1287 if let ::core::option::Option::Some(__r) = __rest {
1288 let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1289 &mut _parent, row, __r, &__next_alias,
1290 )?;
1291 }
1292 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1293 #root::core::SqlValue::#variant_ident(v) => v,
1294 _other => {
1295 ::core::debug_assert!(
1296 false,
1297 "rustango macro bug: load_related on FK `{}` expected \
1298 SqlValue::{} from parent's __rustango_pk_value but got \
1299 {:?} — file a bug at https://github.com/ujeenet/rustango",
1300 #fk_col,
1301 ::core::stringify!(#variant_ident),
1302 _other,
1303 );
1304 #default_expr
1305 }
1306 };
1307 #assign
1308 ::core::result::Result::Ok(true)
1309 }
1310 }
1311 });
1312 quote! {
1313 #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1314 #( #arms )*
1315 });
1316 }
1317}
1318
1319/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1320/// matches `field_name` against the model's FK fields and returns
1321/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1322/// group children by parent PK.
1323///
1324/// Always emitted (with `_ => None` for FK-less models) so the
1325/// trait bound on `fetch_with_prefetch` is universally satisfied.
1326fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1327 let root = rustango_root();
1328 let arms = fk_relations.iter().map(|rel| {
1329 let field_ident = &rel.field_ident;
1330 let fk_name = ident_name(field_ident);
1331 let fk_col = fk_name.as_str();
1332 if rel.pk_kind == DetectedKind::I64 {
1333 // i64 FK — return the stored PK so prefetch_related can
1334 // group children by it. Nullable variant unwraps via
1335 // `as_ref().map(...)`: an unset (NULL) FK column yields
1336 // `None` and that child sits out of the grouping (correct
1337 // semantics — it has no parent to attach to).
1338 if rel.nullable {
1339 quote! {
1340 #fk_col => self.#field_ident
1341 .as_ref()
1342 .map(|fk| #root::sql::ForeignKey::pk(fk)),
1343 }
1344 } else {
1345 quote! {
1346 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1347 }
1348 }
1349 } else {
1350 // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1351 // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1352 // i64-keyed grouping path — the trait signature is
1353 // `Option<i64>` and a non-i64 PK can't lower into it.
1354 // The FK still works for everything else (CRUD, lazy
1355 // load via `.get()`, select_related JOINs); only the
1356 // bulk prefetch grouper needs the integer key.
1357 quote! {
1358 #fk_col => ::core::option::Option::None,
1359 }
1360 }
1361 });
1362 // PK-type-agnostic version: every FK arm emits an
1363 // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1364 // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1365 // opt OUT of the legacy i64 method (it returns None) but opt IN
1366 // here.
1367 let value_arms = fk_relations.iter().map(|rel| {
1368 let field_ident = &rel.field_ident;
1369 let fk_name = ident_name(field_ident);
1370 let fk_col = fk_name.as_str();
1371 if rel.nullable {
1372 quote! {
1373 #fk_col => self.#field_ident
1374 .as_ref()
1375 .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1376 #root::sql::ForeignKey::pk(fk)
1377 )),
1378 }
1379 } else {
1380 quote! {
1381 #fk_col => ::core::option::Option::Some(
1382 ::core::convert::Into::<#root::core::SqlValue>::into(
1383 self.#field_ident.pk()
1384 )
1385 ),
1386 }
1387 }
1388 });
1389 quote! {
1390 impl #root::sql::FkPkAccess for #struct_name {
1391 #[allow(unused_variables)]
1392 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1393 match field_name {
1394 #( #arms )*
1395 _ => ::core::option::Option::None,
1396 }
1397 }
1398 #[allow(unused_variables)]
1399 fn __rustango_fk_pk_value(
1400 &self,
1401 field_name: &str,
1402 ) -> ::core::option::Option<#root::core::SqlValue> {
1403 match field_name {
1404 #( #value_arms )*
1405 _ => ::core::option::Option::None,
1406 }
1407 }
1408 }
1409 }
1410}
1411
1412/// For every `ForeignKey<Parent>` field on `Child`, emit
1413/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1414/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
1415/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
1416/// — the canonical reverse-FK fetch. One round trip, no N+1.
1417///
1418/// **PG-only emission**: the accessor is bounded on
1419/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1420/// both of which are gated behind the `postgres` cargo feature. The
1421/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1422/// model derive itself compiles on tri-dialect / sqlite-only
1423/// downstream builds — the accessor just isn't materialised. A tri-
1424/// dialect `_set_pool` variant is a separate follow-up.
1425fn reverse_helper_tokens(
1426 child_ident: &syn::Ident,
1427 fk_relations: &[FkRelation],
1428 default_related_name: Option<&str>,
1429) -> TokenStream2 {
1430 let root = rustango_root();
1431 if fk_relations.is_empty() {
1432 return TokenStream2::new();
1433 }
1434 // Method-name resolution per FK (issue #816 + follow-up):
1435 // 1. Field-level `#[rustango(related_name = "...")]` on the FK
1436 // itself — wins over everything else.
1437 // 2. Container-level `default_related_name = "..."` on the
1438 // child. Applies to every FK on this model that didn't
1439 // override.
1440 // 3. Fallback: `<child_snake>_set`. `Post` → `post_set`,
1441 // `BlogComment` → `blog_comment_set`. Avoids English-plural
1442 // edge cases.
1443 //
1444 // The PG-on-executor variant keeps the resolved name; the
1445 // tri-dialect `_pool` variant appends `_pool` to it (matches the
1446 // framework's convention for the `&Pool` flavor of every helper).
1447 let default_pg_suffix = default_related_name
1448 .map(str::to_owned)
1449 .unwrap_or_else(|| format!("{}_set", to_snake_case(&ident_name(child_ident))));
1450 let impls = fk_relations.iter().map(|rel| {
1451 let pg_suffix = rel
1452 .related_name
1453 .clone()
1454 .unwrap_or_else(|| default_pg_suffix.clone());
1455 let pool_suffix = format!("{}_pool", pg_suffix);
1456 let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1457 let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1458 let parent_ty = &rel.parent_type;
1459 let fk_name = ident_name(&rel.field_ident);
1460 let fk_col = fk_name.as_str();
1461 let doc = format!(
1462 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1463 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1464 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1465 further `{child_ident}::objects()` filters via direct queryset use."
1466 );
1467 let pool_doc = format!(
1468 "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1469 [`#root::sql::Pool`] and dispatches per backend so the \
1470 reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1471 Use this from framework code that holds a `&Pool` (admin, \
1472 tenancy resolver, viewset handlers); reach for the executor- \
1473 bound variant when you already have a typed `sqlx::Executor`."
1474 );
1475 quote! {
1476 #[cfg(feature = "postgres")]
1477 impl #parent_ty {
1478 #[doc = #doc]
1479 ///
1480 /// # Errors
1481 /// Returns [`#root::sql::ExecError`] for SQL-writing
1482 /// or driver failures.
1483 pub async fn #pg_method_ident<'_c, _E>(
1484 &self,
1485 _executor: _E,
1486 ) -> ::core::result::Result<
1487 ::std::vec::Vec<#child_ident>,
1488 #root::sql::ExecError,
1489 >
1490 where
1491 _E: #root::sql::sqlx::Executor<
1492 '_c,
1493 Database = #root::sql::sqlx::Postgres,
1494 >,
1495 {
1496 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1497 #root::query::QuerySet::<#child_ident>::new()
1498 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1499 .fetch_on(_executor)
1500 .await
1501 }
1502 }
1503
1504 impl #parent_ty {
1505 #[doc = #pool_doc]
1506 ///
1507 /// # Errors
1508 /// Returns [`#root::sql::ExecError`] for SQL-writing
1509 /// or driver failures.
1510 pub async fn #pool_method_ident(
1511 &self,
1512 pool: &#root::sql::Pool,
1513 ) -> ::core::result::Result<
1514 ::std::vec::Vec<#child_ident>,
1515 #root::sql::ExecError,
1516 > {
1517 use #root::sql::FetcherPool as _;
1518 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1519 #root::query::QuerySet::<#child_ident>::new()
1520 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1521 .fetch(pool)
1522 .await
1523 }
1524 }
1525 }
1526 });
1527 quote! { #( #impls )* }
1528}
1529
1530/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1531/// relation declared on the model.
1532/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1533/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1534///
1535/// Closes #239 + #240 — `comment.content_object` /
1536/// `comment.content_object = post` ergonomics on top of the existing
1537/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1538///
1539/// `column_entries` is passed so we can resolve each `ct_column` /
1540/// `pk_column` SQL name back to its Rust field ident — the macro
1541/// only sees the column-side strings in the attribute, but the
1542/// emitted accessor needs to read the actual struct field.
1543fn generic_fk_accessor_tokens(
1544 struct_name: &syn::Ident,
1545 generic_fks: &[GenericFkAttr],
1546 column_entries: &[ColumnEntry],
1547) -> TokenStream2 {
1548 let root = rustango_root();
1549 if generic_fks.is_empty() {
1550 return TokenStream2::new();
1551 }
1552 let methods = generic_fks.iter().filter_map(|gfk| {
1553 // Resolve `ct_column` + `pk_column` to the struct's Rust
1554 // field idents. A typo (column name doesn't match any field)
1555 // emits no method for that registration — the user will see
1556 // the compiler reject the SCHEMA literal anyway, so there's
1557 // a clear error path without us double-reporting.
1558 let ct_ident = column_entries
1559 .iter()
1560 .find(|c| c.column == gfk.ct_column)
1561 .map(|c| c.ident.clone())?;
1562 let pk_ident = column_entries
1563 .iter()
1564 .find(|c| c.column == gfk.pk_column)
1565 .map(|c| c.ident.clone())?;
1566
1567 let accessor_ident =
1568 syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1569 let setter_ident =
1570 syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1571 let name_literal = gfk.name.as_str();
1572
1573 Some(quote! {
1574 #[doc = concat!(
1575 "Resolve the polymorphic `",
1576 #name_literal,
1577 "` relation. Reads `self.",
1578 stringify!(#ct_ident),
1579 "` + `self.",
1580 stringify!(#pk_ident),
1581 "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1582 "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1583 #name_literal,
1584 "\", ...))]`."
1585 )]
1586 pub async fn #accessor_ident(
1587 &self,
1588 pool: &#root::sql::Pool,
1589 ) -> ::core::result::Result<
1590 ::core::option::Option<#root::__serde_json::Value>,
1591 #root::sql::ExecError,
1592 > {
1593 let gfk = #root::contenttypes::GenericForeignKey::new(
1594 self.#ct_ident as i64,
1595 self.#pk_ident as i64,
1596 );
1597 gfk.get_object(pool).await
1598 }
1599
1600 #[doc = concat!(
1601 "Set the polymorphic `",
1602 #name_literal,
1603 "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1604 stringify!(#ct_ident),
1605 "` and `self.",
1606 stringify!(#pk_ident),
1607 "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1608 #name_literal,
1609 "\", ...))]`."
1610 )]
1611 pub async fn #setter_ident<T: #root::core::Model>(
1612 &mut self,
1613 pool: &#root::sql::Pool,
1614 target_pk: i64,
1615 ) -> ::core::result::Result<(), #root::sql::ExecError> {
1616 let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1617 pool,
1618 target_pk,
1619 ).await?;
1620 self.#ct_ident = gfk.content_type_id as _;
1621 self.#pk_ident = gfk.object_pk as _;
1622 ::core::result::Result::Ok(())
1623 }
1624 })
1625 });
1626 quote! {
1627 impl #struct_name {
1628 #( #methods )*
1629 }
1630 }
1631}
1632
1633fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1634 let root = rustango_root();
1635 if m2m_relations.is_empty() {
1636 return TokenStream2::new();
1637 }
1638 let methods = m2m_relations.iter().map(|rel| {
1639 let method_name = format!("{}_m2m", rel.name);
1640 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1641 let through = rel.through.as_str();
1642 let src_col = rel.src.as_str();
1643 let dst_col = rel.dst.as_str();
1644 quote! {
1645 pub fn #method_ident(&self) -> #root::sql::M2MManager {
1646 #root::sql::M2MManager {
1647 src_pk: self.__rustango_pk_value(),
1648 through: #through,
1649 src_col: #src_col,
1650 dst_col: #dst_col,
1651 }
1652 }
1653 }
1654 });
1655 quote! {
1656 impl #struct_name {
1657 #( #methods )*
1658 }
1659 }
1660}
1661
1662/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1663/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1664fn generic_m2m_accessor_tokens(
1665 struct_name: &syn::Ident,
1666 relations: &[GenericM2MAttr],
1667) -> TokenStream2 {
1668 let root = rustango_root();
1669 if relations.is_empty() {
1670 return TokenStream2::new();
1671 }
1672 let methods = relations.iter().map(|rel| {
1673 let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1674 let through = rel.through.as_str();
1675 let pk_col = rel.pk_column.as_str();
1676 let ct_col = rel.ct_column.as_str();
1677 let related_col = rel.related_column.as_str();
1678 quote! {
1679 pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1680 #root::sql::GenericM2MManager {
1681 src_pk: self.__rustango_pk_value(),
1682 src_schema: <Self as #root::core::Model>::SCHEMA,
1683 through: #through,
1684 pk_col: #pk_col,
1685 ct_col: #ct_col,
1686 dst_col: #related_col,
1687 }
1688 }
1689 }
1690 });
1691 quote! {
1692 impl #struct_name {
1693 #( #methods )*
1694 }
1695 }
1696}
1697
1698/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1699/// associated functions for each `#[rustango(reverse_has(...))]`
1700/// attribute. Issue #830.
1701///
1702/// The two emitted functions return ready-to-use `WhereExpr` nodes
1703/// that downstream callers drop into
1704/// `QuerySet::<Self>::where_raw(...)`:
1705///
1706/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1707/// <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1708/// `whereHas` parity (without the closure-style sub-predicate
1709/// refinement — that's a follow-up; users can layer additional
1710/// predicates by constructing the SelectQuery themselves and
1711/// calling `where_raw(exists(query))` from `crate::core::subquery`).
1712/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1713/// `whereDoesntHave` parity.
1714///
1715/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1716/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1717/// machinery threads the outer-table reference through automatically
1718/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1719fn reverse_has_accessor_tokens(
1720 struct_name: &syn::Ident,
1721 reverse_has_relations: &[ReverseHasAttr],
1722) -> TokenStream2 {
1723 let root = rustango_root();
1724 if reverse_has_relations.is_empty() {
1725 return TokenStream2::new();
1726 }
1727 let methods = reverse_has_relations.iter().map(|rel| {
1728 let exists_name = format!("{}_exists_expr", rel.name);
1729 let not_exists_name = format!("{}_not_exists_expr", rel.name);
1730 let count_name = format!("{}_count", rel.name);
1731 let fetch_name = format!("{}_fetch", rel.name);
1732 let first_name = format!("{}_first", rel.name);
1733 let pluck_name = format!("{}_pluck", rel.name);
1734 let accessor_name = rel.name.as_str();
1735 let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1736 let not_exists_ident = syn::Ident::new(¬_exists_name, struct_name.span());
1737 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1738 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1739 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1740 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1741 let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1742 let child = &rel.child;
1743 let child_fk_column = rel.child_fk_column.as_str();
1744 let self_pk_column = rel.self_pk_column.as_str();
1745 let exists_doc = format!(
1746 "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1747 FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1748 Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1749 filter to {struct_name}s with at least one matching child.",
1750 );
1751 let not_exists_doc = format!(
1752 "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1753 (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1754 <outer>.{self_pk_column})`. Drop into \
1755 `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1756 {struct_name}s with **no** matching child.",
1757 );
1758 let count_doc = format!(
1759 "Eloquent `$model->{name}->count()` analog — returns \
1760 the number of `{child}` rows whose `{child_fk_column}` \
1761 matches this `{struct_name}` instance's primary key. \
1762 Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1763 {child_fk_column} = <self.pk>`.",
1764 name = rel.name,
1765 );
1766 let accessor_doc = format!(
1767 "Eloquent `$model->{name}` accessor — returns a \
1768 `QuerySet<{child}>` filtered to rows whose \
1769 `{child_fk_column}` matches this `{struct_name}` \
1770 instance's primary key. **Chainable**: compose `.filter()` \
1771 / `.order_by()` / `.limit()` etc. on top, then call \
1772 `.fetch(&pool)` (the QuerySet trait method) when \
1773 done. For the simple \"fetch all\" hot path with no \
1774 further composition, prefer the bare-name \
1775 `{name}_fetch(&pool)` companion.",
1776 name = rel.name,
1777 );
1778 let fetch_doc = format!(
1779 "Eloquent `$model->{name}->get()` — bare-name hot-path \
1780 over `{name}(&self).fetch(&pool)`. Use this when \
1781 you don't need further `.filter()` / `.order_by()` \
1782 composition; falls back to the chainable accessor when \
1783 you do. Avoids the `_pool` suffix on the most common \
1784 call-site shape.",
1785 name = rel.name,
1786 );
1787 quote! {
1788 #[doc = #accessor_doc]
1789 pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1790 #root::query::QuerySet::<#child>::new()
1791 .filter(#child_fk_column, self.__rustango_pk_value())
1792 }
1793
1794 #[doc = #fetch_doc]
1795 pub async fn #fetch_ident(
1796 &self,
1797 pool: &#root::sql::Pool,
1798 ) -> ::core::result::Result<
1799 ::std::vec::Vec<#child>,
1800 #root::sql::ExecError,
1801 > {
1802 use #root::sql::FetcherPool as _;
1803 self.#accessor_ident().fetch(pool).await
1804 }
1805
1806 /// Eloquent `$model->relation->first()` / `hasOne`
1807 /// semantics — bare-name shortcut over
1808 /// `self.<name>().first(&pool)`. Returns `None` when no
1809 /// child rows match. Useful when the relation is
1810 /// nominally many-to-one in shape but at most one row is
1811 /// expected (latest comment, primary tag, etc.).
1812 pub async fn #first_ident(
1813 &self,
1814 pool: &#root::sql::Pool,
1815 ) -> ::core::result::Result<
1816 ::core::option::Option<#child>,
1817 #root::sql::ExecError,
1818 > {
1819 self.#accessor_ident().first(pool).await
1820 }
1821
1822 /// Eloquent `$model->relation->pluck($col)` — project a
1823 /// single column from the child rows into a `Vec<U>`.
1824 /// Skips the typed `Child` decode, which is cheaper when
1825 /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1826 pub async fn #pluck_ident<U>(
1827 &self,
1828 col: &'static str,
1829 pool: &#root::sql::Pool,
1830 ) -> ::core::result::Result<
1831 ::std::vec::Vec<U>,
1832 #root::sql::ExecError,
1833 >
1834 where
1835 U: #root::sql::FlatScalar,
1836 {
1837 self.#accessor_ident()
1838 .values_list_flat(col)
1839 .fetch::<U>(pool)
1840 .await
1841 }
1842
1843 #[doc = #exists_doc]
1844 pub fn #exists_ident() -> #root::core::WhereExpr {
1845 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1846 let child_schema =
1847 <#child as #root::core::Model>::SCHEMA;
1848 let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1849 lhs: Expr::Column(#child_fk_column),
1850 op: Op::Eq,
1851 rhs: Expr::OuterRef(#self_pk_column),
1852 });
1853 WhereExpr::Exists(::std::boxed::Box::new(inner))
1854 }
1855
1856 #[doc = #not_exists_doc]
1857 pub fn #not_exists_ident() -> #root::core::WhereExpr {
1858 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1859 let child_schema =
1860 <#child as #root::core::Model>::SCHEMA;
1861 let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1862 lhs: Expr::Column(#child_fk_column),
1863 op: Op::Eq,
1864 rhs: Expr::OuterRef(#self_pk_column),
1865 });
1866 WhereExpr::NotExists(::std::boxed::Box::new(inner))
1867 }
1868
1869 #[doc = #count_doc]
1870 pub async fn #count_ident(
1871 &self,
1872 pool: &#root::sql::Pool,
1873 ) -> ::core::result::Result<
1874 i64,
1875 #root::sql::ExecError,
1876 > {
1877 use #root::sql::CounterPool as _;
1878 #root::query::QuerySet::<#child>::new()
1879 .filter(#child_fk_column, self.__rustango_pk_value())
1880 .count(pool)
1881 .await
1882 }
1883 }
1884 });
1885 quote! {
1886 impl #struct_name {
1887 #( #methods )*
1888 }
1889 }
1890}
1891
1892/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1893/// `#[rustango(through(...))]` attribute. Issue #817.
1894///
1895/// Each accessor builds a correlated subquery via
1896/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1897/// intermediate table, filters on the FK column pointing at this
1898/// model, and projects the intermediate PK. The outer queryset
1899/// filters the far model by its FK-to-intermediate column being in
1900/// that set.
1901///
1902/// The returned `QuerySet<Far>` is **chainable** — the subquery
1903/// lives inside a `where_raw` clause so the user's later
1904/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1905/// disturb it.
1906///
1907/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1908/// SQLite — no LATERAL or backend-specific syntax involved.
1909fn through_accessor_tokens(
1910 struct_name: &syn::Ident,
1911 through_relations: &[ThroughAttr],
1912) -> TokenStream2 {
1913 let root = rustango_root();
1914 if through_relations.is_empty() {
1915 return TokenStream2::new();
1916 }
1917 let methods = through_relations.iter().map(|rel| {
1918 let method_name = format!("{}_through", rel.name);
1919 let count_name = format!("{}_through_count", rel.name);
1920 let fetch_name = format!("{}_through_fetch", rel.name);
1921 let first_name = format!("{}_through_first", rel.name);
1922 let pluck_name = format!("{}_through_pluck", rel.name);
1923 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1924 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1925 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1926 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1927 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1928 let far = &rel.far;
1929 let intermediate = &rel.intermediate;
1930 let far_fk_column = rel.far_fk_column.as_str();
1931 let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1932 let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1933 let doc = format!(
1934 "Eloquent `hasManyThrough` accessor — returns a \
1935 `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1936 instance through the intermediate `{intermediate}` table. \
1937 Generated SQL shape: \
1938 `… WHERE {far_fk_column} IN (SELECT \
1939 {intermediate_pk_column} FROM <{intermediate}> WHERE \
1940 {intermediate_fk_column} = self.pk)`. Chainable like any \
1941 other QuerySet — compose `.filter()` / `.order_by()` / \
1942 `.limit()` etc. on top.",
1943 );
1944 let count_doc = format!(
1945 "Eloquent `$model->{name}->count()` analog for the \
1946 through-relation — returns the number of `{far}` rows \
1947 reachable through `{intermediate}`. Equivalent to \
1948 `self.{name}_through().count(pool)` but spelled \
1949 as a bare instance method for parity with the \
1950 `reverse_has` `<name>_count` shape.",
1951 name = rel.name,
1952 );
1953 quote! {
1954 #[doc = #doc]
1955 pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1956 use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1957 let intermediate_schema =
1958 <#intermediate as #root::core::Model>::SCHEMA;
1959 let sub = SelectQuery::new(intermediate_schema)
1960 .where_clause(WhereExpr::Predicate(Filter::new(
1961 #intermediate_fk_column,
1962 Op::Eq,
1963 self.__rustango_pk_value(),
1964 )))
1965 .projection(::std::vec![#intermediate_pk_column]);
1966 #root::query::QuerySet::<#far>::new().where_raw(
1967 WhereExpr::InSubquery {
1968 column: #far_fk_column,
1969 negated: false,
1970 subquery: ::std::boxed::Box::new(sub),
1971 },
1972 )
1973 }
1974
1975 #[doc = #count_doc]
1976 pub async fn #count_ident(
1977 &self,
1978 pool: &#root::sql::Pool,
1979 ) -> ::core::result::Result<
1980 i64,
1981 #root::sql::ExecError,
1982 > {
1983 use #root::sql::CounterPool as _;
1984 self.#method_ident().count(pool).await
1985 }
1986
1987 /// Eloquent `$model->relation->get()` for the
1988 /// through-relation — bare-name hot-path over
1989 /// `self.<name>_through().fetch(&pool)`. Use this
1990 /// when you don't need further `.filter()` /
1991 /// `.order_by()` composition; falls back to the
1992 /// chainable accessor when you do. Avoids the `_pool`
1993 /// suffix on the most common call-site shape.
1994 pub async fn #fetch_ident(
1995 &self,
1996 pool: &#root::sql::Pool,
1997 ) -> ::core::result::Result<
1998 ::std::vec::Vec<#far>,
1999 #root::sql::ExecError,
2000 > {
2001 use #root::sql::FetcherPool as _;
2002 self.#method_ident().fetch(pool).await
2003 }
2004
2005 /// Eloquent `hasOneThrough` analog — bare-name shortcut
2006 /// over `self.<name>_through().first(&pool)`. Returns
2007 /// `None` when no far rows are reachable through the
2008 /// intermediate. Useful when at most one row is
2009 /// expected (latest comment by country, primary tag,
2010 /// etc.).
2011 pub async fn #first_ident(
2012 &self,
2013 pool: &#root::sql::Pool,
2014 ) -> ::core::result::Result<
2015 ::core::option::Option<#far>,
2016 #root::sql::ExecError,
2017 > {
2018 self.#method_ident().first(pool).await
2019 }
2020
2021 /// Pluck a single column from the far rows into `Vec<U>`
2022 /// — cheaper than the typed `<Far>` decode when only one
2023 /// scalar column is needed.
2024 pub async fn #pluck_ident<U>(
2025 &self,
2026 col: &'static str,
2027 pool: &#root::sql::Pool,
2028 ) -> ::core::result::Result<
2029 ::std::vec::Vec<U>,
2030 #root::sql::ExecError,
2031 >
2032 where
2033 U: #root::sql::FlatScalar,
2034 {
2035 self.#method_ident()
2036 .values_list_flat(col)
2037 .fetch::<U>(pool)
2038 .await
2039 }
2040 }
2041 });
2042 quote! {
2043 impl #struct_name {
2044 #( #methods )*
2045 }
2046 }
2047}
2048
2049struct ColumnEntry {
2050 /// The struct field ident, used both for the inherent const name on
2051 /// the model and for the inner column type's name.
2052 ident: syn::Ident,
2053 /// The struct's field type, used as `Column::Value`.
2054 value_ty: Type,
2055 /// Rust-side field name (e.g. `"id"`).
2056 name: String,
2057 /// SQL-side column name (e.g. `"user_id"`).
2058 column: String,
2059 /// `#root::core::FieldType::I64` etc.
2060 field_type_tokens: TokenStream2,
2061 /// A bare or optional `Uuid` field; MySQL decodes it from text.
2062 uuid_column: bool,
2063}
2064
2065struct CollectedFields {
2066 field_schemas: Vec<TokenStream2>,
2067 from_row_inits: Vec<TokenStream2>,
2068 /// Aliased counterparts of `from_row_inits` — read columns via
2069 /// `format!("{prefix}__{col}")` aliases so a Model can be
2070 /// decoded from a JOINed row's projected target columns.
2071 from_aliased_row_inits: Vec<TokenStream2>,
2072 /// MySQL twins of `from_row_inits` / `from_aliased_row_inits`.
2073 from_row_inits_my: Vec<TokenStream2>,
2074 from_aliased_row_inits_my: Vec<TokenStream2>,
2075 /// Static column-name list — used by the simple insert path
2076 /// (no `Auto<T>` fields). Aligned with `insert_values`.
2077 insert_columns: Vec<TokenStream2>,
2078 /// Static `Into<SqlValue>` expressions, one per field. Aligned
2079 /// with `insert_columns`. Used by the simple insert path only.
2080 insert_values: Vec<TokenStream2>,
2081 /// Per-field push expressions for the dynamic (Auto-aware)
2082 /// insert path. Each statement either unconditionally pushes
2083 /// `(column, value)` or, for an `Auto<T>` field, conditionally
2084 /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
2085 insert_pushes: Vec<TokenStream2>,
2086 /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
2087 /// when `has_auto == false`.
2088 returning_cols: Vec<TokenStream2>,
2089 /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
2090 /// field. Run after `insert_returning` to populate the model.
2091 auto_assigns: Vec<TokenStream2>,
2092 /// `(ident, column_literal)` pairs for every Auto field. Used by
2093 /// the bulk_insert codegen to rebuild assigns against `_row_mut`
2094 /// instead of `self`.
2095 auto_field_idents: Vec<(syn::Ident, String)>,
2096 /// #1028 — `(ident, column)` for each `generated_as` field. Drives
2097 /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
2098 /// back into the struct after insert (MySQL has no RETURNING → the
2099 /// field stays at its placeholder until the next read).
2100 generated_field_idents: Vec<(syn::Ident, String)>,
2101 /// First `Auto<T>` field the database fills, with its inner `T`: the
2102 /// MySQL `LAST_INSERT_ID()` target. Rust-filled ones never are (#1934).
2103 first_db_auto: Option<(syn::Ident, Type)>,
2104 /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2105 /// by the all-Auto-Unset bulk path (Auto cols dropped from
2106 /// `columns`).
2107 bulk_pushes_no_auto: Vec<TokenStream2>,
2108 /// Bulk-insert per-row pushes for **all fields including Auto**.
2109 /// Used by the all-Auto-Set bulk path (Auto col included with the
2110 /// caller-supplied value).
2111 bulk_pushes_all: Vec<TokenStream2>,
2112 /// Column-name literals for non-Auto fields only (paired with
2113 /// `bulk_pushes_no_auto`).
2114 bulk_columns_no_auto: Vec<TokenStream2>,
2115 /// Column-name literals for every field including Auto (paired
2116 /// with `bulk_pushes_all`).
2117 bulk_columns_all: Vec<TokenStream2>,
2118 /// Fills each `default_uuid_v7` field of `_row` in place, so a
2119 /// mutable bulk insert hands the PKs back (#1934).
2120 bulk_fill_in_place: Vec<TokenStream2>,
2121 /// Rust-filled `Auto` fields (`default_uuid_v7`, `auto_now(_add)`), reset when `insert_or_ignore` skips (#1937).
2122 rust_filled_idents: Vec<syn::Ident>,
2123 /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2124 /// + the loop that asserts every row matches. One pair per Auto
2125 /// field. Empty when `has_auto == false`.
2126 bulk_auto_uniformity: Vec<TokenStream2>,
2127 /// Identifier of the first Auto field, used as the witness for
2128 /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2129 first_auto_ident: Option<syn::Ident>,
2130 /// `true` if any field on the struct is `Auto<T>`.
2131 has_auto: bool,
2132 /// `true` when the primary-key field's Rust type is `Auto<T>`.
2133 /// Gates `save()` codegen — only Auto PKs let us infer
2134 /// insert-vs-update from the in-memory value.
2135 pk_is_auto: bool,
2136 /// `Assignment` constructors for every non-PK column. Drives the
2137 /// UPDATE branch of `save()`.
2138 update_assignments: Vec<TokenStream2>,
2139 /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2140 /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2141 upsert_update_columns: Vec<TokenStream2>,
2142 primary_key: Option<(syn::Ident, String)>,
2143 column_entries: Vec<ColumnEntry>,
2144 /// Rust-side field names, in declaration order. Used to validate
2145 /// container attributes like `display = "…"`.
2146 field_names: Vec<String>,
2147 /// FK fields on this child model. Drives the reverse-relation
2148 /// helper emit — for each FK, the macro adds an inherent
2149 /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2150 /// method on the parent type.
2151 fk_relations: Vec<FkRelation>,
2152 /// SQL column name of the `#[rustango(soft_delete)]` field, if
2153 /// the model has one. Drives emission of the `soft_delete_on` /
2154 /// `restore_on` inherent methods. At most one such column per
2155 /// model is allowed; collect_fields rejects duplicates.
2156 soft_delete_column: Option<String>,
2157 /// Rust field ident of the `#[rustango(soft_delete)]` field —
2158 /// companion to `soft_delete_column` for emitting predicates
2159 /// that need to read the field off `&self` (e.g. `trashed()`).
2160 soft_delete_field_ident: Option<syn::Ident>,
2161}
2162
2163#[derive(Clone)]
2164struct FkRelation {
2165 /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2166 /// helper is emitted as `impl <ParentType> { … }`.
2167 parent_type: Type,
2168 /// Rust field ident of the FK. Its name is the key for select_related,
2169 /// prefetch and filters; the SQL column may differ (#1936).
2170 field_ident: syn::Ident,
2171 /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2172 /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2173 /// default `ForeignKey<T>` (no explicit K); other kinds when the
2174 /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2175 pk_kind: DetectedKind,
2176 /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2177 /// FK column). Drives the `Some(...)` wrapping in load_related
2178 /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2179 /// the codegen matches the field's declared shape.
2180 nullable: bool,
2181 /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2182 /// override. When set, the reverse helper picks this name instead
2183 /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2184 /// #816 (issue's "Related" note re: per-FK override).
2185 related_name: Option<String>,
2186}
2187
2188fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2189 let root = rustango_root();
2190 let cap = named.named.len();
2191 let mut out = CollectedFields {
2192 field_schemas: Vec::with_capacity(cap),
2193 from_row_inits: Vec::with_capacity(cap),
2194 from_aliased_row_inits: Vec::with_capacity(cap),
2195 from_row_inits_my: Vec::with_capacity(cap),
2196 from_aliased_row_inits_my: Vec::with_capacity(cap),
2197 insert_columns: Vec::with_capacity(cap),
2198 insert_values: Vec::with_capacity(cap),
2199 insert_pushes: Vec::with_capacity(cap),
2200 returning_cols: Vec::new(),
2201 auto_assigns: Vec::new(),
2202 auto_field_idents: Vec::new(),
2203 generated_field_idents: Vec::new(),
2204 first_db_auto: None,
2205 bulk_pushes_no_auto: Vec::with_capacity(cap),
2206 bulk_pushes_all: Vec::with_capacity(cap),
2207 bulk_columns_no_auto: Vec::with_capacity(cap),
2208 bulk_columns_all: Vec::with_capacity(cap),
2209 bulk_fill_in_place: Vec::new(),
2210 rust_filled_idents: Vec::new(),
2211 bulk_auto_uniformity: Vec::new(),
2212 first_auto_ident: None,
2213 has_auto: false,
2214 pk_is_auto: false,
2215 update_assignments: Vec::with_capacity(cap),
2216 upsert_update_columns: Vec::with_capacity(cap),
2217 primary_key: None,
2218 column_entries: Vec::with_capacity(cap),
2219 field_names: Vec::with_capacity(cap),
2220 fk_relations: Vec::new(),
2221 soft_delete_column: None,
2222 soft_delete_field_ident: None,
2223 };
2224
2225 for field in &named.named {
2226 let info = process_field(field, table)?;
2227 out.field_names.push(ident_name(info.ident));
2228 out.field_schemas.push(info.schema);
2229 out.from_row_inits.push(info.from_row_init);
2230 out.from_aliased_row_inits.push(info.from_aliased_row_init);
2231 out.from_row_inits_my.push(info.from_row_init_my);
2232 out.from_aliased_row_inits_my
2233 .push(info.from_aliased_row_init_my);
2234 if let Some(parent_ty) = info.fk_inner.clone() {
2235 out.fk_relations.push(FkRelation {
2236 parent_type: parent_ty,
2237 field_ident: info.ident.clone(),
2238 pk_kind: info.fk_pk_kind,
2239 nullable: info.nullable,
2240 related_name: info.related_name.clone(),
2241 });
2242 }
2243 if info.soft_delete {
2244 if out.soft_delete_column.is_some() {
2245 return Err(syn::Error::new_spanned(
2246 field,
2247 "only one field may be marked `#[rustango(soft_delete)]`",
2248 ));
2249 }
2250 out.soft_delete_column = Some(info.column.clone());
2251 out.soft_delete_field_ident = Some(info.ident.clone());
2252 }
2253 let column = info.column.as_str();
2254 let ident = info.ident;
2255 // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2256 // skip every write path — Postgres recomputes the value
2257 // from EXPR. Push only the column-entry record (so typed
2258 // column constants still exist for filtering / projection)
2259 // and the schema literal (already pushed above) and move
2260 // on. No insert_columns/values, no insert_pushes, no
2261 // bulk_*, no update_assignments, no upsert_update_columns,
2262 // no returning_cols.
2263 if info.generated_as.is_some() {
2264 out.column_entries.push(ColumnEntry {
2265 ident: ident.clone(),
2266 value_ty: info.value_ty.clone(),
2267 name: ident_name(ident),
2268 column: info.column.clone(),
2269 field_type_tokens: info.field_type_tokens,
2270 uuid_column: info.uuid_column,
2271 });
2272 // #1028 — refresh the DB-computed value after insert on
2273 // RETURNING-capable backends. The column joins `returning_cols`
2274 // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2275 // ident is recorded so the `AssignAutoPkPool` impl decodes it
2276 // back into the struct. MySQL has no `INSERT … RETURNING`, so
2277 // it keeps the placeholder until the next read.
2278 out.returning_cols.push(quote!(#column));
2279 out.generated_field_idents
2280 .push((ident.clone(), info.column.clone()));
2281 continue;
2282 }
2283 out.insert_columns.push(quote!(#column));
2284 out.insert_values.push(quote! {
2285 ::core::convert::Into::<#root::core::SqlValue>::into(
2286 ::core::clone::Clone::clone(&self.#ident)
2287 )
2288 });
2289 if info.auto {
2290 out.has_auto = true;
2291 if out.first_auto_ident.is_none() {
2292 out.first_auto_ident = Some(ident.clone());
2293 }
2294 // `default_uuid_v7` (issue #823) generates the PK Rust-side
2295 // before binding, so the value is already in
2296 // `self.#ident` after the insert_push — RETURNING is
2297 // redundant. Skip adding this column to returning_cols /
2298 // auto_assigns / auto_field_idents to avoid (a) an
2299 // unnecessary RETURNING column on every dialect, and (b)
2300 // the MySQL `LAST_INSERT_ID()` path that can only fill an
2301 // integer PK.
2302 // `auto_now_add` / `auto_now` join `default_uuid_v7` here
2303 // for the same reason: they are filled Rust-side below and
2304 // bound, so the value is already in `self` and RETURNING
2305 // would be a redundant column on every dialect.
2306 if !info.default_uuid_v7 && !info.auto_now_add && !info.auto_now {
2307 if out.first_db_auto.is_none() {
2308 out.first_db_auto = auto_inner_type(info.value_ty)
2309 .cloned()
2310 .map(|ty| (ident.clone(), ty));
2311 }
2312 out.returning_cols.push(quote!(#column));
2313 out.auto_field_idents
2314 .push((ident.clone(), info.column.clone()));
2315 out.auto_assigns.push(quote! {
2316 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2317 });
2318 }
2319 if info.default_uuid_v7 {
2320 // Rust-side UUIDv7 generation (issue #823, Eloquent
2321 // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2322 // and bind; Auto::Set → bind the user's value. The
2323 // column is ALWAYS present in the INSERT statement —
2324 // no RETURNING / no DB DEFAULT needed.
2325 out.insert_pushes.push(quote! {
2326 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2327 self.#ident = #root::sql::Auto::Set(
2328 #root::__uuid::Uuid::now_v7(),
2329 );
2330 }
2331 if let #root::sql::Auto::Set(_v) = &self.#ident {
2332 _columns.push(#column);
2333 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2334 ::core::clone::Clone::clone(_v)
2335 ));
2336 }
2337 });
2338 } else if info.auto_now_add || info.auto_now {
2339 // Rust-side write timestamp (#1464), mirroring
2340 // `default_uuid_v7` above. `auto_now` has always bound
2341 // `Utc::now()` on UPDATE but not on INSERT, so the
2342 // first write of an `updated_at` took the DB default
2343 // while every later one took the clock — the same
2344 // column, filled two different ways.
2345 //
2346 // The default is the problem. `CREATE TABLE IF NOT
2347 // EXISTS` leaves an upgraded table's
2348 // `DEFAULT CURRENT_TIMESTAMP` in place, and SQLite's
2349 // ALTER TABLE grammar (RENAME/ADD/DROP) has no
2350 // statement that changes it — so every insert relying
2351 // on that default wrote the legacy shape forever, while
2352 // the migrate sweep converted the rows around it. The
2353 // column went permanently mixed and `ORDER BY` inverted
2354 // the admin audit log (#1616 rework review,
2355 // correctness-001 / dialects-005 / security-002).
2356 //
2357 // Binding it here makes the stale default unreachable
2358 // rather than trying to migrate it: the timestamp comes
2359 // from the application, not the database.
2360 out.insert_pushes.push(quote! {
2361 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2362 self.#ident = #root::sql::Auto::Set(
2363 #root::__chrono::Utc::now(),
2364 );
2365 }
2366 if let #root::sql::Auto::Set(_v) = &self.#ident {
2367 _columns.push(#column);
2368 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2369 ::core::clone::Clone::clone(_v)
2370 ));
2371 }
2372 });
2373 } else {
2374 out.insert_pushes.push(quote! {
2375 if let #root::sql::Auto::Set(_v) = &self.#ident {
2376 _columns.push(#column);
2377 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2378 ::core::clone::Clone::clone(_v)
2379 ));
2380 }
2381 });
2382 }
2383 // …except the timestamp columns, which must appear in BOTH
2384 // paths (#1464). The path is chosen by the *first* Auto
2385 // field — in practice the PK — so a bulk insert of rows
2386 // with an unset PK took the no-auto branch and dropped the
2387 // timestamp column, falling back to the DB default that
2388 // this change exists to stop depending on.
2389 //
2390 // `rows` is borrowed immutably here, so this fills per row
2391 // at push time rather than writing back into the struct. An
2392 // explicitly-Set value is still honoured; only `Unset`
2393 // takes the clock.
2394 // `default_uuid_v7` likewise: its column has no DB default (#1934).
2395 if info.default_uuid_v7 || info.auto_now_add || info.auto_now {
2396 out.rust_filled_idents.push(ident.clone());
2397 }
2398 let rust_fill = if info.default_uuid_v7 {
2399 Some(quote!(#root::__uuid::Uuid::now_v7()))
2400 } else if info.auto_now_add || info.auto_now {
2401 Some(quote!(#root::__chrono::Utc::now()))
2402 } else {
2403 None
2404 };
2405 // Bulk: Auto fields appear in the all-Set path. An unset filled
2406 // field takes its fill there too, not NULL (#1950).
2407 out.bulk_columns_all.push(quote!(#column));
2408 if let Some(fill) = &rust_fill {
2409 let push = quote! {
2410 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2411 match &_row.#ident {
2412 #root::sql::Auto::Set(_v) => ::core::clone::Clone::clone(_v),
2413 #root::sql::Auto::Unset => #fill,
2414 }
2415 ));
2416 };
2417 out.bulk_columns_no_auto.push(quote!(#column));
2418 out.bulk_pushes_no_auto.push(push.clone());
2419 out.bulk_pushes_all.push(push);
2420 } else {
2421 out.bulk_pushes_all.push(quote! {
2422 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2423 ::core::clone::Clone::clone(&_row.#ident)
2424 ));
2425 });
2426 }
2427 if info.default_uuid_v7 {
2428 out.bulk_fill_in_place.push(quote! {
2429 if matches!(&_row.#ident, #root::sql::Auto::Unset) {
2430 _row.#ident = #root::sql::Auto::Set(#root::__uuid::Uuid::now_v7());
2431 }
2432 });
2433 }
2434 // Uniformity check: every row's Auto state must match the
2435 // first row's. Mixed Set/Unset within one bulk_insert is
2436 // rejected here so the column list stays consistent.
2437 // A filled field may be unset on any row, so it is exempt.
2438 let ident_clone = ident.clone();
2439 if rust_fill.is_none() {
2440 out.bulk_auto_uniformity.push(quote! {
2441 for _r in rows.iter().skip(1) {
2442 if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2443 return ::core::result::Result::Err(
2444 #root::sql::ExecError::Sql(
2445 #root::sql::SqlError::BulkAutoMixed
2446 )
2447 );
2448 }
2449 }
2450 });
2451 }
2452 } else {
2453 out.insert_pushes.push(quote! {
2454 _columns.push(#column);
2455 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2456 ::core::clone::Clone::clone(&self.#ident)
2457 ));
2458 });
2459 // Bulk: non-Auto fields appear in BOTH paths.
2460 out.bulk_columns_no_auto.push(quote!(#column));
2461 out.bulk_columns_all.push(quote!(#column));
2462 let push_expr = quote! {
2463 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2464 ::core::clone::Clone::clone(&_row.#ident)
2465 ));
2466 };
2467 out.bulk_pushes_no_auto.push(push_expr.clone());
2468 out.bulk_pushes_all.push(push_expr);
2469 }
2470 if info.primary_key {
2471 if out.primary_key.is_some() {
2472 return Err(syn::Error::new_spanned(
2473 field,
2474 "only one field may be marked `#[rustango(primary_key)]`",
2475 ));
2476 }
2477 out.primary_key = Some((ident.clone(), info.column.clone()));
2478 if info.auto {
2479 out.pk_is_auto = true;
2480 }
2481 } else if info.auto_now_add {
2482 // Immutable post-insert: skip from UPDATE entirely.
2483 } else if info.auto_now {
2484 // `auto_now` columns: bind `chrono::Utc::now()` on every
2485 // UPDATE so the column is always overridden with the
2486 // wall-clock at write time, regardless of what value the
2487 // user left in the struct field.
2488 out.update_assignments.push(quote! {
2489 #root::core::Assignment::new(
2490 #column,
2491 ::core::convert::Into::<#root::core::SqlValue>::into(
2492 #root::__chrono::Utc::now()
2493 ),
2494 )
2495 });
2496 out.upsert_update_columns.push(quote!(#column));
2497 } else {
2498 out.update_assignments.push(quote! {
2499 #root::core::Assignment::new(
2500 #column,
2501 ::core::convert::Into::<#root::core::SqlValue>::into(
2502 ::core::clone::Clone::clone(&self.#ident)
2503 ),
2504 )
2505 });
2506 out.upsert_update_columns.push(quote!(#column));
2507 }
2508 out.column_entries.push(ColumnEntry {
2509 ident: ident.clone(),
2510 value_ty: info.value_ty.clone(),
2511 name: ident_name(ident),
2512 column: info.column.clone(),
2513 field_type_tokens: info.field_type_tokens,
2514 uuid_column: info.uuid_column,
2515 });
2516 }
2517 Ok(out)
2518}
2519
2520fn model_impl_tokens(
2521 struct_name: &syn::Ident,
2522 model_name: &str,
2523 table: &str,
2524 display: Option<&str>,
2525 app_label: Option<&str>,
2526 admin: Option<&AdminAttrs>,
2527 default_order: &[(String, bool, proc_macro2::Span)],
2528 field_schemas: &[TokenStream2],
2529 soft_delete_column: Option<&str>,
2530 permissions: bool,
2531 audit_track: Option<&[String]>,
2532 m2m_relations: &[M2MAttr],
2533 indexes: &[IndexAttr],
2534 checks: &[CheckAttr],
2535 excludes: &[ExcludeAttr],
2536 composite_fks: &[CompositeFkAttr],
2537 generic_fks: &[GenericFkAttr],
2538 scope: Option<&str>,
2539 is_view: bool,
2540 verbose_name: Option<&str>,
2541 verbose_name_plural: Option<&str>,
2542 managed: bool,
2543 base_manager_name: Option<&str>,
2544 order_with_respect_to: Option<&str>,
2545 proxy: bool,
2546 required_db_features: &[String],
2547 required_db_vendor: Option<&str>,
2548 default_related_name: Option<&str>,
2549 db_table_comment: Option<&str>,
2550 get_latest_by: Option<(&str, bool)>,
2551 extra_permissions: &[(String, String)],
2552 default_permissions: &[String],
2553 global_scopes: &[GlobalScopeAttr],
2554 reverse_has_relations: &[ReverseHasAttr],
2555 generic_has_relations: &[GenericHasAttr],
2556) -> TokenStream2 {
2557 let root = rustango_root();
2558 let display_tokens = if let Some(name) = display {
2559 quote!(::core::option::Option::Some(#name))
2560 } else {
2561 quote!(::core::option::Option::None)
2562 };
2563 let app_label_tokens = if let Some(name) = app_label {
2564 quote!(::core::option::Option::Some(#name))
2565 } else {
2566 quote!(::core::option::Option::None)
2567 };
2568 let soft_delete_tokens = if let Some(col) = soft_delete_column {
2569 quote!(::core::option::Option::Some(#col))
2570 } else {
2571 quote!(::core::option::Option::None)
2572 };
2573 let audit_track_tokens = match audit_track {
2574 None => quote!(::core::option::Option::None),
2575 Some(names) => {
2576 let lits = names.iter().map(|n| n.as_str());
2577 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2578 }
2579 };
2580 let admin_tokens = admin_config_tokens(admin);
2581 // Default `tenant` so single-tenant projects (no `scope` attr
2582 // anywhere) keep the v0.24.x behavior. Container-attr parser
2583 // already validated the value is "registry" or "tenant".
2584 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2585 Some("registry") => quote!(#root::core::ModelScope::Registry),
2586 _ => quote!(#root::core::ModelScope::Tenant),
2587 };
2588 let verbose_name_tokens = optional_str(verbose_name);
2589 let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2590 let base_manager_name_tokens = optional_str(base_manager_name);
2591 let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2592 let required_db_features_lits: Vec<&str> =
2593 required_db_features.iter().map(String::as_str).collect();
2594 let required_db_vendor_tokens = optional_str(required_db_vendor);
2595 let default_related_name_tokens = optional_str(default_related_name);
2596 let db_table_comment_tokens = optional_str(db_table_comment);
2597 let get_latest_by_tokens = match get_latest_by {
2598 Some((col, desc)) => {
2599 quote!(::core::option::Option::Some((#col, #desc)))
2600 }
2601 None => quote!(::core::option::Option::None),
2602 };
2603 let extra_permission_tokens: Vec<_> = extra_permissions
2604 .iter()
2605 .map(|(c, l)| quote!((#c, #l)))
2606 .collect();
2607 let default_permission_tokens: Vec<_> = default_permissions
2608 .iter()
2609 .map(|action| quote!(#action))
2610 .collect();
2611 let indexes_tokens = indexes.iter().map(|idx| {
2612 // When no explicit `name = "..."` was given, derive a stable,
2613 // collision-free name from the table + columns
2614 // (`<table>_<col>_<col>_idx`) instead of a shared literal that
2615 // would clash the moment a model declares two unnamed indexes.
2616 // Capped at Postgres's 63-char identifier limit.
2617 let derived_name = idx.name.clone().unwrap_or_else(|| {
2618 let mut n = format!("{table}_{}_idx", idx.columns.join("_"));
2619 if n.len() > 63 {
2620 n.truncate(63);
2621 }
2622 n
2623 });
2624 let name = derived_name.as_str();
2625 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2626 let unique = idx.unique;
2627 // Map the parsed method string onto the IndexMethod enum
2628 // variant — kept at the codegen layer so the IR doesn't
2629 // carry the string form.
2630 let method_variant = match idx.method.as_str() {
2631 "gin" => quote!(#root::core::IndexMethod::Gin),
2632 "gist" => quote!(#root::core::IndexMethod::Gist),
2633 "brin" => quote!(#root::core::IndexMethod::Brin),
2634 "spgist" => quote!(#root::core::IndexMethod::SpGist),
2635 "hash" => quote!(#root::core::IndexMethod::Hash),
2636 "bloom" => quote!(#root::core::IndexMethod::Bloom),
2637 _ => quote!(#root::core::IndexMethod::BTree),
2638 };
2639 let where_clause = match &idx.where_clause {
2640 Some(s) => quote!(::core::option::Option::Some(#s)),
2641 None => quote!(::core::option::Option::None),
2642 };
2643 let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2644 quote! {{
2645 let mut i = #root::core::IndexSchema::new(#name, &[ #(#cols),* ]);
2646 i.unique = #unique;
2647 i.method = #method_variant;
2648 i.where_clause = #where_clause;
2649 i.include = &[ #(#include_lits),* ];
2650 i
2651 }}
2652 });
2653 let checks_tokens = checks.iter().map(|c| {
2654 let name = c.name.as_str();
2655 let expr = c.expr.as_str();
2656 quote!(#root::core::CheckConstraint::new(#name, #expr))
2657 });
2658 let excludes_tokens = excludes.iter().map(|e| {
2659 let name = e.name.as_str();
2660 let using = e.using.as_str();
2661 let element_tokens = e.elements.iter().map(|(col, op)| {
2662 let col_s = col.as_str();
2663 let op_s = op.as_str();
2664 quote!((#col_s, #op_s))
2665 });
2666 let where_tokens = match e.where_clause.as_deref() {
2667 Some(w) => quote!(::core::option::Option::Some(#w)),
2668 None => quote!(::core::option::Option::None),
2669 };
2670 quote! {{
2671 let mut e = #root::core::ExclusionConstraint::new(
2672 #name,
2673 #using,
2674 &[ #(#element_tokens),* ],
2675 );
2676 e.where_clause = #where_tokens;
2677 e
2678 }}
2679 });
2680 let composite_fk_tokens = composite_fks.iter().map(|rel| {
2681 let name = rel.name.as_str();
2682 let to = rel.to.as_str();
2683 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2684 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2685 quote! {
2686 #root::core::CompositeFkRelation::new(
2687 #name,
2688 #to,
2689 &[ #(#from_cols),* ],
2690 &[ #(#on_cols),* ],
2691 )
2692 }
2693 });
2694 let generic_fk_tokens = generic_fks.iter().map(|rel| {
2695 let name = rel.name.as_str();
2696 let ct_col = rel.ct_column.as_str();
2697 let pk_col = rel.pk_column.as_str();
2698 quote!(#root::core::GenericRelation::new(#name, #ct_col, #pk_col))
2699 });
2700 // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2701 // no `#[rustango(default_order = "...")]` attribute was supplied.
2702 let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2703 let col_lit = col.as_str();
2704 quote! { (#col_lit, #desc) }
2705 });
2706
2707 // Issue #820 — `global_scopes` slice literal. Empty when no
2708 // `#[rustango(global_scope(...))]` attribute was supplied. The
2709 // `apply` path is re-emitted verbatim so it resolves in the
2710 // consumer's scope; the name is stored as a string literal.
2711 let global_scope_tokens = global_scopes.iter().map(|s| {
2712 let name = s.name.as_str();
2713 // The slice sits in a nested `const`, where `Self` does not resolve.
2714 let mut apply = s.apply.clone();
2715 if let Some(first) = apply.segments.first_mut() {
2716 if first.ident == "Self" {
2717 first.ident = struct_name.clone();
2718 }
2719 }
2720 quote!(#root::core::GlobalScope::new(#name, #apply))
2721 });
2722
2723 let m2m_tokens = m2m_relations.iter().map(|rel| {
2724 let name = rel.name.as_str();
2725 let to = rel.to.as_str();
2726 let through = rel.through.as_str();
2727 let src = rel.src.as_str();
2728 let dst = rel.dst.as_str();
2729 let auto_create = rel.auto_create;
2730 quote! {{
2731 let mut m = #root::core::M2MRelation::new(#name, #to, #through, #src, #dst);
2732 m.auto_create = #auto_create;
2733 m
2734 }}
2735 });
2736 // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2737 // when the model declares `#[rustango(reverse_has(...))]`. Each
2738 // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2739 // a const expression. Models without reverse_has fall through to
2740 // the trait's empty default — no override emitted.
2741 let reverse_relations_override = if reverse_has_relations.is_empty() {
2742 quote!()
2743 } else {
2744 let entries = reverse_has_relations.iter().map(|rel| {
2745 let name = rel.name.as_str();
2746 let child = &rel.child;
2747 let child_fk_column = rel.child_fk_column.as_str();
2748 let self_pk_column = rel.self_pk_column.as_str();
2749 quote! {
2750 #root::core::ReverseRelation::new(
2751 #name,
2752 <#child as #root::core::Model>::SCHEMA,
2753 #child_fk_column,
2754 #self_pk_column,
2755 )
2756 }
2757 });
2758 quote! {
2759 fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2760 const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2761 RELS
2762 }
2763 }
2764 };
2765 // Issue #830 — `Model::generic_reverse_relations()` override for
2766 // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2767 let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2768 quote!()
2769 } else {
2770 let entries = generic_has_relations.iter().map(|rel| {
2771 let name = rel.name.as_str();
2772 let child = &rel.child;
2773 let ct_column = rel.ct_column.as_str();
2774 let pk_column = rel.pk_column.as_str();
2775 let self_pk_column = rel.self_pk_column.as_str();
2776 quote! {
2777 #root::core::GenericReverseRelation::new(
2778 #name,
2779 <#child as #root::core::Model>::SCHEMA,
2780 #ct_column,
2781 #pk_column,
2782 #self_pk_column,
2783 )
2784 }
2785 });
2786 quote! {
2787 fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2788 const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2789 RELS
2790 }
2791 }
2792 };
2793 let fields_slice = const_slice(
2794 "e!(#root::core::FieldSchema),
2795 field_schemas.iter().cloned(),
2796 );
2797 let m2m_slice = const_slice("e!(#root::core::M2MRelation), m2m_tokens);
2798 let indexes_slice = const_slice("e!(#root::core::IndexSchema), indexes_tokens);
2799 let checks_slice = const_slice("e!(#root::core::CheckConstraint), checks_tokens);
2800 let excludes_slice = const_slice("e!(#root::core::ExclusionConstraint), excludes_tokens);
2801 let composite_fk_slice = const_slice(
2802 "e!(#root::core::CompositeFkRelation),
2803 composite_fk_tokens,
2804 );
2805 let generic_fk_slice = const_slice("e!(#root::core::GenericRelation), generic_fk_tokens);
2806 let audited_update_override = if audit_track.is_some() {
2807 quote! {
2808 fn __rustango_audited_update() -> ::core::option::Option<#root::audit::AuditedUpdate> {
2809 ::core::option::Option::Some(Self::__rustango_update_audited)
2810 }
2811 fn __rustango_audited_delete() -> ::core::option::Option<#root::audit::AuditedDelete> {
2812 ::core::option::Option::Some(Self::__rustango_delete_audited)
2813 }
2814 fn __rustango_audited_create() -> ::core::option::Option<#root::audit::AuditedCreate> {
2815 ::core::option::Option::Some(Self::__rustango_create_audited)
2816 }
2817 fn __rustango_audited_update_record(
2818 ) -> ::core::option::Option<#root::audit::AuditedUpdateRecord> {
2819 ::core::option::Option::Some(Self::__rustango_update_record_audited)
2820 }
2821 }
2822 } else {
2823 quote!()
2824 };
2825 let global_scope_slice = const_slice("e!(#root::core::GlobalScope), global_scope_tokens);
2826 quote! {
2827 impl #root::core::Model for #struct_name {
2828 const SCHEMA: &'static #root::core::ModelSchema = &{
2829 #root::core::ModelSchema::from_parts(#root::core::ModelSchemaParts {
2830 name: #model_name,
2831 table: #table,
2832 fields: #fields_slice,
2833 display: #display_tokens,
2834 app_label: #app_label_tokens,
2835 admin: #admin_tokens,
2836 soft_delete_column: #soft_delete_tokens,
2837 permissions: #permissions,
2838 audit_track: #audit_track_tokens,
2839 m2m: #m2m_slice,
2840 indexes: #indexes_slice,
2841 check_constraints: #checks_slice,
2842 exclusion_constraints: #excludes_slice,
2843 composite_relations: #composite_fk_slice,
2844 generic_relations: #generic_fk_slice,
2845 scope: #scope_tokens,
2846 default_order: &[ #(#default_order_tokens),* ],
2847 is_view: #is_view,
2848 verbose_name: #verbose_name_tokens,
2849 verbose_name_plural: #verbose_name_plural_tokens,
2850 managed: #managed,
2851 base_manager_name: #base_manager_name_tokens,
2852 order_with_respect_to: #order_with_respect_to_tokens,
2853 proxy: #proxy,
2854 required_db_features: &[ #(#required_db_features_lits),* ],
2855 required_db_vendor: #required_db_vendor_tokens,
2856 default_related_name: #default_related_name_tokens,
2857 db_table_comment: #db_table_comment_tokens,
2858 get_latest_by: #get_latest_by_tokens,
2859 extra_permissions: &[ #(#extra_permission_tokens),* ],
2860 default_permissions: &[ #(#default_permission_tokens),* ],
2861 global_scopes: #global_scope_slice,
2862 })
2863 };
2864
2865 #reverse_relations_override
2866 #generic_reverse_relations_override
2867 #audited_update_override
2868 }
2869 }
2870}
2871
2872/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2873/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2874/// otherwise a static reference to a populated `AdminConfig`.
2875fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2876 let root = rustango_root();
2877 let Some(admin) = admin else {
2878 return quote!(::core::option::Option::None);
2879 };
2880
2881 let list_display = admin
2882 .list_display
2883 .as_ref()
2884 .map(|(v, _)| v.as_slice())
2885 .unwrap_or(&[]);
2886 let list_display_lits = list_display.iter().map(|s| s.as_str());
2887
2888 let search_fields = admin
2889 .search_fields
2890 .as_ref()
2891 .map(|(v, _)| v.as_slice())
2892 .unwrap_or(&[]);
2893 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2894
2895 let readonly_fields = admin
2896 .readonly_fields
2897 .as_ref()
2898 .map(|(v, _)| v.as_slice())
2899 .unwrap_or(&[]);
2900 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2901
2902 let list_filter = admin
2903 .list_filter
2904 .as_ref()
2905 .map(|(v, _)| v.as_slice())
2906 .unwrap_or(&[]);
2907 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2908
2909 let actions = admin
2910 .actions
2911 .as_ref()
2912 .map(|(v, _)| v.as_slice())
2913 .unwrap_or(&[]);
2914 let actions_lits = actions.iter().map(|s| s.as_str());
2915
2916 let fieldsets = admin
2917 .fieldsets
2918 .as_ref()
2919 .map(|(v, _)| v.as_slice())
2920 .unwrap_or(&[]);
2921 let fieldset_tokens = const_slice(
2922 "e!(#root::core::Fieldset),
2923 fieldsets.iter().map(|(title, fields)| {
2924 let title = title.as_str();
2925 let field_lits = fields.iter().map(String::as_str);
2926 quote!(#root::core::Fieldset::new(#title, &[ #( #field_lits ),* ]))
2927 }),
2928 );
2929
2930 let list_display_links = admin
2931 .list_display_links
2932 .as_ref()
2933 .map(|(v, _)| v.as_slice())
2934 .unwrap_or(&[]);
2935 let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2936
2937 let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2938 let actions_on_top = admin.actions_on_top.unwrap_or(true);
2939 let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2940 let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2941
2942 let prepopulated = admin
2943 .prepopulated_fields
2944 .as_ref()
2945 .map(|(v, _)| v.as_slice())
2946 .unwrap_or(&[]);
2947 let prepopulated_tokens = const_slice(
2948 "e!(#root::core::PrepopulatedField),
2949 prepopulated.iter().map(|(target, sources)| {
2950 let target = target.as_str();
2951 let source_lits = sources.iter().map(String::as_str);
2952 quote!(#root::core::PrepopulatedField::new(#target, &[ #( #source_lits ),* ]))
2953 }),
2954 );
2955
2956 let raw_id_fields = admin
2957 .raw_id_fields
2958 .as_ref()
2959 .map(|(v, _)| v.as_slice())
2960 .unwrap_or(&[]);
2961 let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2962
2963 let autocomplete_fields = admin
2964 .autocomplete_fields
2965 .as_ref()
2966 .map(|(v, _)| v.as_slice())
2967 .unwrap_or(&[]);
2968 let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
2969
2970 // #352 — list_select_related accepts "all" | "none" | "field, field, …".
2971 let list_select_related_tokens = match admin.list_select_related.as_deref() {
2972 None | Some("all") => quote!(#root::core::ListSelectRelated::All),
2973 Some("none") => quote!(#root::core::ListSelectRelated::None),
2974 Some(raw) => {
2975 let names: Vec<&str> = raw
2976 .split(',')
2977 .map(str::trim)
2978 .filter(|s| !s.is_empty())
2979 .collect();
2980 quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
2981 }
2982 };
2983
2984 // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
2985 // a Vec<(String, String)>. Empty / unset → no overrides.
2986 let formfield_pairs: Vec<(&str, &str)> = admin
2987 .formfield_overrides
2988 .as_ref()
2989 .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
2990 .unwrap_or_default();
2991 let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
2992 let field = *field;
2993 let widget = *widget;
2994 quote!((#field, #widget))
2995 });
2996
2997 let list_per_page = admin.list_per_page.unwrap_or(0);
2998
2999 let ordering_pairs = admin
3000 .ordering
3001 .as_ref()
3002 .map(|(v, _)| v.as_slice())
3003 .unwrap_or(&[]);
3004 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
3005 let name = name.as_str();
3006 let desc = *desc;
3007 quote!((#name, #desc))
3008 });
3009
3010 quote! {
3011 ::core::option::Option::Some({
3012 const ADMIN: &#root::core::AdminConfig = &{
3013 #root::core::AdminConfig::from_parts(#root::core::AdminConfigParts {
3014 list_display: &[ #( #list_display_lits ),* ],
3015 search_fields: &[ #( #search_fields_lits ),* ],
3016 list_per_page: #list_per_page,
3017 ordering: &[ #( #ordering_tokens ),* ],
3018 readonly_fields: &[ #( #readonly_fields_lits ),* ],
3019 list_filter: &[ #( #list_filter_lits ),* ],
3020 actions: &[ #( #actions_lits ),* ],
3021 fieldsets: #fieldset_tokens,
3022 list_display_links: &[ #( #list_display_links_lits ),* ],
3023 search_help_text: #search_help_text,
3024 actions_on_top: #actions_on_top,
3025 actions_on_bottom: #actions_on_bottom,
3026 date_hierarchy: #date_hierarchy,
3027 prepopulated_fields: #prepopulated_tokens,
3028 raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
3029 autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
3030 list_select_related: #list_select_related_tokens,
3031 formfield_overrides: &[ #( #formfield_tokens ),* ],
3032 })
3033 };
3034 ADMIN
3035 })
3036 }
3037}
3038
3039fn inherent_impl_tokens(
3040 struct_name: &syn::Ident,
3041 fields: &CollectedFields,
3042 primary_key: Option<&(syn::Ident, String)>,
3043 column_consts: &TokenStream2,
3044 audited_fields: Option<&[&ColumnEntry]>,
3045 upsert_unique: Option<&[String]>,
3046 manager_fns: &[syn::Ident],
3047) -> TokenStream2 {
3048 let root = rustango_root();
3049 // Audit-emit fragments threaded into write paths. Non-empty only
3050 // when the model carries `#[rustango(audit(...))]`. They reborrow
3051 // `_executor` (a `&mut PgConnection` for audited models — the
3052 // macro switches the signature below) so the data write and the
3053 // audit INSERT both run on the same caller-supplied connection.
3054 let executor_passes_to_data_write = if audited_fields.is_some() {
3055 quote!(&mut *_executor)
3056 } else {
3057 quote!(_executor)
3058 };
3059 let executor_param = if audited_fields.is_some() {
3060 quote!(_executor: &mut #root::sql::sqlx::PgConnection)
3061 } else {
3062 quote!(_executor: _E)
3063 };
3064 let executor_generics = if audited_fields.is_some() {
3065 quote!()
3066 } else {
3067 quote!(<'_c, _E>)
3068 };
3069 let executor_where = if audited_fields.is_some() {
3070 quote!()
3071 } else {
3072 quote! {
3073 where
3074 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
3075 }
3076 };
3077 // For audited models the `_on` methods take `&mut PgConnection`, so
3078 // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
3079 // must acquire a connection first. Non-audited models keep the
3080 // direct delegation since `&PgPool` IS an Executor.
3081 let pool_to_save_on = if audited_fields.is_some() {
3082 quote! {
3083 let mut _conn = pool.acquire().await?;
3084 self.save_on(&mut *_conn).await
3085 }
3086 } else {
3087 quote!(self.save_on(pool).await)
3088 };
3089 let pool_to_insert_on = if audited_fields.is_some() {
3090 quote! {
3091 let mut _conn = pool.acquire().await?;
3092 self.insert_on(&mut *_conn).await
3093 }
3094 } else {
3095 quote!(self.insert_on(pool).await)
3096 };
3097 let pool_to_delete_on = if audited_fields.is_some() {
3098 quote! {
3099 let mut _conn = pool.acquire().await?;
3100 self.delete_on(&mut *_conn).await
3101 }
3102 } else {
3103 quote!(self.delete_on(pool).await)
3104 };
3105 let pool_to_bulk_insert_on = if audited_fields.is_some() {
3106 quote! {
3107 let mut _tx = pool.begin().await?;
3108 Self::bulk_insert_on(rows, &mut *_tx).await?;
3109 _tx.commit().await?;
3110 ::core::result::Result::Ok(())
3111 }
3112 } else {
3113 quote!(Self::bulk_insert_on(rows, pool).await)
3114 };
3115 // Pre-existing bug surfaced by batch 22's first audited Auto<T>
3116 // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
3117 // directly, but `upsert_on` for audited models takes
3118 // `&mut PgConnection` (the audit emit needs a real connection).
3119 // Add the missing acquire shim to keep audited Auto-PK upsert
3120 // compiling.
3121 let pool_to_upsert_on = if audited_fields.is_some() {
3122 quote! {
3123 let mut _tx = pool.begin().await?;
3124 self.upsert_on(&mut *_tx).await?;
3125 _tx.commit().await?;
3126 ::core::result::Result::Ok(())
3127 }
3128 } else {
3129 quote!(self.upsert_on(pool).await)
3130 };
3131
3132 // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
3133 // (audit-on-connection over &Pool needs a bi-dialect transaction
3134 // helper, deferred). Two body shapes:
3135 // - has_auto: build InsertQuery skipping Auto::Unset columns,
3136 // request Auto cols in `returning`, dispatch via
3137 // `insert_returning_pool`, then on the returned `PgRow` /
3138 // `MySqlAutoId(id)` enum — pull each Auto field from the PG
3139 // row OR drop the single i64 into the first Auto field on MySQL
3140 // (multi-Auto models on MySQL error at runtime since
3141 // `LAST_INSERT_ID()` only reports one)
3142 // - non-Auto: build InsertQuery with explicit columns/values and
3143 // call `insert_pool` (no returning needed)
3144 // pool_insert_method body for the audited Auto-PK case is moved
3145 // to after audit_pair_tokens / audit_pk_to_string (they live
3146 // ~150 lines below). This block keeps the non-audited and
3147 // non-Auto branches in place — the audited Auto-PK arm is
3148 // computed below and merged via the dispatch helper variable.
3149 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
3150 // Audited models with explicit (non-Auto) PKs go through
3151 // the non-Auto insert path below — the audit emit is one
3152 // round-trip after the INSERT inside the same tx via
3153 // audit::save_one_with_audit? No, INSERT semantics
3154 // differ. For non-Auto PK + audited, route through a
3155 // dedicated insert + audit emit on the same tx, but defer
3156 // the macro emission to the audit-bundle-aware block below
3157 // — this `quote!()` placeholder gets overwritten there.
3158 quote!()
3159 } else if audited_fields.is_some() && fields.has_auto {
3160 // Audited Auto-PK insert_pool — assembled after the audit
3161 // bundles. Placeholder; real emission below.
3162 quote!()
3163 } else if fields.has_auto {
3164 let pushes = &fields.insert_pushes;
3165 let returning_cols = &fields.returning_cols;
3166 // A row not inserted was never saved, so a Rust-filled value must not stay (#1937).
3167 let filled = &fields.rust_filled_idents;
3168 let was_unset: Vec<syn::Ident> = filled
3169 .iter()
3170 .map(|i| quote::format_ident!("__{}_was_unset", ident_name(i)))
3171 .collect();
3172 let insert_or_ignore_call = quote! {
3173 #( let #was_unset = matches!(&self.#filled, #root::sql::Auto::Unset); )*
3174 let mut _columns: ::std::vec::Vec<&'static str> =
3175 ::std::vec::Vec::new();
3176 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3177 ::std::vec::Vec::new();
3178 #( #pushes )*
3179 let _query = #root::core::InsertQuery::new(
3180 <Self as #root::core::Model>::SCHEMA,
3181 _columns,
3182 _values,
3183 );
3184 let _result = #root::sql::insert_or_ignore(pool, &_query).await;
3185 if !matches!(_result, ::core::result::Result::Ok(true)) {
3186 #( if #was_unset { self.#filled = #root::sql::Auto::Unset; } )*
3187 }
3188 _result
3189 };
3190 // When every `Auto<T>` field is filled Rust-side
3191 // (`default_uuid_v7`, issue #823), there is no column to read
3192 // back from the database — `returning_cols` is empty. Route
3193 // through plain `insert_pool` instead of
3194 // `insert_returning_pool` to skip the redundant RETURNING /
3195 // LAST_INSERT_ID round-trip.
3196 if fields.returning_cols.is_empty() {
3197 quote! {
3198 /// Insert this row against either backend. Every
3199 /// `Auto<T>` PK on this model is filled Rust-side
3200 /// (e.g. `default_uuid_v7`) before binding, so no
3201 /// RETURNING round-trip is needed.
3202 ///
3203 /// # Errors
3204 /// As [`Self::insert`].
3205 pub async fn insert_pool(
3206 &mut self,
3207 pool: &#root::sql::Pool,
3208 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3209 let mut _columns: ::std::vec::Vec<&'static str> =
3210 ::std::vec::Vec::new();
3211 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3212 ::std::vec::Vec::new();
3213 #( #pushes )*
3214 let _query = #root::core::InsertQuery::new(
3215 <Self as #root::core::Model>::SCHEMA,
3216 _columns,
3217 _values,
3218 );
3219 #root::sql::insert_pool(pool, &_query).await
3220 }
3221
3222 /// Eloquent `Model::insertOrIgnore()` — same shape
3223 /// as the auto-PK branch above. Returns `Ok(true)`
3224 /// when inserted, `Ok(false)` when a conflict caused
3225 /// the INSERT to silently skip.
3226 ///
3227 /// # Errors
3228 /// As [`Self::insert`].
3229 pub async fn insert_or_ignore(
3230 &mut self,
3231 pool: &#root::sql::Pool,
3232 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3233 #insert_or_ignore_call
3234 }
3235 }
3236 } else {
3237 quote! {
3238 /// Insert this row against either backend, populating any
3239 /// `Auto<T>` PK from the auto-assigned value.
3240 ///
3241 /// # Errors
3242 /// As [`Self::insert`].
3243 pub async fn insert_pool(
3244 &mut self,
3245 pool: &#root::sql::Pool,
3246 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3247 let mut _columns: ::std::vec::Vec<&'static str> =
3248 ::std::vec::Vec::new();
3249 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3250 ::std::vec::Vec::new();
3251 #( #pushes )*
3252 let _query = #root::core::InsertQuery::new(
3253 <Self as #root::core::Model>::SCHEMA,
3254 _columns,
3255 _values,
3256 )
3257 .returning(::std::vec![ #( #returning_cols ),* ]);
3258 let _result = #root::sql::insert_returning_pool(
3259 pool, &_query,
3260 ).await?;
3261 #root::sql::apply_auto_pk(_result, self)
3262 }
3263
3264 /// Eloquent `Model::insertOrIgnore()` — INSERT this
3265 /// row or silently skip on unique-constraint
3266 /// violation. Returns `Ok(true)` when a row was
3267 /// inserted, `Ok(false)` when a conflict caused the
3268 /// INSERT to silently skip.
3269 ///
3270 /// **Caveat on auto-PK models**: when the row is
3271 /// skipped (conflict), this instance's `Auto<T>`
3272 /// fields stay `Unset` — no PK is back-populated
3273 /// because the server didn't auto-assign one. For
3274 /// "insert then read back the PK or the existing
3275 /// row's PK", use the `upsert` family or
3276 /// `get_or_create`.
3277 ///
3278 /// # Errors
3279 /// As [`Self::insert`].
3280 pub async fn insert_or_ignore(
3281 &mut self,
3282 pool: &#root::sql::Pool,
3283 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3284 #insert_or_ignore_call
3285 }
3286 }
3287 }
3288 } else {
3289 let insert_columns = &fields.insert_columns;
3290 let insert_values = &fields.insert_values;
3291 quote! {
3292 /// Insert this row into its table against either backend.
3293 /// Equivalent to [`Self::insert`] but takes
3294 /// [`#root::sql::Pool`].
3295 ///
3296 /// # Errors
3297 /// As [`Self::insert`].
3298 pub async fn insert_pool(
3299 &self,
3300 pool: &#root::sql::Pool,
3301 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3302 let _query = #root::core::InsertQuery::new(
3303 <Self as #root::core::Model>::SCHEMA,
3304 ::std::vec![ #( #insert_columns ),* ],
3305 ::std::vec![ #( #insert_values ),* ],
3306 );
3307 #root::sql::insert_pool(pool, &_query).await
3308 }
3309
3310 /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3311 /// or silently skip on unique-constraint violation. Maps
3312 /// to per-dialect "INSERT ... DO NOTHING on conflict":
3313 /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3314 /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL an
3315 /// `ON DUPLICATE KEY UPDATE` that leaves the row as is.
3316 ///
3317 /// Returns `Ok(true)` when a row was inserted,
3318 /// `Ok(false)` when a conflict caused the INSERT to
3319 /// silently skip.
3320 ///
3321 /// Use for "create-if-absent" patterns where you don't
3322 /// need the row back. For "find-or-create with the row
3323 /// returned", use the queryset-level
3324 /// `crate::sql::get_or_create` free function.
3325 ///
3326 /// # Errors
3327 /// As [`Self::insert`], plus any dialect-specific
3328 /// translation error from the ConflictClause writer.
3329 pub async fn insert_or_ignore(
3330 &self,
3331 pool: &#root::sql::Pool,
3332 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3333 let _query = #root::core::InsertQuery::new(
3334 <Self as #root::core::Model>::SCHEMA,
3335 ::std::vec![ #( #insert_columns ),* ],
3336 ::std::vec![ #( #insert_values ),* ],
3337 );
3338 #root::sql::insert_or_ignore(pool, &_query).await
3339 }
3340 }
3341 };
3342
3343 // The tracked (column, JSON value) pairs, spliced only into
3344 // `__rustango_audit_pairs`. Empty when the model isn't audited.
3345 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3346 .map(|tracked| {
3347 tracked
3348 .iter()
3349 .map(|c| {
3350 let column_lit = c.column.as_str();
3351 let ident = &c.ident;
3352 quote! {
3353 (
3354 #column_lit,
3355 #root::__serde_json::to_value(&self.#ident)
3356 .unwrap_or(#root::__serde_json::Value::Null),
3357 )
3358 }
3359 })
3360 .collect()
3361 })
3362 .unwrap_or_default();
3363 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3364 if fields.pk_is_auto {
3365 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3366 } else {
3367 quote!(::std::format!("{}", &self.#pk_ident))
3368 }
3369 } else {
3370 quote!(::std::string::String::new())
3371 };
3372 // `__rustango_audit_entry`: the one place a snapshot `PendingEntry`
3373 // is built, so every audited write path records the same shape.
3374 let delete_audited_body = if primary_key.is_some() {
3375 quote! {
3376 ::std::boxed::Box::pin(#root::audit::delete_many_with_audit::<Self>(
3377 pool,
3378 query,
3379 |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3380 ))
3381 }
3382 } else {
3383 quote! {
3384 let _ = (pool, query);
3385 ::std::boxed::Box::pin(async {
3386 ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3387 table: <Self as #root::core::Model>::SCHEMA.table,
3388 })
3389 })
3390 }
3391 };
3392 let update_audited_body = if primary_key.is_some() {
3393 quote! {
3394 ::std::boxed::Box::pin(#root::audit::update_many_with_audit::<Self>(
3395 pool,
3396 query,
3397 move |_r: &Self| _r.__rustango_audit_entry(op),
3398 ))
3399 }
3400 } else {
3401 quote! {
3402 let _ = (pool, query, op);
3403 ::std::boxed::Box::pin(async {
3404 ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3405 table: <Self as #root::core::Model>::SCHEMA.table,
3406 })
3407 })
3408 }
3409 };
3410 let audit_entry_method = if audited_fields.is_some() {
3411 let pairs = audit_pair_tokens.iter();
3412 let pk_str = audit_pk_to_string.clone();
3413 quote! {
3414 /// This row's tracked `(column, value)` pairs.
3415 #[doc(hidden)]
3416 pub fn __rustango_audit_pairs(
3417 &self,
3418 ) -> ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> {
3419 ::std::vec![ #( #pairs ),* ]
3420 }
3421
3422 /// This row's audit PK.
3423 #[doc(hidden)]
3424 pub fn __rustango_audit_pk(&self) -> ::std::string::String {
3425 #pk_str
3426 }
3427
3428 /// Snapshot audit entry for `operation` on this row.
3429 #[doc(hidden)]
3430 pub fn __rustango_audit_entry(
3431 &self,
3432 operation: #root::audit::AuditOp,
3433 ) -> #root::audit::PendingEntry {
3434 #root::audit::PendingEntry {
3435 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3436 entity_pk: self.__rustango_audit_pk(),
3437 operation,
3438 source: #root::audit::current_source(),
3439 changes: #root::audit::snapshot_changes(&self.__rustango_audit_pairs()),
3440 }
3441 }
3442
3443 /// Audited bulk `UPDATE`, behind `Model::__rustango_audited_update`.
3444 #[doc(hidden)]
3445 pub fn __rustango_update_audited<'a>(
3446 pool: &'a #root::sql::Pool,
3447 query: &'a #root::core::UpdateQuery,
3448 op: #root::audit::AuditOp,
3449 ) -> ::std::pin::Pin<::std::boxed::Box<
3450 dyn ::core::future::Future<
3451 Output = ::core::result::Result<u64, #root::sql::ExecError>,
3452 > + ::core::marker::Send + 'a,
3453 >> {
3454 #update_audited_body
3455 }
3456
3457 /// Audited `create` entry for a new row, behind `Model::__rustango_audited_create`.
3458 #[doc(hidden)]
3459 pub fn __rustango_create_audited<'a, 't>(
3460 tx: &'a mut #root::sql::PoolTx<'t>,
3461 pk: #root::core::SqlValue,
3462 ) -> ::std::pin::Pin<::std::boxed::Box<
3463 dyn ::core::future::Future<
3464 Output = ::core::result::Result<(), #root::sql::ExecError>,
3465 > + ::core::marker::Send + 'a,
3466 >> {
3467 ::std::boxed::Box::pin(#root::audit::record_create::<Self>(
3468 tx,
3469 <Self as #root::core::Model>::SCHEMA,
3470 pk,
3471 |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Create),
3472 ))
3473 }
3474
3475 /// Audited one-row `update` entry, behind `Model::__rustango_audited_update_record`.
3476 #[doc(hidden)]
3477 pub fn __rustango_update_record_audited<'a, 't>(
3478 tx: &'a mut #root::sql::PoolTx<'t>,
3479 pk: #root::core::SqlValue,
3480 ) -> ::std::pin::Pin<::std::boxed::Box<
3481 dyn ::core::future::Future<
3482 Output = ::core::result::Result<(), #root::sql::ExecError>,
3483 > + ::core::marker::Send + 'a,
3484 >> {
3485 ::std::boxed::Box::pin(#root::audit::record_update::<Self>(
3486 tx,
3487 <Self as #root::core::Model>::SCHEMA,
3488 pk,
3489 |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Update),
3490 ))
3491 }
3492
3493 /// Audited bulk `DELETE`, behind `Model::__rustango_audited_delete`.
3494 #[doc(hidden)]
3495 pub fn __rustango_delete_audited<'a>(
3496 pool: &'a #root::sql::Pool,
3497 query: &'a #root::core::DeleteQuery,
3498 ) -> ::std::pin::Pin<::std::boxed::Box<
3499 dyn ::core::future::Future<
3500 Output = ::core::result::Result<u64, #root::sql::ExecError>,
3501 > + ::core::marker::Send + 'a,
3502 >> {
3503 #delete_audited_body
3504 }
3505 }
3506 } else {
3507 quote!()
3508 };
3509 // Builds `_audit_entry`; `written` is the soft-delete column's new
3510 // value, recorded in place of the stale `self` field.
3511 let make_entry = |op_path: TokenStream2, written: Option<TokenStream2>| -> TokenStream2 {
3512 if let (Some(v), Some(col)) = (written, fields.soft_delete_column.as_deref()) {
3513 quote! {
3514 let mut _audit_entry = self.__rustango_audit_entry(#op_path);
3515 _audit_entry.set_tracked(#col, #v);
3516 }
3517 } else {
3518 quote!(let _audit_entry = self.__rustango_audit_entry(#op_path);)
3519 }
3520 };
3521 let soft_deleted_json = quote!(#root::__serde_json::to_value(&_now)
3522 .unwrap_or(#root::__serde_json::Value::Null));
3523 let restored_json = quote!(#root::__serde_json::Value::Null);
3524 // `_on` emit; `affected` paths skip the row when nothing changed.
3525 let make_op_emit =
3526 |op_path: TokenStream2, written: Option<TokenStream2>, affected: bool| -> TokenStream2 {
3527 if audited_fields.is_none() {
3528 return quote!();
3529 }
3530 let entry = make_entry(op_path, written);
3531 let emit = quote! {
3532 #entry
3533 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3534 };
3535 if affected {
3536 quote!(if _affected > 0 { #emit })
3537 } else {
3538 emit
3539 }
3540 };
3541 let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create), None, false);
3542 let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete), None, true);
3543 let audit_softdelete_emit = make_op_emit(
3544 quote!(#root::audit::AuditOp::SoftDelete),
3545 Some(soft_deleted_json.clone()),
3546 true,
3547 );
3548 let audit_restore_emit = make_op_emit(
3549 quote!(#root::audit::AuditOp::Restore),
3550 Some(restored_json.clone()),
3551 true,
3552 );
3553 // `&Pool` update runner for soft_delete / restore: one tx with the
3554 // audit row when audited, a plain UPDATE otherwise.
3555 let make_pool_update = |op_path: TokenStream2, written: TokenStream2| -> TokenStream2 {
3556 if audited_fields.is_some() {
3557 let entry = make_entry(op_path, Some(written));
3558 quote! {
3559 #entry
3560 #root::audit::save_one_with_audit(pool, &_query, &_audit_entry).await
3561 }
3562 } else {
3563 quote!(#root::sql::update_pool(pool, &_query).await)
3564 }
3565 };
3566 let pool_softdelete_run =
3567 make_pool_update(quote!(#root::audit::AuditOp::SoftDelete), soft_deleted_json);
3568 let pool_restore_run = make_pool_update(quote!(#root::audit::AuditOp::Restore), restored_json);
3569
3570 // `save_pool(&Pool)` — emitted for every model with a PK. The
3571 // audited `save_pool` is built below on the diff path; both share
3572 // `save_partial_methods`, which differ only in how they run `_query`.
3573 let save_partial_methods = |pk_ident: &syn::Ident,
3574 pk_column_lit: &str,
3575 run: TokenStream2|
3576 -> TokenStream2 {
3577 let assignments = &fields.update_assignments;
3578 quote! {
3579 /// Save (UPDATE) only the listed Rust-side fields,
3580 /// leaving every other column untouched. Issue #66.
3581 ///
3582 /// `fields` are Rust-side struct field names; the macro
3583 /// resolves each to its SQL column. Unknown field
3584 /// names return [`#root::core::QueryError::UnknownField`]
3585 /// wrapped in `ExecError::Query`. An empty list is a
3586 /// no-op (returns `Ok(())` and logs a `tracing::warn!`).
3587 /// On an audited model the audit diff covers only these fields.
3588 ///
3589 /// Use this when:
3590 /// * you only mutated a couple of fields on a wide row
3591 /// (avoid re-writing every column on every save), or
3592 /// * two writers diverged after their initial read and
3593 /// you want to preserve the other writer's changes to
3594 /// columns you didn't touch.
3595 ///
3596 /// Auto-PK models with an unset PK return
3597 /// [`#root::core::QueryError::UnknownField`] with
3598 /// field name `<pk>` — `save_partial` is an
3599 /// UPDATE-only path. Call [`Self::insert_pool`]
3600 /// (or [`Self::save_pool`] which dispatches based on
3601 /// PK state) for the INSERT case.
3602 ///
3603 /// # Errors
3604 /// As [`Self::save_pool`], plus `UnknownField` for
3605 /// unknown / empty / Auto-Unset cases.
3606 pub async fn save_partial(
3607 &mut self,
3608 fields: &[&str],
3609 pool: &#root::sql::Pool,
3610 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3611 if fields.is_empty() {
3612 #root::__tracing::warn!(
3613 target: "rustango::save_partial",
3614 model = <Self as #root::core::Model>::SCHEMA.name,
3615 "save_partial called with empty field list — no-op"
3616 );
3617 return ::core::result::Result::Ok(0);
3618 }
3619 let _schema = <Self as #root::core::Model>::SCHEMA;
3620 // Validate field names against the schema.
3621 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3622 ::std::collections::HashSet::with_capacity(fields.len());
3623 for f in fields {
3624 match _schema.field(f) {
3625 ::core::option::Option::Some(fs) => {
3626 _wanted_cols.insert(fs.column);
3627 }
3628 ::core::option::Option::None => {
3629 return ::core::result::Result::Err(
3630 #root::sql::ExecError::Query(
3631 #root::core::QueryError::UnknownField {
3632 model: _schema.name,
3633 field: (*f).to_owned(),
3634 }
3635 )
3636 );
3637 }
3638 }
3639 }
3640 // Build the full assignment vec, then keep only the
3641 // assignments whose column is in `_wanted_cols`.
3642 let _full: ::std::vec::Vec<#root::core::Assignment> =
3643 ::std::vec![ #( #assignments ),* ];
3644 let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3645 .into_iter()
3646 .filter(|a| _wanted_cols.contains(a.column))
3647 .collect();
3648 if _filtered.is_empty() {
3649 // All field names valid, but they all map to
3650 // non-assignable slots (PK column, computed/
3651 // virtual fields, relations without an
3652 // assignment). Same no-op as an empty list.
3653 #root::__tracing::warn!(
3654 target: "rustango::save_partial",
3655 model = _schema.name,
3656 "save_partial: every named field maps to a non-assignable column — no-op"
3657 );
3658 return ::core::result::Result::Ok(0);
3659 }
3660 let _query = #root::core::UpdateQuery::new(
3661 _schema,
3662 _filtered,
3663 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3664 #pk_column_lit,
3665 #root::core::Op::Eq,
3666 ::core::convert::Into::<#root::core::SqlValue>::into(
3667 ::core::clone::Clone::clone(&self.#pk_ident)
3668 ),
3669 )),
3670 );
3671 #run
3672 }
3673
3674 /// Typed-column counterpart of [`Self::save_partial`] —
3675 /// issue #67. `fields` is a tuple of [`Column`]
3676 /// constants whose `Model` matches `Self`; typos and
3677 /// model mismatches surface at *compile time*
3678 /// (`Author::name` inside a `Post::save_partial_typed`
3679 /// call is a type error, no runtime check).
3680 ///
3681 /// ```ignore
3682 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3683 /// ```
3684 ///
3685 /// Lowers to [`Self::save_partial`] under the hood — the
3686 /// tuple is reduced to a `&[&str]` slice of Rust-side
3687 /// field names and forwarded.
3688 ///
3689 /// [`Column`]: #root::core::Column
3690 ///
3691 /// # Errors
3692 /// As [`Self::save_partial`].
3693 pub async fn save_partial_typed<
3694 L: #root::core::TypedFieldList<Self>,
3695 >(
3696 &mut self,
3697 fields: L,
3698 pool: &#root::sql::Pool,
3699 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3700 let _names = fields.rust_field_names();
3701 let _refs: ::std::vec::Vec<&str> =
3702 _names.iter().copied().collect();
3703 self.save_partial(&_refs, pool).await
3704 }
3705 }
3706 };
3707 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3708 let pk_column_lit = pk_col.as_str();
3709 let assignments = &fields.update_assignments;
3710 if audited_fields.is_some() {
3711 quote!()
3712 } else {
3713 let partial = save_partial_methods(
3714 pk_ident,
3715 pk_column_lit,
3716 quote!(#root::sql::update_pool(pool, &_query).await),
3717 );
3718 let dispatch_unset = if fields.pk_is_auto {
3719 quote! {
3720 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3721 return self.insert_pool(pool).await.map(|()| 1u64);
3722 }
3723 }
3724 } else {
3725 quote!()
3726 };
3727 quote! {
3728 /// Save this row to its table against either backend.
3729 /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3730 /// `UPDATE` keyed on the PK; global scopes do not apply.
3731 ///
3732 /// # Errors
3733 /// As [`Self::save`].
3734 pub async fn save_pool(
3735 &mut self,
3736 pool: &#root::sql::Pool,
3737 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3738 #dispatch_unset
3739 let _query = #root::core::UpdateQuery::new(
3740 <Self as #root::core::Model>::SCHEMA,
3741 ::std::vec![ #( #assignments ),* ],
3742 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3743 #pk_column_lit,
3744 #root::core::Op::Eq,
3745 ::core::convert::Into::<#root::core::SqlValue>::into(
3746 ::core::clone::Clone::clone(&self.#pk_ident)
3747 ),
3748 )),
3749 );
3750 let _affected = #root::sql::update_pool(pool, &_query).await?;
3751 ::core::result::Result::Ok(_affected)
3752 }
3753
3754 #partial
3755 }
3756 }
3757 } else {
3758 quote!()
3759 };
3760
3761 // Audited `insert_pool` (overrides the placeholder set higher up
3762 // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3763 // audited models get insert_pool routing through
3764 // audit::insert_one_with_audit (per-backend tx wraps INSERT
3765 // + auto-PK readback + audit emit). Snapshot-style audit (the
3766 // PendingEntry's `changes` carries post-write field values).
3767 let pool_insert_method = if audited_fields.is_some() {
3768 if let Some(_) = primary_key {
3769 let pushes = if fields.has_auto {
3770 fields.insert_pushes.clone()
3771 } else {
3772 // For non-Auto-PK models, the macro normally builds
3773 // {columns, values} from fields.insert_columns +
3774 // fields.insert_values rather than insert_pushes.
3775 // Map those into the pushes shape.
3776 fields
3777 .insert_columns
3778 .iter()
3779 .zip(&fields.insert_values)
3780 .map(|(col, val)| {
3781 quote! {
3782 _columns.push(#col);
3783 _values.push(#val);
3784 }
3785 })
3786 .collect()
3787 };
3788 // Rust-filled Auto PKs (`default_uuid_v7`) leave the list empty (#1934).
3789 let returning_cols: Vec<proc_macro2::TokenStream> = if !fields.returning_cols.is_empty()
3790 {
3791 fields.returning_cols.clone()
3792 } else {
3793 // Non-Auto-PK: still need RETURNING something for the
3794 // audit helper's contract (it errors on empty
3795 // returning). Return the PK column so the audit row
3796 // can carry the assigned PK back. Some non-Auto PKs
3797 // are server-side-default (e.g. UUIDv4 default), so
3798 // RETURNING is genuinely useful.
3799 primary_key
3800 .map(|(_, col)| {
3801 let lit = col.as_str();
3802 vec![quote!(#lit)]
3803 })
3804 .unwrap_or_default()
3805 };
3806 quote! {
3807 /// Insert this row against either backend with audit
3808 /// emission inside the same transaction. Bi-dialect
3809 /// counterpart of [`Self::insert`] for audited models.
3810 ///
3811 /// Snapshot-style audit (post-write field values).
3812 ///
3813 /// # Errors
3814 /// As [`Self::insert`].
3815 pub async fn insert_pool(
3816 &mut self,
3817 pool: &#root::sql::Pool,
3818 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3819 let mut _columns: ::std::vec::Vec<&'static str> =
3820 ::std::vec::Vec::new();
3821 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3822 ::std::vec::Vec::new();
3823 #( #pushes )*
3824 let _query = #root::core::InsertQuery::new(
3825 <Self as #root::core::Model>::SCHEMA,
3826 _columns,
3827 _values,
3828 )
3829 .returning(::std::vec![ #( #returning_cols ),* ]);
3830 #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self| {
3831 _m.__rustango_audit_entry(#root::audit::AuditOp::Create)
3832 })
3833 .await
3834 }
3835 }
3836 } else {
3837 quote!()
3838 }
3839 } else {
3840 // Keep the non-audited pool_insert_method we built earlier.
3841 pool_insert_method
3842 };
3843
3844 // Update audited save_pool: now that insert_pool is wired for
3845 // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3846 // insert_pool. Non-audited save_pool already does this.
3847 // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3848 // Replaces the snapshot-only emission with a per-backend transaction
3849 // body that:
3850 // 1. SELECTs the tracked columns by PK (typed Row::try_get per
3851 // column), capturing BEFORE values
3852 // 2. compiles the UPDATE via pool.dialect() and runs it on the tx
3853 // 3. builds AFTER pairs from &self
3854 // 4. diffs BEFORE/AFTER, emits one PendingEntry with
3855 // AuditOp::Update + diff_changes(...) on the same tx connection
3856 // 5. commits
3857 //
3858 // Per-backend arms inline the SQL string + placeholder shape, then
3859 // share the `audit_before_pair_tokens` decoder block (Row::try_get
3860 // is polymorphic over Row type — the same tokens work against
3861 // PgRow and MySqlRow as long as the field's Rust type implements
3862 // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3863 // primitives + chrono/uuid/serde_json::Value all do).
3864 let pool_save_method = if let Some(tracked) = audited_fields {
3865 if let Some((pk_ident, pk_col)) = primary_key {
3866 let pk_column_lit = pk_col.as_str();
3867 let partial = save_partial_methods(
3868 pk_ident,
3869 pk_column_lit,
3870 quote! {
3871 self.__rustango_save_with_diff(
3872 pool,
3873 &_query,
3874 ::core::option::Option::Some(&_wanted_cols),
3875 )
3876 .await
3877 },
3878 );
3879 // Per-tracked-column BEFORE-pair token list. Each entry
3880 // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3881 // The Row alias resolves to PgRow / MySqlRow per call site,
3882 // so the same template generates both the PG and MySQL bodies.
3883 let mk_before_pairs =
3884 |getter: &dyn Fn(&ColumnEntry) -> TokenStream2| -> Vec<TokenStream2> {
3885 tracked
3886 .iter()
3887 .map(|c| {
3888 let getter = getter(c);
3889 let column_lit = c.column.as_str();
3890 let value_ty = &c.value_ty;
3891 quote! {
3892 (
3893 #column_lit,
3894 match #getter::<#value_ty>(
3895 _audit_before_row, #column_lit,
3896 ) {
3897 ::core::result::Result::Ok(v) => {
3898 #root::__serde_json::to_value(&v)
3899 .unwrap_or(#root::__serde_json::Value::Null)
3900 }
3901 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3902 },
3903 )
3904 }
3905 })
3906 .collect()
3907 };
3908 let before_pairs_pg = mk_before_pairs(&|_| quote!(#root::sql::try_get_returning));
3909 let before_pairs_my = mk_before_pairs(&|c| {
3910 if c.uuid_column {
3911 quote!(#root::sql::try_get_flat_my)
3912 } else {
3913 quote!(#root::sql::try_get_returning_my)
3914 }
3915 });
3916 let before_pairs_sqlite =
3917 mk_before_pairs(&|_| quote!(#root::sql::try_get_returning_sqlite));
3918 let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
3919 let pk_value_for_bind = if fields.pk_is_auto {
3920 quote!(self.#pk_ident.get().copied().unwrap_or_default())
3921 } else {
3922 quote!(::core::clone::Clone::clone(&self.#pk_ident))
3923 };
3924 let assignments = &fields.update_assignments;
3925 let unset_dispatch = if fields.has_auto {
3926 quote! {
3927 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3928 return self.insert_pool(pool).await.map(|()| 1u64);
3929 }
3930 }
3931 } else {
3932 quote!()
3933 };
3934 quote! {
3935 /// Save this row against either backend with audit
3936 /// emission (diff-style: BEFORE+AFTER) inside the
3937 /// same transaction. Auto::Unset PK routes to
3938 /// insert_pool. Bi-dialect counterpart of
3939 /// [`Self::save`] for audited models.
3940 ///
3941 /// The audit row's `changes` JSON contains one
3942 /// `{ "field": { "before": …, "after": … } }` entry
3943 /// per tracked column whose value actually changed
3944 /// — same shape as the existing &PgPool save() emits.
3945 /// Keyed on the PK; global scopes do not apply.
3946 ///
3947 /// # Errors
3948 /// As [`Self::save`].
3949 pub async fn save_pool(
3950 &mut self,
3951 pool: &#root::sql::Pool,
3952 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3953 #unset_dispatch
3954 let _query = #root::core::UpdateQuery::new(
3955 <Self as #root::core::Model>::SCHEMA,
3956 ::std::vec![ #( #assignments ),* ],
3957 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3958 #pk_column_lit,
3959 #root::core::Op::Eq,
3960 ::core::convert::Into::<#root::core::SqlValue>::into(
3961 ::core::clone::Clone::clone(&self.#pk_ident)
3962 ),
3963 )),
3964 );
3965 self.__rustango_save_with_diff(pool, &_query, ::core::option::Option::None)
3966 .await
3967 }
3968
3969 /// Run `query` and write its diff entry, limited to `only` columns.
3970 #[doc(hidden)]
3971 async fn __rustango_save_with_diff(
3972 &self,
3973 pool: &#root::sql::Pool,
3974 query: &#root::core::UpdateQuery,
3975 only: ::core::option::Option<&::std::collections::HashSet<&'static str>>,
3976 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3977 let mut _after_pairs = self.__rustango_audit_pairs();
3978 if let ::core::option::Option::Some(only) = only {
3979 _after_pairs.retain(|(col, _)| only.contains(col));
3980 }
3981 let _before_query = #root::audit::before_image_query(
3982 <Self as #root::core::Model>::SCHEMA,
3983 #pk_column_lit,
3984 ::core::convert::Into::<#root::core::SqlValue>::into(
3985 #pk_value_for_bind,
3986 ),
3987 &[ #( #tracked_cols ),* ],
3988 );
3989 #root::audit::save_one_with_diff(
3990 pool,
3991 query,
3992 &_before_query,
3993 <Self as #root::core::Model>::SCHEMA.table,
3994 self.__rustango_audit_pk(),
3995 _after_pairs,
3996 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
3997 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
3998 |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
3999 ).await
4000 }
4001
4002 #partial
4003 }
4004 } else {
4005 quote!()
4006 }
4007 } else {
4008 pool_save_method
4009 };
4010
4011 // `delete_pool(&Pool)` — emitted for every model with a PK. Two
4012 // body shapes:
4013 // - non-audited: simple dispatch through `sql::delete_pool`
4014 // - audited: routes through `audit::delete_one_with_audit`,
4015 // which opens a per-backend transaction wrapping DELETE +
4016 // audit emit so the data write and audit row commit atomically.
4017 let pool_delete_method = {
4018 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
4019 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
4020 if let Some(pk_ident) = pk_ident_for_pool {
4021 if audited_fields.is_some() {
4022 quote! {
4023 /// Delete this row against either backend with audit
4024 /// emission inside the same transaction. Bi-dialect
4025 /// counterpart of [`Self::delete`] for audited models.
4026 /// Keyed on the PK; global scopes do not apply.
4027 ///
4028 /// # Errors
4029 /// As [`Self::delete`].
4030 pub async fn delete_pool(
4031 &self,
4032 pool: &#root::sql::Pool,
4033 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4034 let _query = #root::core::DeleteQuery::by_pk(
4035 <Self as #root::core::Model>::SCHEMA,
4036 #pk_column_lit,
4037 ::core::convert::Into::<#root::core::SqlValue>::into(
4038 ::core::clone::Clone::clone(&self.#pk_ident)
4039 ),
4040 );
4041 let _audit_entry =
4042 self.__rustango_audit_entry(#root::audit::AuditOp::Delete);
4043 #root::audit::delete_one_with_audit(
4044 pool, &_query, &_audit_entry,
4045 ).await
4046 }
4047 }
4048 } else {
4049 quote! {
4050 /// Delete the row identified by this instance's primary key
4051 /// against either backend. Equivalent to [`Self::delete`] but
4052 /// takes [`#root::sql::Pool`] and dispatches per backend.
4053 /// Global scopes do not apply.
4054 ///
4055 /// # Errors
4056 /// As [`Self::delete`].
4057 pub async fn delete_pool(
4058 &self,
4059 pool: &#root::sql::Pool,
4060 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4061 let _query = #root::core::DeleteQuery::by_pk(
4062 <Self as #root::core::Model>::SCHEMA,
4063 #pk_column_lit,
4064 ::core::convert::Into::<#root::core::SqlValue>::into(
4065 ::core::clone::Clone::clone(&self.#pk_ident)
4066 ),
4067 );
4068 #root::sql::delete_pool(pool, &_query).await
4069 }
4070 }
4071 }
4072 } else {
4073 quote!()
4074 }
4075 };
4076
4077 // Bulk writers on audited models write one audit row per affected
4078 // row, in the write's transaction (#1747).
4079 let (bulk_delete_run, bulk_update_run, truncate_run) = if audited_fields.is_some() {
4080 (
4081 quote!(#root::audit::delete_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4082 _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4083 })
4084 .await),
4085 quote!(#root::audit::update_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4086 _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
4087 })
4088 .await),
4089 quote!(#root::audit::truncate_with_audit(pool, _table, &_sql).await),
4090 )
4091 } else {
4092 (
4093 quote!(#root::sql::delete_pool(pool, &_query).await),
4094 quote!(#root::sql::update_pool(pool, &_query).await),
4095 quote!(#root::sql::raw_execute_pool(pool, &_sql, ::std::vec::Vec::new()).await),
4096 )
4097 };
4098
4099 // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
4100 // matching this instance's PK and overwrite the in-memory state
4101 // with the freshly-fetched columns.
4102 // Issue #825. Only emitted when the model declares a PK; non-PK
4103 // models can't address a specific row.
4104 //
4105 // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
4106 // every field from `self`; resets the PK to `Auto::Unset` when
4107 // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
4108 // a fresh autoincrement value. Non-Auto PKs preserve the source
4109 // PK — the caller must overwrite before insert. Pure-Rust, no
4110 // I/O, no dialect surface.
4111 let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
4112 let other_field_clones: Vec<TokenStream2> = fields
4113 .column_entries
4114 .iter()
4115 .filter(|c| &c.ident != pk_ident)
4116 .map(|c| {
4117 let ident = &c.ident;
4118 quote! {
4119 #ident: ::core::clone::Clone::clone(&self.#ident)
4120 }
4121 })
4122 .collect();
4123 let pk_clone_token = if fields.pk_is_auto {
4124 quote! { #pk_ident: #root::sql::Auto::Unset }
4125 } else {
4126 quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
4127 };
4128 let replicate_doc = if fields.pk_is_auto {
4129 quote! {
4130 /// Eloquent-style `replicate()` — return a clone of this
4131 /// row with the primary key reset to [`Auto::Unset`] so
4132 /// the copy is ready for `insert_pool` / `save_pool` to
4133 /// allocate a fresh autoincrement value. Every other
4134 /// field is `Clone`d verbatim. Issue #825.
4135 ///
4136 /// `auto_now_add` / `auto_now` timestamp fields are
4137 /// **not** reset (Eloquent's `replicate` doesn't reset
4138 /// them either) — pass them through the normal insert
4139 /// path if you want fresh values, or assign them
4140 /// explicitly after the call.
4141 }
4142 } else {
4143 quote! {
4144 /// Eloquent-style `replicate()` — clone this row
4145 /// verbatim. Because the primary key is **not** an
4146 /// `Auto<T>`, the clone keeps the source PK; the
4147 /// caller must overwrite `copy.<pk>` before inserting
4148 /// to avoid a unique-key violation. Issue #825.
4149 }
4150 };
4151 // 2026-06-07 — field-name / shortcut collision guard.
4152 //
4153 // The macro emits both `pub const <field>: <field>_col = ...`
4154 // (per-field typed-column const, used by the typed-builder
4155 // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
4156 // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
4157 // model has a field named e.g. `count`, both items would land
4158 // in the same inherent impl with the same name, and the
4159 // compiler rejects the derive with "duplicate definitions".
4160 //
4161 // Drop the conflicting shortcut for that model. Callers can
4162 // still reach the same behavior via
4163 // `QuerySet::<Model>::default().count(&pool)`.
4164 let column_names: ::std::collections::HashSet<String> = fields
4165 .column_entries
4166 .iter()
4167 .map(|c| ident_name(&c.ident))
4168 .collect();
4169 let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
4170 if column_names.contains(name) {
4171 quote! {}
4172 } else {
4173 tokens
4174 }
4175 };
4176 let count_method = emit_if_no_field_collision(
4177 "count",
4178 quote! {
4179 /// Count rows of this model — `SELECT COUNT(*) FROM
4180 /// <table>`. Eloquent `Model::count()` parity.
4181 ///
4182 /// Skipped on models that already declare a field named
4183 /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
4184 /// in that case.
4185 ///
4186 /// # Errors
4187 /// As [`CounterPool::count`].
4188 ///
4189 /// [`CounterPool::count`]: rustango::sql::CounterPool::count
4190 pub async fn count(
4191 pool: &#root::sql::Pool,
4192 ) -> ::core::result::Result<i64, #root::sql::ExecError> {
4193 use #root::sql::CounterPool as _;
4194 #root::query::QuerySet::<Self>::default()
4195 .count(pool)
4196 .await
4197 }
4198 },
4199 );
4200 let value_method = emit_if_no_field_collision(
4201 "value",
4202 quote! {
4203 /// Pluck a single scalar from the first row.
4204 /// Eloquent `Model::query()->value($col)` parity.
4205 ///
4206 /// Skipped on models that already declare a field named
4207 /// `value`. Drop into
4208 /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4209 ///
4210 /// # Errors
4211 /// As `ValuesFlatQuerySet::first`.
4212 pub async fn value<U>(
4213 col: &str,
4214 pool: &#root::sql::Pool,
4215 ) -> ::core::result::Result<
4216 ::core::option::Option<U>,
4217 #root::sql::ExecError,
4218 >
4219 where
4220 U: #root::sql::FlatScalar,
4221 {
4222 let _col_static: &'static str = Self::__resolve_col(col)?;
4223 #root::query::QuerySet::<Self>::default()
4224 .values_list_flat(_col_static)
4225 .first::<U>(pool)
4226 .await
4227 }
4228 },
4229 );
4230 let sum_method = emit_if_no_field_collision(
4231 "sum",
4232 quote! {
4233 /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4234 /// Skipped on models that already declare a field named
4235 /// `sum`.
4236 ///
4237 /// # Errors
4238 /// As [`#root::sql::fetch_aggregate_pool`].
4239 pub async fn sum<U>(
4240 col: &str,
4241 pool: &#root::sql::Pool,
4242 ) -> ::core::result::Result<
4243 ::core::option::Option<U>,
4244 #root::sql::ExecError,
4245 >
4246 where
4247 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4248 + #root::sql::MaybeMyFromRow
4249 + #root::sql::MaybeSqliteFromRow
4250 + ::core::marker::Send
4251 + ::core::marker::Unpin,
4252 {
4253 #root::query::QuerySet::<Self>::default()
4254 .sum::<U>(col, pool)
4255 .await
4256 }
4257 },
4258 );
4259 let avg_method = emit_if_no_field_collision(
4260 "avg",
4261 quote! {
4262 /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4263 /// models that already declare a field named `avg`.
4264 ///
4265 /// # Errors
4266 /// As [`#root::sql::fetch_aggregate_pool`].
4267 pub async fn avg<U>(
4268 col: &str,
4269 pool: &#root::sql::Pool,
4270 ) -> ::core::result::Result<
4271 ::core::option::Option<U>,
4272 #root::sql::ExecError,
4273 >
4274 where
4275 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4276 + #root::sql::MaybeMyFromRow
4277 + #root::sql::MaybeSqliteFromRow
4278 + ::core::marker::Send
4279 + ::core::marker::Unpin,
4280 {
4281 #root::query::QuerySet::<Self>::default()
4282 .avg::<U>(col, pool)
4283 .await
4284 }
4285 },
4286 );
4287 let min_method = emit_if_no_field_collision(
4288 "min",
4289 quote! {
4290 /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4291 /// models that already declare a field named `min`.
4292 ///
4293 /// # Errors
4294 /// As [`#root::sql::fetch_aggregate_pool`].
4295 pub async fn min<U>(
4296 col: &str,
4297 pool: &#root::sql::Pool,
4298 ) -> ::core::result::Result<
4299 ::core::option::Option<U>,
4300 #root::sql::ExecError,
4301 >
4302 where
4303 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4304 + #root::sql::MaybeMyFromRow
4305 + #root::sql::MaybeSqliteFromRow
4306 + ::core::marker::Send
4307 + ::core::marker::Unpin,
4308 {
4309 #root::query::QuerySet::<Self>::default()
4310 .min::<U>(col, pool)
4311 .await
4312 }
4313 },
4314 );
4315 let max_method = emit_if_no_field_collision(
4316 "max",
4317 quote! {
4318 /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4319 /// models that already declare a field named `max`.
4320 ///
4321 /// # Errors
4322 /// As [`#root::sql::fetch_aggregate_pool`].
4323 pub async fn max<U>(
4324 col: &str,
4325 pool: &#root::sql::Pool,
4326 ) -> ::core::result::Result<
4327 ::core::option::Option<U>,
4328 #root::sql::ExecError,
4329 >
4330 where
4331 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4332 + #root::sql::MaybeMyFromRow
4333 + #root::sql::MaybeSqliteFromRow
4334 + ::core::marker::Send
4335 + ::core::marker::Unpin,
4336 {
4337 #root::query::QuerySet::<Self>::default()
4338 .max::<U>(col, pool)
4339 .await
4340 }
4341 },
4342 );
4343 let first_method = emit_if_no_field_collision(
4344 "first",
4345 quote! {
4346 /// First row of this model. Eloquent `Model::first()`.
4347 /// Skipped on models that already declare a field named
4348 /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4349 ///
4350 /// # Errors
4351 /// As `QuerySet::first`.
4352 pub async fn first(
4353 pool: &#root::sql::Pool,
4354 ) -> ::core::result::Result<
4355 ::core::option::Option<Self>,
4356 #root::sql::ExecError,
4357 > {
4358 #root::query::QuerySet::<Self>::default()
4359 .first(pool)
4360 .await
4361 }
4362 },
4363 );
4364 let last_method = emit_if_no_field_collision(
4365 "last",
4366 quote! {
4367 /// Last row of this model by primary-key DESC.
4368 /// Eloquent `Model::query()->latest('id')->first()`
4369 /// parity — fetches the highest-PK row without
4370 /// requiring the caller to spell the PK column.
4371 /// Returns `None` on an empty table.
4372 ///
4373 /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4374 /// Skipped on models that already declare a field
4375 /// named `last`.
4376 ///
4377 /// # Errors
4378 /// As `QuerySet::last`.
4379 pub async fn last(
4380 pool: &#root::sql::Pool,
4381 ) -> ::core::result::Result<
4382 ::core::option::Option<Self>,
4383 #root::sql::ExecError,
4384 > {
4385 #root::query::QuerySet::<Self>::default()
4386 .last(pool)
4387 .await
4388 }
4389 },
4390 );
4391 quote! {
4392 /// Re-SELECT this row by its primary key and overwrite
4393 /// every in-memory field with the freshly-fetched value.
4394 /// Issue #825.
4395 ///
4396 /// Use this when the row may have been modified by another
4397 /// process / connection / job since you read it — e.g. after
4398 /// a queued task callback, or to re-sync stale UI state
4399 /// before re-saving.
4400 ///
4401 /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4402 /// when the primary key no longer matches any row (e.g.
4403 /// the row was deleted concurrently).
4404 ///
4405 /// # Errors
4406 /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4407 /// the PK no longer exists.
4408 ///
4409 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4410 pub async fn refresh_from_db(
4411 &mut self,
4412 pool: &#root::sql::Pool,
4413 ) -> ::core::result::Result<(), #root::sql::ExecError> {
4414 use #root::sql::FetcherPool as _;
4415 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4416 ::core::clone::Clone::clone(&self.#pk_ident),
4417 );
4418 let mut _rows: ::std::vec::Vec<Self> =
4419 #root::query::QuerySet::<Self>::default()
4420 .filter(::core::stringify!(#pk_ident), _pk_val)
4421 .limit(1)
4422 .fetch(pool)
4423 .await?;
4424 match _rows.into_iter().next() {
4425 ::core::option::Option::Some(_fresh) => {
4426 *self = _fresh;
4427 ::core::result::Result::Ok(())
4428 }
4429 ::core::option::Option::None => ::core::result::Result::Err(
4430 #root::sql::ExecError::Driver(
4431 #root::sql::sqlx::Error::RowNotFound,
4432 ),
4433 ),
4434 }
4435 }
4436
4437 /// Atomically increment the integer column `col` by
4438 /// `by` for this row. Equivalent to
4439 /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4440 ///
4441 /// **Doesn't mutate `self`** — the in-memory copy is now
4442 /// stale; call [`Self::refresh_from_db`] /
4443 /// [`Self::fresh`] to re-sync. Returns the rows-
4444 /// affected count (0 when the PK doesn't match any row,
4445 /// 1 on success).
4446 ///
4447 /// `col` is the Rust field name as a string; unknown
4448 /// fields surface as `UnknownField` at runtime. Negative
4449 /// `by` values atomically decrement (see also
4450 /// [`Self::decrement`]).
4451 ///
4452 /// # Errors
4453 /// As [`UpdaterPool::execute_pool`].
4454 ///
4455 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4456 pub async fn increment(
4457 &self,
4458 col: &str,
4459 by: i64,
4460 pool: &#root::sql::Pool,
4461 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4462 Self::__increment_one(self, col, by, pool).await
4463 }
4464
4465 /// Sibling of [`Self::increment`] — atomically
4466 /// decrement this row's `col` by `by`. Eloquent
4467 /// `$model->decrement($col, $by)` parity. Equivalent to
4468 /// `self.increment(col, -by, &pool)`; the separate name
4469 /// keeps call sites readable.
4470 ///
4471 /// # Errors
4472 /// As [`Self::increment`].
4473 pub async fn decrement(
4474 &self,
4475 col: &str,
4476 by: i64,
4477 pool: &#root::sql::Pool,
4478 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4479 Self::__increment_one(self, col, -by, pool).await
4480 }
4481
4482 /// Bulk-increment: add `by` to `col` on every row of the
4483 /// table. Eloquent `Model::query()->increment($col, $by)`
4484 /// parity. Use for counters, score adjustments, view
4485 /// rollups.
4486 ///
4487 /// # Errors
4488 /// As [`UpdaterPool::execute_pool`].
4489 ///
4490 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4491 pub async fn increment_each(
4492 col: &str,
4493 by: i64,
4494 pool: &#root::sql::Pool,
4495 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4496 Self::__increment_all(col, by, pool).await
4497 }
4498
4499 /// Sibling of [`Self::increment_each`] — bulk-decrement.
4500 ///
4501 /// # Errors
4502 /// As [`Self::increment_each`].
4503 pub async fn decrement_each(
4504 col: &str,
4505 by: i64,
4506 pool: &#root::sql::Pool,
4507 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4508 Self::__increment_all(col, -by, pool).await
4509 }
4510
4511 /// Internal: forward to
4512 /// [`#root::sql::model_shortcuts::increment_one_pool`].
4513 /// One-line wrapper kept as a per-Model method so the
4514 /// macro's emitted `increment` / `decrement` instance
4515 /// calls don't have to thread `Self` through manually.
4516 #[doc(hidden)]
4517 pub async fn __increment_one(
4518 this: &Self,
4519 col: &str,
4520 by: i64,
4521 pool: &#root::sql::Pool,
4522 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4523 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4524 ::core::clone::Clone::clone(&this.#pk_ident),
4525 );
4526 #root::sql::model_shortcuts::increment_one_pool::<Self>(
4527 ::core::stringify!(#pk_ident),
4528 _pk_val,
4529 col,
4530 by,
4531 pool,
4532 )
4533 .await
4534 }
4535
4536 /// Internal: run
4537 /// [`#root::sql::model_shortcuts::increment_all_query`].
4538 #[doc(hidden)]
4539 pub async fn __increment_all(
4540 col: &str,
4541 by: i64,
4542 pool: &#root::sql::Pool,
4543 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4544 let _query =
4545 #root::sql::model_shortcuts::increment_all_query::<Self>(col, by)?;
4546 #bulk_update_run
4547 }
4548
4549 /// Internal: forward to
4550 /// [`#root::sql::model_shortcuts::resolve_col`].
4551 #[doc(hidden)]
4552 pub fn __resolve_col(
4553 col: &str,
4554 ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4555 #root::sql::model_shortcuts::resolve_col::<Self>(col)
4556 }
4557
4558 /// Internal: forward to
4559 /// [`#root::sql::model_shortcuts::add_signed_expr`].
4560 #[doc(hidden)]
4561 #[must_use]
4562 pub fn __add_signed_expr(
4563 col_static: &'static str,
4564 signed_by: i64,
4565 ) -> #root::core::Expr {
4566 #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4567 }
4568
4569 /// Re-SELECT this row by its primary key and return a
4570 /// **new** instance with the freshly-fetched fields.
4571 /// Eloquent `Model::fresh()` parity — non-mutating
4572 /// counterpart of [`Self::refresh_from_db`].
4573 ///
4574 /// Returns `Ok(None)` when the row was deleted
4575 /// concurrently — vs [`Self::refresh_from_db`]
4576 /// which surfaces that as `RowNotFound` because
4577 /// in-place mutation has nothing to write to.
4578 ///
4579 /// Useful when you want to compare the in-memory
4580 /// instance against the persisted state (audit-style
4581 /// diffs, conflict detection) without mutating the
4582 /// reference you already hold.
4583 ///
4584 /// # Errors
4585 /// As [`FetcherPool::fetch`].
4586 ///
4587 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4588 pub async fn fresh(
4589 &self,
4590 pool: &#root::sql::Pool,
4591 ) -> ::core::result::Result<
4592 ::core::option::Option<Self>,
4593 #root::sql::ExecError,
4594 > {
4595 use #root::sql::FetcherPool as _;
4596 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4597 ::core::clone::Clone::clone(&self.#pk_ident),
4598 );
4599 let _rows: ::std::vec::Vec<Self> =
4600 #root::query::QuerySet::<Self>::default()
4601 .filter(::core::stringify!(#pk_ident), _pk_val)
4602 .limit(1)
4603 .fetch(pool)
4604 .await?;
4605 ::core::result::Result::Ok(_rows.into_iter().next())
4606 }
4607
4608 #replicate_doc
4609 #[must_use]
4610 pub fn replicate(&self) -> Self {
4611 Self {
4612 #pk_clone_token,
4613 #( #other_field_clones, )*
4614 }
4615 }
4616
4617 #first_method
4618
4619 #last_method
4620
4621 /// Throwing counterpart of [`Self::first`] —
4622 /// errors with `RowNotFound` when the table is empty.
4623 /// Eloquent `Model::firstOrFail()` parity.
4624 ///
4625 /// # Errors
4626 /// As [`Self::first`]; additionally
4627 /// [`sqlx::Error::RowNotFound`] on empty tables.
4628 ///
4629 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4630 pub async fn first_or_fail(
4631 pool: &#root::sql::Pool,
4632 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4633 // Route through the queryset rather than `Self::first`,
4634 // which is suppressed on models with a field named
4635 // `first` (field/shortcut collision guard).
4636 match #root::query::QuerySet::<Self>::default().first(pool).await? {
4637 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4638 ::core::option::Option::None => ::core::result::Result::Err(
4639 #root::sql::ExecError::Driver(
4640 #root::sql::sqlx::Error::RowNotFound,
4641 ),
4642 ),
4643 }
4644 }
4645
4646 /// Single-column projection — `SELECT <col> FROM
4647 /// <table>`. Returns `Vec<U>` where each element is the
4648 /// decoded value of the column. Eloquent
4649 /// `Model::pluck($column)` parity.
4650 ///
4651 /// Thin wrapper over `QuerySet::<Self>::default()
4652 /// .values_list_flat(col).fetch::<U>(pool)`. `U` is a
4653 /// `FlatScalar`; use `Option<_>` for a nullable column.
4654 ///
4655 /// # Errors
4656 /// As `ValuesFlatQuerySet::fetch`.
4657 pub async fn pluck<U>(
4658 col: &'static str,
4659 pool: &#root::sql::Pool,
4660 ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4661 where
4662 U: #root::sql::FlatScalar,
4663 {
4664 #root::query::QuerySet::<Self>::default()
4665 .values_list_flat(col)
4666 .fetch::<U>(pool)
4667 .await
4668 }
4669
4670 /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4671 /// stream every row of this model in batches of `n`,
4672 /// invoking the callback once per batch. Stable PK-ASC
4673 /// ordering so the LIMIT/OFFSET pagination is
4674 /// deterministic across drivers.
4675 ///
4676 /// The callback is async — it can do further DB work
4677 /// (writes, related-row lookups, queue dispatch) per
4678 /// batch. Return `Err(...)` from the callback to abort
4679 /// the iteration early; the error bubbles up.
4680 ///
4681 /// **When to use**: bulk processing flows that can't
4682 /// fit the whole table in memory — sending newsletters,
4683 /// running data migrations, computing summary
4684 /// statistics. For small / known-bounded tables, plain
4685 /// `Self::all(&pool)` is simpler.
4686 ///
4687 /// **Caveat**: ascending OFFSET pagination is O(N²) on
4688 /// large tables. For multi-million-row scans prefer
4689 /// keyset-by-PK (the standard "WHERE id > last_seen"
4690 /// shape) over `chunk(...)`.
4691 ///
4692 /// Skipped on models without a primary key — chunking
4693 /// needs a stable order to avoid skipping / repeating
4694 /// rows across batches.
4695 pub async fn chunk<F, Fut>(
4696 n: i64,
4697 pool: &#root::sql::Pool,
4698 mut cb: F,
4699 ) -> ::core::result::Result<(), #root::sql::ExecError>
4700 where
4701 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4702 Fut: ::core::future::Future<
4703 Output = ::core::result::Result<(), #root::sql::ExecError>,
4704 >,
4705 {
4706 use #root::sql::FetcherPool as _;
4707 let pk_col = match Self::primary_key_column() {
4708 ::core::option::Option::Some(c) => c,
4709 ::core::option::Option::None => {
4710 return ::core::result::Result::Ok(());
4711 }
4712 };
4713 let mut offset: i64 = 0;
4714 loop {
4715 let rows: ::std::vec::Vec<Self> =
4716 #root::query::QuerySet::<Self>::default()
4717 .order_by(&[(pk_col, false)])
4718 .limit(n)
4719 .offset(offset)
4720 .fetch(pool)
4721 .await?;
4722 if rows.is_empty() {
4723 return ::core::result::Result::Ok(());
4724 }
4725 let len = rows.len() as i64;
4726 cb(rows).await?;
4727 if len < n {
4728 return ::core::result::Result::Ok(());
4729 }
4730 offset += n;
4731 }
4732 }
4733
4734 /// Eloquent `Model::chunkById($n, fn (...))` — same
4735 /// per-batch callback shape as [`Self::chunk`], but uses
4736 /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4737 /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4738 /// — the right choice for multi-million-row sweeps.
4739 ///
4740 /// Requires the primary key to be a signed integer type
4741 /// (`i64` / `i32`); the keyset comparison rides on
4742 /// `__rustango_pk_value()` lowering through
4743 /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4744 /// non-integer PKs (UUID, String) — those should use
4745 /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4746 /// keyset loop.
4747 ///
4748 /// Callback errors abort iteration; the error bubbles up
4749 /// unchanged. Empty table → callback invoked zero times.
4750 pub async fn chunk_by_id<F, Fut>(
4751 n: i64,
4752 pool: &#root::sql::Pool,
4753 mut cb: F,
4754 ) -> ::core::result::Result<(), #root::sql::ExecError>
4755 where
4756 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4757 Fut: ::core::future::Future<
4758 Output = ::core::result::Result<(), #root::sql::ExecError>,
4759 >,
4760 {
4761 use #root::sql::FetcherPool as _;
4762 let pk_col = match Self::primary_key_column() {
4763 ::core::option::Option::Some(c) => c,
4764 ::core::option::Option::None => {
4765 return ::core::result::Result::Ok(());
4766 }
4767 };
4768 // Track the largest PK seen so the next batch picks
4769 // up from there. `i64::MIN` as the sentinel — the
4770 // very first iteration's `> MIN` matches every row,
4771 // so the loop entry is uniform with subsequent
4772 // iterations.
4773 let mut last_seen: i64 = i64::MIN;
4774 loop {
4775 let key = ::std::format!("{}__gt", pk_col);
4776 let rows: ::std::vec::Vec<Self> =
4777 #root::query::QuerySet::<Self>::default()
4778 .filter(key.as_str(), last_seen)
4779 .order_by(&[(pk_col, false)])
4780 .limit(n)
4781 .fetch(pool)
4782 .await?;
4783 if rows.is_empty() {
4784 return ::core::result::Result::Ok(());
4785 }
4786 let len = rows.len() as i64;
4787 // Capture the last row's PK BEFORE moving rows
4788 // into the callback.
4789 let max_pk = match rows
4790 .last()
4791 .map(|r| r.__rustango_pk_value())
4792 {
4793 ::core::option::Option::Some(
4794 #root::core::SqlValue::I64(v),
4795 ) => v,
4796 ::core::option::Option::Some(
4797 #root::core::SqlValue::I32(v),
4798 ) => i64::from(v),
4799 _ => return ::core::result::Result::Ok(()),
4800 };
4801 cb(rows).await?;
4802 if len < n {
4803 return ::core::result::Result::Ok(());
4804 }
4805 last_seen = max_pk;
4806 }
4807 }
4808
4809 /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4810 /// per-row callback companion to [`Self::chunk`].
4811 /// Streams every row in keyset-paginated batches of
4812 /// `batch` size, calling `cb` once per row.
4813 ///
4814 /// Inherits the keyset-paginated scan of
4815 /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4816 /// non-integer PKs are silently a no-op).
4817 ///
4818 /// ```ignore
4819 /// Post::each(500, &pool, |p| async move {
4820 /// reindex(p).await?;
4821 /// Ok(())
4822 /// }).await?;
4823 /// ```
4824 ///
4825 /// Return `Err(...)` from the callback to abort
4826 /// iteration; the error bubbles up unchanged.
4827 pub async fn each<F, Fut>(
4828 batch: i64,
4829 pool: &#root::sql::Pool,
4830 mut cb: F,
4831 ) -> ::core::result::Result<(), #root::sql::ExecError>
4832 where
4833 F: ::core::ops::FnMut(Self) -> Fut,
4834 Fut: ::core::future::Future<
4835 Output = ::core::result::Result<(), #root::sql::ExecError>,
4836 >,
4837 {
4838 use #root::sql::FetcherPool as _;
4839 let pk_col = match Self::primary_key_column() {
4840 ::core::option::Option::Some(c) => c,
4841 ::core::option::Option::None => {
4842 return ::core::result::Result::Ok(());
4843 }
4844 };
4845 let mut last_seen: i64 = i64::MIN;
4846 loop {
4847 let key = ::std::format!("{}__gt", pk_col);
4848 let rows: ::std::vec::Vec<Self> =
4849 #root::query::QuerySet::<Self>::default()
4850 .filter(key.as_str(), last_seen)
4851 .order_by(&[(pk_col, false)])
4852 .limit(batch)
4853 .fetch(pool)
4854 .await?;
4855 if rows.is_empty() {
4856 return ::core::result::Result::Ok(());
4857 }
4858 let len = rows.len() as i64;
4859 let max_pk = match rows
4860 .last()
4861 .map(|r| r.__rustango_pk_value())
4862 {
4863 ::core::option::Option::Some(
4864 #root::core::SqlValue::I64(v),
4865 ) => v,
4866 ::core::option::Option::Some(
4867 #root::core::SqlValue::I32(v),
4868 ) => i64::from(v),
4869 _ => return ::core::result::Result::Ok(()),
4870 };
4871 for row in rows {
4872 cb(row).await?;
4873 }
4874 if len < batch {
4875 return ::core::result::Result::Ok(());
4876 }
4877 last_seen = max_pk;
4878 }
4879 }
4880
4881 /// Delete every row of this model — `TRUNCATE TABLE
4882 /// <table> RESTART IDENTITY CASCADE` on Postgres,
4883 /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4884 /// support `TRUNCATE` inside foreign-key constraints
4885 /// or — for SQLite — at all). Eloquent `Model::truncate()`
4886 /// parity.
4887 ///
4888 /// **Use only in tests / fixture-reset flows.** Production
4889 /// writes through this would silently bypass the
4890 /// `pre_delete` / `post_delete` signals (no per-row hooks
4891 /// fire on a TRUNCATE / bulk DELETE FROM). An audited model
4892 /// gets one bulk audit entry, not one per row. Global scopes
4893 /// do not apply: every row goes.
4894 ///
4895 /// # Errors
4896 /// As [`raw_execute_pool`].
4897 ///
4898 /// [`raw_execute_pool`]: rustango::sql::raw_execute_pool
4899 pub async fn truncate(
4900 pool: &#root::sql::Pool,
4901 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4902 let _table = <Self as #root::core::Model>::SCHEMA.table;
4903 let _dialect = pool.dialect();
4904 let _quoted = _dialect.quote_ident(_table);
4905 let _sql = if _dialect.name() == "postgres" {
4906 ::std::format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", _quoted)
4907 } else {
4908 ::std::format!("DELETE FROM {}", _quoted)
4909 };
4910 #truncate_run
4911 }
4912
4913 /// Bulk-delete every row whose primary key is in
4914 /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4915 /// Returns the affected row count.
4916 ///
4917 /// Eloquent `Model::destroy([1, 2, 3])` parity.
4918 /// Empty `pks` is a no-op (returns 0).
4919 ///
4920 /// Accepts any iterable whose elements are
4921 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4922 /// `[i64; N]`, etc.
4923 ///
4924 /// # Errors
4925 /// As `delete_pool`.
4926 pub async fn destroy<V>(
4927 pks: impl ::core::iter::IntoIterator<Item = V>,
4928 pool: &#root::sql::Pool,
4929 ) -> ::core::result::Result<u64, #root::sql::ExecError>
4930 where
4931 V: ::core::convert::Into<#root::core::SqlValue>,
4932 {
4933 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4934 pks.into_iter().map(::core::convert::Into::into).collect();
4935 if _values.is_empty() {
4936 return ::core::result::Result::Ok(0);
4937 }
4938 let _pk = <Self as #root::core::Model>::SCHEMA
4939 .primary_key()
4940 .ok_or_else(|| {
4941 #root::sql::ExecError::Sql(#root::sql::SqlError::MissingPrimaryKey)
4942 })?;
4943 let _query = #root::query::QuerySet::<Self>::default()
4944 .filter_op(
4945 _pk.name,
4946 #root::core::Op::In,
4947 #root::core::SqlValue::List(_values),
4948 )
4949 .compile_delete()?;
4950 #bulk_delete_run
4951 }
4952
4953 /// Fetch every row where `<col> = <val>`. Eloquent
4954 /// `Model::where($col, $val)->get()` parity.
4955 ///
4956 /// Thin wrapper over `QuerySet::<Self>::default()
4957 /// .filter(col, val).fetch(pool)`. For one row,
4958 /// use [`Self::first_where`]; for a chain that
4959 /// needs further `.filter()` / `.order_by()` /
4960 /// `.limit()`, drop down to `Self::query().filter(...)`
4961 /// directly.
4962 ///
4963 /// `val` accepts any value `Into<SqlValue>` so plain
4964 /// strings, ints, UUIDs, etc. all work.
4965 ///
4966 /// # Errors
4967 /// As [`FetcherPool::fetch`].
4968 ///
4969 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4970 pub async fn where_(
4971 col: &str,
4972 val: impl ::core::convert::Into<#root::core::SqlValue>,
4973 pool: &#root::sql::Pool,
4974 ) -> ::core::result::Result<
4975 ::std::vec::Vec<Self>,
4976 #root::sql::ExecError,
4977 > {
4978 use #root::sql::FetcherPool as _;
4979 #root::query::QuerySet::<Self>::default()
4980 .filter(col, val)
4981 .fetch(pool)
4982 .await
4983 }
4984
4985 /// Fetch every row where `<col> IN (vals)`. Eloquent
4986 /// `Model::whereIn($col, $vals)->get()` parity. Empty
4987 /// `vals` returns no rows (matches SQL's empty-IN
4988 /// semantics).
4989 ///
4990 /// `vals` accepts any iterable whose items are
4991 /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
4992 /// etc.
4993 ///
4994 /// # Errors
4995 /// As [`FetcherPool::fetch`].
4996 ///
4997 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4998 pub async fn where_in<V>(
4999 col: &str,
5000 vals: impl ::core::iter::IntoIterator<Item = V>,
5001 pool: &#root::sql::Pool,
5002 ) -> ::core::result::Result<
5003 ::std::vec::Vec<Self>,
5004 #root::sql::ExecError,
5005 >
5006 where
5007 V: ::core::convert::Into<#root::core::SqlValue>,
5008 {
5009 use #root::sql::FetcherPool as _;
5010 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5011 vals.into_iter().map(::core::convert::Into::into).collect();
5012 if _values.is_empty() {
5013 return ::core::result::Result::Ok(::std::vec::Vec::new());
5014 }
5015 let _key = ::std::format!("{}__in", col);
5016 #root::query::QuerySet::<Self>::default()
5017 .filter(&_key, #root::core::SqlValue::List(_values))
5018 .fetch(pool)
5019 .await
5020 }
5021
5022 /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
5023 /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
5024 /// `vals` returns every row (matches SQL's empty-NOT-IN
5025 /// semantics — vacuously true for every row).
5026 ///
5027 /// # Errors
5028 /// As [`FetcherPool::fetch`].
5029 ///
5030 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5031 pub async fn where_not_in<V>(
5032 col: &str,
5033 vals: impl ::core::iter::IntoIterator<Item = V>,
5034 pool: &#root::sql::Pool,
5035 ) -> ::core::result::Result<
5036 ::std::vec::Vec<Self>,
5037 #root::sql::ExecError,
5038 >
5039 where
5040 V: ::core::convert::Into<#root::core::SqlValue>,
5041 {
5042 use #root::sql::FetcherPool as _;
5043 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5044 vals.into_iter().map(::core::convert::Into::into).collect();
5045 if _values.is_empty() {
5046 return #root::query::QuerySet::<Self>::default()
5047 .fetch(pool)
5048 .await;
5049 }
5050 let _key = ::std::format!("{}__not_in", col);
5051 #root::query::QuerySet::<Self>::default()
5052 .filter(&_key, #root::core::SqlValue::List(_values))
5053 .fetch(pool)
5054 .await
5055 }
5056
5057 /// Fetch every row where `<col> IS NULL`. Eloquent
5058 /// `Model::whereNull($col)->get()` parity.
5059 ///
5060 /// # Errors
5061 /// As [`FetcherPool::fetch`].
5062 ///
5063 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5064 pub async fn where_null(
5065 col: &str,
5066 pool: &#root::sql::Pool,
5067 ) -> ::core::result::Result<
5068 ::std::vec::Vec<Self>,
5069 #root::sql::ExecError,
5070 > {
5071 use #root::sql::FetcherPool as _;
5072 let _key = ::std::format!("{}__isnull", col);
5073 #root::query::QuerySet::<Self>::default()
5074 .filter(&_key, #root::core::SqlValue::Bool(true))
5075 .fetch(pool)
5076 .await
5077 }
5078
5079 /// Fetch every row where `<col> IS NOT NULL`. Eloquent
5080 /// `Model::whereNotNull($col)->get()` parity.
5081 ///
5082 /// # Errors
5083 /// As [`FetcherPool::fetch`].
5084 ///
5085 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5086 pub async fn where_not_null(
5087 col: &str,
5088 pool: &#root::sql::Pool,
5089 ) -> ::core::result::Result<
5090 ::std::vec::Vec<Self>,
5091 #root::sql::ExecError,
5092 > {
5093 use #root::sql::FetcherPool as _;
5094 let _key = ::std::format!("{}__isnull", col);
5095 #root::query::QuerySet::<Self>::default()
5096 .filter(&_key, #root::core::SqlValue::Bool(false))
5097 .fetch(pool)
5098 .await
5099 }
5100
5101 /// Fetch up to `n` rows in random order. Eloquent
5102 /// `Model::inRandomOrder()->limit($n)->get()` /
5103 /// `Model::query()->inRandomOrder()->get()->take($n)`
5104 /// parity. **Performance caveat**: random ordering
5105 /// forces a full table scan + per-row random key sort;
5106 /// the optimizer cannot use an index. Prefer a
5107 /// `pk >= rand_offset LIMIT N` walk for huge tables.
5108 ///
5109 /// # Errors
5110 /// As [`FetcherPool::fetch`].
5111 ///
5112 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5113 pub async fn random_n(
5114 n: i64,
5115 pool: &#root::sql::Pool,
5116 ) -> ::core::result::Result<
5117 ::std::vec::Vec<Self>,
5118 #root::sql::ExecError,
5119 > {
5120 use #root::sql::FetcherPool as _;
5121 #root::query::QuerySet::<Self>::default()
5122 .order_random()
5123 .limit(n)
5124 .fetch(pool)
5125 .await
5126 }
5127
5128 /// Fetch one row in random order. Eloquent
5129 /// `Model::inRandomOrder()->first()` parity. Same
5130 /// performance caveat as [`Self::random_n`].
5131 ///
5132 /// # Errors
5133 /// As [`FetcherPool::fetch`].
5134 ///
5135 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5136 pub async fn random(
5137 pool: &#root::sql::Pool,
5138 ) -> ::core::result::Result<
5139 ::core::option::Option<Self>,
5140 #root::sql::ExecError,
5141 > {
5142 ::core::result::Result::Ok(
5143 Self::random_n(1, pool).await?.into_iter().next(),
5144 )
5145 }
5146
5147 /// Fetch every row ordered ASC by `field`. Eloquent
5148 /// `Model::oldest($field)->get()` parity — the multi-row
5149 /// counterpart of [`Self::earliest`].
5150 ///
5151 /// # Errors
5152 /// As [`FetcherPool::fetch`].
5153 ///
5154 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5155 pub async fn oldest(
5156 field: &str,
5157 pool: &#root::sql::Pool,
5158 ) -> ::core::result::Result<
5159 ::std::vec::Vec<Self>,
5160 #root::sql::ExecError,
5161 > {
5162 use #root::sql::FetcherPool as _;
5163 #root::query::QuerySet::<Self>::default()
5164 .order_by(&[(field, false)])
5165 .fetch(pool)
5166 .await
5167 }
5168
5169 /// Fetch every row ordered DESC by `field`. Eloquent
5170 /// `Model::latest($field)->get()` parity — the multi-row
5171 /// counterpart of [`Self::latest`].
5172 ///
5173 /// # Errors
5174 /// As [`FetcherPool::fetch`].
5175 ///
5176 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5177 pub async fn newest(
5178 field: &str,
5179 pool: &#root::sql::Pool,
5180 ) -> ::core::result::Result<
5181 ::std::vec::Vec<Self>,
5182 #root::sql::ExecError,
5183 > {
5184 use #root::sql::FetcherPool as _;
5185 #root::query::QuerySet::<Self>::default()
5186 .order_by(&[(field, true)])
5187 .fetch(pool)
5188 .await
5189 }
5190
5191 /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5192 /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5193 /// Routes through the existing `__year` lookup suffix
5194 /// (issue #829).
5195 ///
5196 /// # Errors
5197 /// As [`FetcherPool::fetch`].
5198 ///
5199 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5200 pub async fn where_year(
5201 col: &str,
5202 year: i64,
5203 pool: &#root::sql::Pool,
5204 ) -> ::core::result::Result<
5205 ::std::vec::Vec<Self>,
5206 #root::sql::ExecError,
5207 > {
5208 use #root::sql::FetcherPool as _;
5209 let _key = ::std::format!("{}__year", col);
5210 #root::query::QuerySet::<Self>::default()
5211 .filter(&_key, #root::core::SqlValue::I64(year))
5212 .fetch(pool)
5213 .await
5214 }
5215
5216 /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5217 /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5218 /// `month` is 1–12.
5219 ///
5220 /// # Errors
5221 /// As [`FetcherPool::fetch`].
5222 ///
5223 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5224 pub async fn where_month(
5225 col: &str,
5226 month: i64,
5227 pool: &#root::sql::Pool,
5228 ) -> ::core::result::Result<
5229 ::std::vec::Vec<Self>,
5230 #root::sql::ExecError,
5231 > {
5232 use #root::sql::FetcherPool as _;
5233 let _key = ::std::format!("{}__month", col);
5234 #root::query::QuerySet::<Self>::default()
5235 .filter(&_key, #root::core::SqlValue::I64(month))
5236 .fetch(pool)
5237 .await
5238 }
5239
5240 /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5241 /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5242 /// `day` is 1–31.
5243 ///
5244 /// # Errors
5245 /// As [`FetcherPool::fetch`].
5246 ///
5247 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5248 pub async fn where_day(
5249 col: &str,
5250 day: i64,
5251 pool: &#root::sql::Pool,
5252 ) -> ::core::result::Result<
5253 ::std::vec::Vec<Self>,
5254 #root::sql::ExecError,
5255 > {
5256 use #root::sql::FetcherPool as _;
5257 let _key = ::std::format!("{}__day", col);
5258 #root::query::QuerySet::<Self>::default()
5259 .filter(&_key, #root::core::SqlValue::I64(day))
5260 .fetch(pool)
5261 .await
5262 }
5263
5264 /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5265 /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5266 /// `hour` is 0–23.
5267 ///
5268 /// # Errors
5269 /// As [`FetcherPool::fetch`].
5270 ///
5271 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5272 pub async fn where_hour(
5273 col: &str,
5274 hour: i64,
5275 pool: &#root::sql::Pool,
5276 ) -> ::core::result::Result<
5277 ::std::vec::Vec<Self>,
5278 #root::sql::ExecError,
5279 > {
5280 use #root::sql::FetcherPool as _;
5281 let _key = ::std::format!("{}__hour", col);
5282 #root::query::QuerySet::<Self>::default()
5283 .filter(&_key, #root::core::SqlValue::I64(hour))
5284 .fetch(pool)
5285 .await
5286 }
5287
5288 /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5289 /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5290 /// `minute` is 0–59.
5291 ///
5292 /// # Errors
5293 /// As [`FetcherPool::fetch`].
5294 ///
5295 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5296 pub async fn where_minute(
5297 col: &str,
5298 minute: i64,
5299 pool: &#root::sql::Pool,
5300 ) -> ::core::result::Result<
5301 ::std::vec::Vec<Self>,
5302 #root::sql::ExecError,
5303 > {
5304 use #root::sql::FetcherPool as _;
5305 let _key = ::std::format!("{}__minute", col);
5306 #root::query::QuerySet::<Self>::default()
5307 .filter(&_key, #root::core::SqlValue::I64(minute))
5308 .fetch(pool)
5309 .await
5310 }
5311
5312 /// Fetch every row where `<col> LIKE <pattern>` —
5313 /// caller-supplied pattern (must include `%` / `_`
5314 /// wildcards manually). Eloquent `Model::whereLike`
5315 /// parity.
5316 ///
5317 /// # Errors
5318 /// As [`FetcherPool::fetch`].
5319 ///
5320 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5321 pub async fn where_like(
5322 col: &str,
5323 pattern: impl ::core::convert::Into<::std::string::String>,
5324 pool: &#root::sql::Pool,
5325 ) -> ::core::result::Result<
5326 ::std::vec::Vec<Self>,
5327 #root::sql::ExecError,
5328 > {
5329 use #root::sql::FetcherPool as _;
5330 let _key = ::std::format!("{}__like", col);
5331 #root::query::QuerySet::<Self>::default()
5332 .filter(
5333 &_key,
5334 #root::core::SqlValue::String(pattern.into()),
5335 )
5336 .fetch(pool)
5337 .await
5338 }
5339
5340 /// Fetch every row where `<col> ILIKE <pattern>` —
5341 /// case-insensitive LIKE (PG native, MySQL/SQLite
5342 /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5343 ///
5344 /// # Errors
5345 /// As [`FetcherPool::fetch`].
5346 ///
5347 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5348 pub async fn where_ilike(
5349 col: &str,
5350 pattern: impl ::core::convert::Into<::std::string::String>,
5351 pool: &#root::sql::Pool,
5352 ) -> ::core::result::Result<
5353 ::std::vec::Vec<Self>,
5354 #root::sql::ExecError,
5355 > {
5356 use #root::sql::FetcherPool as _;
5357 let _key = ::std::format!("{}__ilike", col);
5358 #root::query::QuerySet::<Self>::default()
5359 .filter(
5360 &_key,
5361 #root::core::SqlValue::String(pattern.into()),
5362 )
5363 .fetch(pool)
5364 .await
5365 }
5366
5367 /// Fetch every row where `<col>` starts with `prefix`
5368 /// (auto-appends `%`). Eloquent
5369 /// `whereLike("col", "$prefix%")` parity.
5370 ///
5371 /// # Errors
5372 /// As [`FetcherPool::fetch`].
5373 ///
5374 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5375 pub async fn where_starts_with(
5376 col: &str,
5377 prefix: impl ::core::convert::Into<::std::string::String>,
5378 pool: &#root::sql::Pool,
5379 ) -> ::core::result::Result<
5380 ::std::vec::Vec<Self>,
5381 #root::sql::ExecError,
5382 > {
5383 use #root::sql::FetcherPool as _;
5384 let _key = ::std::format!("{}__startswith", col);
5385 #root::query::QuerySet::<Self>::default()
5386 .filter(
5387 &_key,
5388 #root::core::SqlValue::String(prefix.into()),
5389 )
5390 .fetch(pool)
5391 .await
5392 }
5393
5394 /// Fetch every row where `<col>` ends with `suffix`
5395 /// (auto-prepends `%`). Eloquent
5396 /// `whereLike("col", "%$suffix")` parity.
5397 ///
5398 /// # Errors
5399 /// As [`FetcherPool::fetch`].
5400 ///
5401 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5402 pub async fn where_ends_with(
5403 col: &str,
5404 suffix: impl ::core::convert::Into<::std::string::String>,
5405 pool: &#root::sql::Pool,
5406 ) -> ::core::result::Result<
5407 ::std::vec::Vec<Self>,
5408 #root::sql::ExecError,
5409 > {
5410 use #root::sql::FetcherPool as _;
5411 let _key = ::std::format!("{}__endswith", col);
5412 #root::query::QuerySet::<Self>::default()
5413 .filter(
5414 &_key,
5415 #root::core::SqlValue::String(suffix.into()),
5416 )
5417 .fetch(pool)
5418 .await
5419 }
5420
5421 /// Fetch every row where `<col>` contains `substr`
5422 /// (auto-wraps with `%`).
5423 /// Eloquent `whereLike("col", "%$substr%")` parity.
5424 ///
5425 /// # Errors
5426 /// As [`FetcherPool::fetch`].
5427 ///
5428 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5429 pub async fn where_contains(
5430 col: &str,
5431 substr: impl ::core::convert::Into<::std::string::String>,
5432 pool: &#root::sql::Pool,
5433 ) -> ::core::result::Result<
5434 ::std::vec::Vec<Self>,
5435 #root::sql::ExecError,
5436 > {
5437 use #root::sql::FetcherPool as _;
5438 let _key = ::std::format!("{}__contains", col);
5439 #root::query::QuerySet::<Self>::default()
5440 .filter(
5441 &_key,
5442 #root::core::SqlValue::String(substr.into()),
5443 )
5444 .fetch(pool)
5445 .await
5446 }
5447
5448 /// Fetch every row where `<col> > val`. Eloquent
5449 /// `Model::where($col, ">", $val)->get()` parity.
5450 ///
5451 /// # Errors
5452 /// As [`FetcherPool::fetch`].
5453 ///
5454 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5455 pub async fn where_gt(
5456 col: &str,
5457 val: impl ::core::convert::Into<#root::core::SqlValue>,
5458 pool: &#root::sql::Pool,
5459 ) -> ::core::result::Result<
5460 ::std::vec::Vec<Self>,
5461 #root::sql::ExecError,
5462 > {
5463 use #root::sql::FetcherPool as _;
5464 let _key = ::std::format!("{}__gt", col);
5465 #root::query::QuerySet::<Self>::default()
5466 .filter(&_key, val)
5467 .fetch(pool)
5468 .await
5469 }
5470
5471 /// Fetch every row where `<col> >= val`. Eloquent
5472 /// `Model::where($col, ">=", $val)->get()` parity.
5473 ///
5474 /// # Errors
5475 /// As [`FetcherPool::fetch`].
5476 ///
5477 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5478 pub async fn where_gte(
5479 col: &str,
5480 val: impl ::core::convert::Into<#root::core::SqlValue>,
5481 pool: &#root::sql::Pool,
5482 ) -> ::core::result::Result<
5483 ::std::vec::Vec<Self>,
5484 #root::sql::ExecError,
5485 > {
5486 use #root::sql::FetcherPool as _;
5487 let _key = ::std::format!("{}__gte", col);
5488 #root::query::QuerySet::<Self>::default()
5489 .filter(&_key, val)
5490 .fetch(pool)
5491 .await
5492 }
5493
5494 /// Fetch every row where `<col> < val`. Eloquent
5495 /// `Model::where($col, "<", $val)->get()` parity.
5496 ///
5497 /// # Errors
5498 /// As [`FetcherPool::fetch`].
5499 ///
5500 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5501 pub async fn where_lt(
5502 col: &str,
5503 val: impl ::core::convert::Into<#root::core::SqlValue>,
5504 pool: &#root::sql::Pool,
5505 ) -> ::core::result::Result<
5506 ::std::vec::Vec<Self>,
5507 #root::sql::ExecError,
5508 > {
5509 use #root::sql::FetcherPool as _;
5510 let _key = ::std::format!("{}__lt", col);
5511 #root::query::QuerySet::<Self>::default()
5512 .filter(&_key, val)
5513 .fetch(pool)
5514 .await
5515 }
5516
5517 /// Fetch every row where `<col> <= val`. Eloquent
5518 /// `Model::where($col, "<=", $val)->get()` parity.
5519 ///
5520 /// # Errors
5521 /// As [`FetcherPool::fetch`].
5522 ///
5523 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5524 pub async fn where_lte(
5525 col: &str,
5526 val: impl ::core::convert::Into<#root::core::SqlValue>,
5527 pool: &#root::sql::Pool,
5528 ) -> ::core::result::Result<
5529 ::std::vec::Vec<Self>,
5530 #root::sql::ExecError,
5531 > {
5532 use #root::sql::FetcherPool as _;
5533 let _key = ::std::format!("{}__lte", col);
5534 #root::query::QuerySet::<Self>::default()
5535 .filter(&_key, val)
5536 .fetch(pool)
5537 .await
5538 }
5539
5540 /// Fetch every row where `<col> <> val`. Eloquent
5541 /// `Model::where($col, "!=", $val)->get()` parity.
5542 ///
5543 /// # Errors
5544 /// As [`FetcherPool::fetch`].
5545 ///
5546 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5547 pub async fn where_ne(
5548 col: &str,
5549 val: impl ::core::convert::Into<#root::core::SqlValue>,
5550 pool: &#root::sql::Pool,
5551 ) -> ::core::result::Result<
5552 ::std::vec::Vec<Self>,
5553 #root::sql::ExecError,
5554 > {
5555 use #root::sql::FetcherPool as _;
5556 let _key = ::std::format!("{}__ne", col);
5557 #root::query::QuerySet::<Self>::default()
5558 .filter(&_key, val)
5559 .fetch(pool)
5560 .await
5561 }
5562
5563 /// Fetch every row matching `<col> = val` for ANY of the
5564 /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5565 /// parity. Empty `cols` returns no rows.
5566 ///
5567 /// Resolves each `&str` column to its SCHEMA-registered
5568 /// `&'static str` once and builds a single OR-composed
5569 /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5570 ///
5571 /// # Errors
5572 /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5573 /// when any column is not declared on the model.
5574 ///
5575 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5576 pub async fn where_any(
5577 cols: &[&str],
5578 val: impl ::core::convert::Into<#root::core::SqlValue>,
5579 pool: &#root::sql::Pool,
5580 ) -> ::core::result::Result<
5581 ::std::vec::Vec<Self>,
5582 #root::sql::ExecError,
5583 > {
5584 Self::__where_multi(cols, val, false, pool).await
5585 }
5586
5587 /// Fetch every row matching `<col> = val` for ALL listed
5588 /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5589 /// Empty `cols` returns every row (vacuous AND).
5590 ///
5591 /// # Errors
5592 /// As [`Self::where_any`].
5593 pub async fn where_all(
5594 cols: &[&str],
5595 val: impl ::core::convert::Into<#root::core::SqlValue>,
5596 pool: &#root::sql::Pool,
5597 ) -> ::core::result::Result<
5598 ::std::vec::Vec<Self>,
5599 #root::sql::ExecError,
5600 > {
5601 Self::__where_multi(cols, val, true, pool).await
5602 }
5603
5604 /// Internal: build a Q expression composing `cols` via
5605 /// AND (`all`) or OR (`!all`), then fetch. Backs
5606 /// `where_any` / `where_all`.
5607 #[doc(hidden)]
5608 pub async fn __where_multi(
5609 cols: &[&str],
5610 val: impl ::core::convert::Into<#root::core::SqlValue>,
5611 all: bool,
5612 pool: &#root::sql::Pool,
5613 ) -> ::core::result::Result<
5614 ::std::vec::Vec<Self>,
5615 #root::sql::ExecError,
5616 > {
5617 #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5618 .await
5619 }
5620
5621 /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5622 /// parity. PK-ordered
5623 /// is NOT guaranteed without an explicit `order_by` —
5624 /// drop into `Self::query()` for that.
5625 ///
5626 /// # Errors
5627 /// As [`FetcherPool::fetch`].
5628 ///
5629 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5630 pub async fn take(
5631 n: i64,
5632 pool: &#root::sql::Pool,
5633 ) -> ::core::result::Result<
5634 ::std::vec::Vec<Self>,
5635 #root::sql::ExecError,
5636 > {
5637 use #root::sql::FetcherPool as _;
5638 #root::query::QuerySet::<Self>::default()
5639 .limit(n)
5640 .fetch(pool)
5641 .await
5642 }
5643
5644 /// Fetch the page-th window of `per_page` rows
5645 /// (1-indexed). Eloquent
5646 /// `Model::query()->forPage($page, $perPage)->get()`
5647 /// parity. The DB scans an `OFFSET (page - 1) * per_page
5648 /// LIMIT per_page`; for large offsets this is O(N) —
5649 /// prefer keyset pagination via PK on hot paths.
5650 ///
5651 /// # Errors
5652 /// As [`FetcherPool::fetch`].
5653 ///
5654 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5655 pub async fn for_page(
5656 page: i64,
5657 per_page: i64,
5658 pool: &#root::sql::Pool,
5659 ) -> ::core::result::Result<
5660 ::std::vec::Vec<Self>,
5661 #root::sql::ExecError,
5662 > {
5663 use #root::sql::FetcherPool as _;
5664 let _offset = #root::list_params::page_offset(page, per_page);
5665 #root::query::QuerySet::<Self>::default()
5666 .limit(per_page)
5667 .offset(_offset)
5668 .fetch(pool)
5669 .await
5670 }
5671
5672 /// Eloquent `Model::paginate($per_page, $page)` — fetch
5673 /// one page of rows AND the total row count in one
5674 /// call. Returns `(rows, total)`. Two queries: a
5675 /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5676 /// for the total.
5677 ///
5678 /// Useful for paginated UIs that need both the visible
5679 /// rows AND a "Page X of Y" / total-count widget. For
5680 /// large tables prefer keyset pagination + cached count;
5681 /// every call to `paginate` re-counts the full table.
5682 ///
5683 /// Same 1-indexed `page` convention as [`Self::for_page`].
5684 ///
5685 /// # Errors
5686 /// As [`Self::for_page`] and [`Self::count`].
5687 pub async fn paginate(
5688 page: i64,
5689 per_page: i64,
5690 pool: &#root::sql::Pool,
5691 ) -> ::core::result::Result<
5692 (::std::vec::Vec<Self>, i64),
5693 #root::sql::ExecError,
5694 > {
5695 // Route through the queryset rather than `Self::count`:
5696 // the `count` inherent method is suppressed on models
5697 // that declare a field named `count` (field/shortcut
5698 // collision guard), and `paginate` must still compile
5699 // for those models.
5700 let total = {
5701 use #root::sql::CounterPool as _;
5702 #root::query::QuerySet::<Self>::default()
5703 .count(pool)
5704 .await?
5705 };
5706 let rows = Self::for_page(page, per_page, pool).await?;
5707 ::core::result::Result::Ok((rows, total))
5708 }
5709
5710 /// Bulk-update — set `set_col = set_val` on every row
5711 /// matching `where_col = where_val`. Returns affected row
5712 /// count. Eloquent
5713 /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5714 /// parity.
5715 ///
5716 /// For multi-column updates drop into the queryset
5717 /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5718 ///
5719 /// # Errors
5720 /// As [`UpdaterPool::execute_pool`].
5721 ///
5722 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5723 pub async fn update_where(
5724 where_col: &str,
5725 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5726 set_col: &str,
5727 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5728 pool: &#root::sql::Pool,
5729 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5730 let _query = #root::query::QuerySet::<Self>::default()
5731 .filter(where_col, where_val)
5732 .update()
5733 .set(set_col, set_val)
5734 .compile()?;
5735 #bulk_update_run
5736 }
5737
5738 /// Bulk-delete — remove every row matching
5739 /// `where_col = where_val`. Returns affected row count.
5740 /// Eloquent
5741 /// `Model::where($where_col, $where_val)->delete()` parity.
5742 ///
5743 /// # Errors
5744 /// As [`#root::sql::delete_pool`].
5745 pub async fn delete_where(
5746 where_col: &str,
5747 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5748 pool: &#root::sql::Pool,
5749 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5750 let _query = #root::query::QuerySet::<Self>::default()
5751 .filter_op(where_col, #root::core::Op::Eq, where_val)
5752 .compile_delete()?;
5753 #bulk_delete_run
5754 }
5755
5756 /// Bulk-update — set `set_col = set_val` on EVERY row of
5757 /// this model's table. **No WHERE clause** — use with
5758 /// care. Eloquent `Model::query()->update([$col => $val])`
5759 /// parity.
5760 ///
5761 /// Use for backfills, one-shot reset flows, etc. The
5762 /// returned count is rows affected.
5763 ///
5764 /// # Errors
5765 /// As [`UpdaterPool::execute_pool`].
5766 ///
5767 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5768 pub async fn update_all(
5769 set_col: &str,
5770 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5771 pool: &#root::sql::Pool,
5772 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5773 let _query = #root::query::QuerySet::<Self>::default()
5774 .update()
5775 .set(set_col, set_val)
5776 .compile()?;
5777 #bulk_update_run
5778 }
5779
5780 /// Fetch every row where `<col> NOT LIKE <pattern>`.
5781 /// Eloquent `Model::whereNotLike` parity. Pattern is
5782 /// passed verbatim — caller controls `%` / `_`.
5783 ///
5784 /// # Errors
5785 /// As [`FetcherPool::fetch`].
5786 ///
5787 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5788 pub async fn where_not_like(
5789 col: &str,
5790 pattern: impl ::core::convert::Into<::std::string::String>,
5791 pool: &#root::sql::Pool,
5792 ) -> ::core::result::Result<
5793 ::std::vec::Vec<Self>,
5794 #root::sql::ExecError,
5795 > {
5796 use #root::sql::FetcherPool as _;
5797 let _key = ::std::format!("{}__not_like", col);
5798 #root::query::QuerySet::<Self>::default()
5799 .filter(
5800 &_key,
5801 #root::core::SqlValue::String(pattern.into()),
5802 )
5803 .fetch(pool)
5804 .await
5805 }
5806
5807 /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5808 /// case-insensitive `NOT LIKE` (PG native, MySQL /
5809 /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5810 ///
5811 /// # Errors
5812 /// As [`FetcherPool::fetch`].
5813 ///
5814 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5815 pub async fn where_not_ilike(
5816 col: &str,
5817 pattern: impl ::core::convert::Into<::std::string::String>,
5818 pool: &#root::sql::Pool,
5819 ) -> ::core::result::Result<
5820 ::std::vec::Vec<Self>,
5821 #root::sql::ExecError,
5822 > {
5823 use #root::sql::FetcherPool as _;
5824 let _key = ::std::format!("{}__not_ilike", col);
5825 #root::query::QuerySet::<Self>::default()
5826 .filter(
5827 &_key,
5828 #root::core::SqlValue::String(pattern.into()),
5829 )
5830 .fetch(pool)
5831 .await
5832 }
5833
5834 /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5835 /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5836 /// parity.
5837 ///
5838 /// # Errors
5839 /// As [`FetcherPool::fetch`].
5840 ///
5841 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5842 pub async fn where_not_between(
5843 col: &str,
5844 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5845 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5846 pool: &#root::sql::Pool,
5847 ) -> ::core::result::Result<
5848 ::std::vec::Vec<Self>,
5849 #root::sql::ExecError,
5850 > {
5851 use #root::sql::FetcherPool as _;
5852 let _key = ::std::format!("{}__not_between", col);
5853 let _vals = #root::core::SqlValue::List(::std::vec![
5854 ::core::convert::Into::into(lo),
5855 ::core::convert::Into::into(hi),
5856 ]);
5857 #root::query::QuerySet::<Self>::default()
5858 .filter(&_key, _vals)
5859 .fetch(pool)
5860 .await
5861 }
5862
5863 /// Returns the SQL table name for this model. Eloquent
5864 /// `$model->getTable()` parity.
5865 #[must_use]
5866 pub fn table_name() -> &'static str {
5867 <Self as #root::core::Model>::SCHEMA.table
5868 }
5869
5870 /// Returns the SQL column name of this model's primary
5871 /// key, or `None` when the model has no
5872 /// `#[rustango(primary_key)]`. Eloquent
5873 /// `$model->getKeyName()` parity.
5874 #[must_use]
5875 pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5876 <Self as #root::core::Model>::SCHEMA
5877 .primary_key()
5878 .map(|f| f.column)
5879 }
5880
5881 /// Fetch every row where `<col> BETWEEN lo AND hi`
5882 /// (inclusive on both ends — same as SQL). Eloquent
5883 /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5884 ///
5885 /// # Errors
5886 /// As [`FetcherPool::fetch`].
5887 ///
5888 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5889 pub async fn where_between(
5890 col: &str,
5891 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5892 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5893 pool: &#root::sql::Pool,
5894 ) -> ::core::result::Result<
5895 ::std::vec::Vec<Self>,
5896 #root::sql::ExecError,
5897 > {
5898 use #root::sql::FetcherPool as _;
5899 let _key = ::std::format!("{}__between", col);
5900 let _vals = #root::core::SqlValue::List(::std::vec![
5901 ::core::convert::Into::into(lo),
5902 ::core::convert::Into::into(hi),
5903 ]);
5904 #root::query::QuerySet::<Self>::default()
5905 .filter(&_key, _vals)
5906 .fetch(pool)
5907 .await
5908 }
5909
5910 /// Fetch the first row where `<col> = <val>`. Returns
5911 /// `Ok(None)` when no row matches. Eloquent
5912 /// `Model::firstWhere($col, $val)` parity.
5913 ///
5914 /// Thin wrapper over `QuerySet::<Self>::default()
5915 /// .filter(col, val).first(pool)`. Use this when you
5916 /// want one row identified by a non-PK column (e.g.
5917 /// `User::first_where_pool("email", "x@y.com", &pool)`).
5918 ///
5919 /// `val` accepts any value `Into<SqlValue>` so plain
5920 /// strings, ints, UUIDs, etc. all work.
5921 ///
5922 /// # Errors
5923 /// As `QuerySet::first`.
5924 pub async fn first_where(
5925 col: &str,
5926 val: impl ::core::convert::Into<#root::core::SqlValue>,
5927 pool: &#root::sql::Pool,
5928 ) -> ::core::result::Result<
5929 ::core::option::Option<Self>,
5930 #root::sql::ExecError,
5931 > {
5932 #root::query::QuerySet::<Self>::default()
5933 .filter(col, val)
5934 .first(pool)
5935 .await
5936 }
5937
5938 #value_method
5939
5940 /// Fetch the row with the largest `field` value —
5941 /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
5942 /// `Ok(None)` for an empty table. Eloquent
5943 /// `Model::latest($field)->first()` parity.
5944 /// Thin wrapper over `QuerySet::<Self>::default()
5945 /// .latest(field, pool)`.
5946 ///
5947 /// **Field name** is the Rust field ident as a string
5948 /// (not the SQL column). It is a runtime `&str`, so an
5949 /// unknown one compiles and surfaces as
5950 /// `ExecError::Query(QueryError::UnknownField)` when the
5951 /// query runs.
5952 ///
5953 /// # Errors
5954 /// As `QuerySet::latest`.
5955 pub async fn latest(
5956 field: &str,
5957 pool: &#root::sql::Pool,
5958 ) -> ::core::result::Result<
5959 ::core::option::Option<Self>,
5960 #root::sql::ExecError,
5961 > {
5962 #root::query::QuerySet::<Self>::default()
5963 .latest(field, pool)
5964 .await
5965 }
5966
5967 /// Sibling of [`Self::latest`] — fetches the row
5968 /// with the smallest `field` value (`ORDER BY <field>
5969 /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
5970 /// ->first()` parity.
5971 ///
5972 /// # Errors
5973 /// As [`Self::latest`].
5974 pub async fn earliest(
5975 field: &str,
5976 pool: &#root::sql::Pool,
5977 ) -> ::core::result::Result<
5978 ::core::option::Option<Self>,
5979 #root::sql::ExecError,
5980 > {
5981 #root::query::QuerySet::<Self>::default()
5982 .earliest(field, pool)
5983 .await
5984 }
5985
5986 #count_method
5987
5988 /// `true` when the table contains at least one row.
5989 /// Eloquent `Model::query()->exists()` parity. Thin wrapper
5990 /// over `QuerySet::<Self>::default().exists(pool)`.
5991 ///
5992 /// # Errors
5993 /// As [`ExistsPool::exists`].
5994 ///
5995 /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
5996 pub async fn exists(
5997 pool: &#root::sql::Pool,
5998 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5999 use #root::sql::ExistsPool as _;
6000 #root::query::QuerySet::<Self>::default()
6001 .exists(pool)
6002 .await
6003 }
6004
6005 /// Inverse of [`Self::exists`] — returns `true` when
6006 /// the table has zero rows. Eloquent
6007 /// `Model::doesntExist()` parity.
6008 ///
6009 /// # Errors
6010 /// As [`Self::exists`].
6011 pub async fn doesnt_exist(
6012 pool: &#root::sql::Pool,
6013 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6014 Self::exists(pool).await.map(|e| !e)
6015 }
6016
6017 /// Eloquent `Model::query()->whereKey($pk)->exists()` —
6018 /// `true` when a row with primary key `pk` exists in
6019 /// the table. Sugar over
6020 /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
6021 ///
6022 /// Differs from [`Self::exists`] (which checks "any row
6023 /// in the table") by checking a specific PK existence.
6024 /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
6025 /// because the row is never materialized — the SQL is
6026 /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
6027 ///
6028 /// # Errors
6029 /// As [`#root::sql::ExistsPool::contains_pk`].
6030 pub async fn contains_pk(
6031 pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
6032 pool: &#root::sql::Pool,
6033 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6034 use #root::sql::ExistsPool as _;
6035 #root::query::QuerySet::<Self>::default()
6036 .contains_pk(pool, pk)
6037 .await
6038 }
6039
6040 #sum_method
6041 #avg_method
6042 #min_method
6043 #max_method
6044
6045 /// Fetch every row of this model from `pool`. Eloquent
6046 /// `Model::all()` parity — a thin wrapper over
6047 /// `QuerySet::<Self>::default().fetch(pool)`.
6048 ///
6049 /// **Use with care on large tables** — there's no
6050 /// pagination or limit; the entire table is materialized
6051 /// into memory. For anything beyond fixture / lookup
6052 /// tables, page through `QuerySet::<Self>::default()
6053 /// .order_by(...).limit(N).offset(M).fetch(pool)`
6054 /// or stream via `.iterator(chunk_size)`.
6055 ///
6056 /// # Errors
6057 /// As [`FetcherPool::fetch`].
6058 ///
6059 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6060 pub async fn all(
6061 pool: &#root::sql::Pool,
6062 ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
6063 {
6064 use #root::sql::FetcherPool as _;
6065 #root::query::QuerySet::<Self>::default()
6066 .fetch(pool)
6067 .await
6068 }
6069
6070 /// Look up every row whose primary key is in `pks`.
6071 /// Returns the matching rows in **inventory** order — NOT
6072 /// the order of `pks`. Empty `pks` returns an empty
6073 /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
6074 /// with a list) parity.
6075 ///
6076 /// Thin wrapper over `QuerySet::<Self>::default()
6077 /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
6078 /// Caller-supplied PKs that don't match a row are
6079 /// silently skipped (the returned vec is shorter than
6080 /// the input list). For an order-preserving / "fail
6081 /// when any missing" variant, build the queryset
6082 /// explicitly with `in_bulk` instead.
6083 ///
6084 /// Accepts any iterable whose elements are
6085 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
6086 /// `Vec<Uuid>`, etc.
6087 ///
6088 /// # Errors
6089 /// As [`FetcherPool::fetch`].
6090 ///
6091 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6092 pub async fn find_many<V>(
6093 pks: impl ::core::iter::IntoIterator<Item = V>,
6094 pool: &#root::sql::Pool,
6095 ) -> ::core::result::Result<
6096 ::std::vec::Vec<Self>,
6097 #root::sql::ExecError,
6098 >
6099 where
6100 V: ::core::convert::Into<#root::core::SqlValue>,
6101 {
6102 use #root::sql::FetcherPool as _;
6103 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6104 pks.into_iter().map(::core::convert::Into::into).collect();
6105 if _values.is_empty() {
6106 return ::core::result::Result::Ok(::std::vec::Vec::new());
6107 }
6108 let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
6109 #root::query::QuerySet::<Self>::default()
6110 .filter(&_key, #root::core::SqlValue::List(_values))
6111 .fetch(pool)
6112 .await
6113 }
6114
6115 /// Look up the row whose primary key equals `pk`. Returns
6116 /// `Ok(None)` when no row matches, rather than erroring.
6117 /// Eloquent `Model::find`
6118 /// shape — accepts any value `Into<SqlValue>`.
6119 ///
6120 /// One-liner shortcut for the common
6121 /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6122 /// .limit(1).fetch(pool).await?.into_iter().next()`
6123 /// dance.
6124 ///
6125 /// # Errors
6126 /// As [`FetcherPool::fetch`].
6127 ///
6128 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6129 pub async fn find(
6130 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6131 pool: &#root::sql::Pool,
6132 ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6133 {
6134 use #root::sql::FetcherPool as _;
6135 let _pk_val: #root::core::SqlValue = pk.into();
6136 let mut _rows: ::std::vec::Vec<Self> =
6137 #root::query::QuerySet::<Self>::default()
6138 .filter(::core::stringify!(#pk_ident), _pk_val)
6139 .limit(1)
6140 .fetch(pool)
6141 .await?;
6142 ::core::result::Result::Ok(_rows.into_iter().next())
6143 }
6144
6145 /// Look up the row whose primary key equals `pk`. Errors
6146 /// when no row matches — the throwing counterpart of
6147 /// [`Self::find`]. Eloquent `Model::findOrFail`
6148 /// parity.
6149 ///
6150 /// Translates the miss into
6151 /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6152 /// so callers can `?`-bubble straight through the typical
6153 /// `ExecError` error chain.
6154 ///
6155 /// # Errors
6156 /// As [`Self::find`]; additionally
6157 /// [`sqlx::Error::RowNotFound`] when no row matches.
6158 ///
6159 /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6160 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6161 pub async fn find_or_fail(
6162 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6163 pool: &#root::sql::Pool,
6164 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6165 match Self::find(pk, pool).await? {
6166 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6167 ::core::option::Option::None => ::core::result::Result::Err(
6168 #root::sql::ExecError::Driver(
6169 #root::sql::sqlx::Error::RowNotFound,
6170 ),
6171 ),
6172 }
6173 }
6174
6175 /// Look up multiple rows by primary key, **failing** if
6176 /// any requested PK is missing. Eloquent
6177 /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6178 /// inventory order (NOT request order — same as
6179 /// [`Self::find_many`]).
6180 ///
6181 /// Empty `pks` returns an empty `Vec` (no rows requested
6182 /// → nothing to fail on).
6183 ///
6184 /// Differs from [`Self::find_many`] in that this method
6185 /// requires every PK to resolve. If even one is missing,
6186 /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6187 /// in `ExecError::Driver`). Rows are deduped at the SQL
6188 /// layer, so passing the same PK twice in `pks` counts as
6189 /// one expected row.
6190 ///
6191 /// # Errors
6192 /// As [`Self::find_many`], plus `RowNotFound` when the
6193 /// returned row count is less than the count of distinct
6194 /// requested PKs.
6195 pub async fn find_many_or_fail<V>(
6196 pks: impl ::core::iter::IntoIterator<Item = V>,
6197 pool: &#root::sql::Pool,
6198 ) -> ::core::result::Result<
6199 ::std::vec::Vec<Self>,
6200 #root::sql::ExecError,
6201 >
6202 where
6203 V: ::core::convert::Into<#root::core::SqlValue>,
6204 {
6205 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6206 pks.into_iter().map(::core::convert::Into::into).collect();
6207 if _values.is_empty() {
6208 return ::core::result::Result::Ok(::std::vec::Vec::new());
6209 }
6210 // Dedup the requested PK list before counting so
6211 // duplicate-PK requests don't false-fail (the SQL
6212 // `IN (...)` clause naturally dedups too).
6213 let mut _seen: ::std::collections::HashSet<
6214 ::std::string::String,
6215 > = ::std::collections::HashSet::new();
6216 for v in &_values {
6217 _seen.insert(v.to_display_string());
6218 }
6219 let _expected = _seen.len();
6220 let _rows = Self::find_many(_values, pool).await?;
6221 if _rows.len() < _expected {
6222 return ::core::result::Result::Err(
6223 #root::sql::ExecError::Driver(
6224 #root::sql::sqlx::Error::RowNotFound,
6225 ),
6226 );
6227 }
6228 ::core::result::Result::Ok(_rows)
6229 }
6230
6231 /// Find by primary key or run `fallback` to produce a
6232 /// default row to return. Eloquent
6233 /// `Model::findOr($pk, fn() => …)` parity.
6234 ///
6235 /// Unlike [`Self::find_or_fail`] (which raises on
6236 /// miss), this is the "give me something sensible"
6237 /// branch: typical use is "fetch the user's row, else
6238 /// fall back to an anonymous/guest stub".
6239 ///
6240 /// `fallback` runs only when no row matches — the DB
6241 /// round-trip happens unconditionally.
6242 ///
6243 /// # Errors
6244 /// As [`Self::find`].
6245 pub async fn find_or<F>(
6246 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6247 pool: &#root::sql::Pool,
6248 fallback: F,
6249 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6250 where
6251 F: ::core::ops::FnOnce() -> Self,
6252 {
6253 ::core::result::Result::Ok(
6254 Self::find(pk, pool).await?.unwrap_or_else(fallback),
6255 )
6256 }
6257
6258 /// Same as [`Self::find_or`] but also returns a `bool`
6259 /// indicating whether the row was found in the DB
6260 /// (`true`) or freshly built from `fallback` (`false`).
6261 /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6262 /// in PHP the returned model also exposes `->exists`;
6263 /// here that's surfaced as the second tuple element.
6264 ///
6265 /// Useful for edit-or-create form handlers where the
6266 /// caller needs to know whether to PATCH or POST when
6267 /// the user submits the form.
6268 ///
6269 /// # Errors
6270 /// As [`Self::find`].
6271 pub async fn find_or_new<F>(
6272 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6273 pool: &#root::sql::Pool,
6274 fallback: F,
6275 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6276 where
6277 F: ::core::ops::FnOnce() -> Self,
6278 {
6279 match Self::find(pk, pool).await? {
6280 ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6281 ::core::option::Option::None => {
6282 ::core::result::Result::Ok((fallback(), false))
6283 }
6284 }
6285 }
6286
6287 /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6288 /// [`Self::find_or_new`] but **persists** the new row
6289 /// when the PK isn't found. Returns `(row, exists: bool)`
6290 /// — `exists=true` when the PK was found,
6291 /// `false` when a fresh row was inserted.
6292 ///
6293 /// ```ignore
6294 /// let (post, found) = Post::find_or_insert(
6295 /// pk,
6296 /// &pool,
6297 /// || Post { id: Auto::default(), title: "new".into() },
6298 /// ).await?;
6299 /// // `found` true → returned existing row;
6300 /// // `found` false → fallback was inserted, `post.id` populated.
6301 /// ```
6302 ///
6303 /// **Caveat**: two concurrent calls that both miss the
6304 /// find can both INSERT, violating uniqueness. Wrap in a
6305 /// transaction or rely on a UNIQUE constraint + handle
6306 /// the conflict error if you need race-free semantics.
6307 ///
6308 /// # Errors
6309 /// As [`Self::find`] and [`Self::save_pool`].
6310 pub async fn find_or_insert<F>(
6311 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6312 pool: &#root::sql::Pool,
6313 fallback: F,
6314 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6315 where
6316 F: ::core::ops::FnOnce() -> Self,
6317 {
6318 if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6319 return ::core::result::Result::Ok((_row, true));
6320 }
6321 let mut _new = fallback();
6322 _new.save_pool(pool).await?;
6323 ::core::result::Result::Ok((_new, false))
6324 }
6325
6326 /// Fetch the first row of the table, or run `fallback`
6327 /// when the table is empty. Eloquent
6328 /// `Model::firstOr(fn() => …)` parity.
6329 ///
6330 /// # Errors
6331 /// As [`Self::first`].
6332 pub async fn first_or<F>(
6333 pool: &#root::sql::Pool,
6334 fallback: F,
6335 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6336 where
6337 F: ::core::ops::FnOnce() -> Self,
6338 {
6339 // Route through the queryset rather than `Self::first`,
6340 // which is suppressed on models with a field named
6341 // `first` (field/shortcut collision guard).
6342 ::core::result::Result::Ok(
6343 #root::query::QuerySet::<Self>::default()
6344 .first(pool)
6345 .await?
6346 .unwrap_or_else(fallback),
6347 )
6348 }
6349
6350 /// Fetch exactly one row matching `<col> = <val>`. Errors
6351 /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6352 /// or more than one matches
6353 /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6354 /// Eloquent `Model::sole($col, $val)` parity.
6355 ///
6356 /// # Errors
6357 /// As [`Self::where_`] plus the explicit
6358 /// `RowNotFound` / `MultipleRowsReturned` cases above.
6359 pub async fn sole(
6360 col: &str,
6361 val: impl ::core::convert::Into<#root::core::SqlValue>,
6362 pool: &#root::sql::Pool,
6363 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6364 let mut _rows = Self::where_(col, val, pool).await?;
6365 match _rows.len() {
6366 0 => ::core::result::Result::Err(
6367 #root::sql::ExecError::Driver(
6368 #root::sql::sqlx::Error::RowNotFound,
6369 ),
6370 ),
6371 1 => ::core::result::Result::Ok(_rows.remove(0)),
6372 n => ::core::result::Result::Err(
6373 #root::sql::ExecError::MultipleRowsReturned {
6374 op: "sole",
6375 table: <Self as #root::core::Model>::SCHEMA.name,
6376 count: n,
6377 },
6378 ),
6379 }
6380 }
6381 }
6382 } else {
6383 quote!()
6384 };
6385
6386 // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6387 // the non-audited `_pool` methods but execute against an open
6388 // `PoolTx` so the writes participate in the caller's transaction.
6389 // Auditing inside TX is deferred; these always use the plain
6390 // executor primitives regardless of whether the model is audited.
6391 let tx_insert_method = if fields.has_auto {
6392 let pushes = &fields.insert_pushes;
6393 let returning_cols = &fields.returning_cols;
6394 quote! {
6395 /// Insert this row inside an open transaction, populating
6396 /// any `Auto<T>` PK from the auto-assigned value. Works
6397 /// against any backend that `tx` wraps.
6398 ///
6399 /// # Errors
6400 /// As [`Self::insert_pool`].
6401 pub async fn insert_tx(
6402 &mut self,
6403 tx: &mut #root::sql::PoolTx<'_>,
6404 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6405 let mut _columns: ::std::vec::Vec<&'static str> =
6406 ::std::vec::Vec::new();
6407 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6408 ::std::vec::Vec::new();
6409 #( #pushes )*
6410 let _query = #root::core::InsertQuery::new(
6411 <Self as #root::core::Model>::SCHEMA,
6412 _columns,
6413 _values,
6414 )
6415 .returning(::std::vec![ #( #returning_cols ),* ]);
6416 let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6417 #root::sql::apply_auto_pk(_result, self)
6418 }
6419 }
6420 } else {
6421 let insert_columns = &fields.insert_columns;
6422 let insert_values = &fields.insert_values;
6423 quote! {
6424 /// Insert this row inside an open transaction.
6425 ///
6426 /// # Errors
6427 /// As [`Self::insert_pool`].
6428 pub async fn insert_tx(
6429 &self,
6430 tx: &mut #root::sql::PoolTx<'_>,
6431 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6432 let _query = #root::core::InsertQuery::new(
6433 <Self as #root::core::Model>::SCHEMA,
6434 ::std::vec![ #( #insert_columns ),* ],
6435 ::std::vec![ #( #insert_values ),* ],
6436 );
6437 #root::sql::insert_tx(tx, &_query).await
6438 }
6439 }
6440 };
6441
6442 let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6443 let pk_column_lit = pk_col.as_str();
6444 let assignments = &fields.update_assignments;
6445 let dispatch_unset = if fields.pk_is_auto {
6446 quote! {
6447 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6448 return self.insert_tx(tx).await.map(|()| 1u64);
6449 }
6450 }
6451 } else {
6452 quote!()
6453 };
6454 quote! {
6455 /// Save this row inside an open transaction. `INSERT` when
6456 /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6457 /// PK. Works against any backend that `tx` wraps.
6458 ///
6459 /// # Errors
6460 /// As [`Self::save_pool`].
6461 pub async fn save_tx(
6462 &mut self,
6463 tx: &mut #root::sql::PoolTx<'_>,
6464 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6465 #dispatch_unset
6466 let _query = #root::core::UpdateQuery::new(
6467 <Self as #root::core::Model>::SCHEMA,
6468 ::std::vec![ #( #assignments ),* ],
6469 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6470 #pk_column_lit,
6471 #root::core::Op::Eq,
6472 ::core::convert::Into::<#root::core::SqlValue>::into(
6473 ::core::clone::Clone::clone(&self.#pk_ident)
6474 ),
6475 )),
6476 );
6477 let _affected = #root::sql::update_tx(tx, &_query).await?;
6478 ::core::result::Result::Ok(_affected)
6479 }
6480 }
6481 } else {
6482 quote!()
6483 };
6484
6485 let tx_delete_method = {
6486 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6487 let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6488 if let Some(pk_ident) = pk_ident_for_tx {
6489 quote! {
6490 /// Delete the row identified by this instance's PK
6491 /// inside an open transaction. Works against any backend
6492 /// that `tx` wraps.
6493 ///
6494 /// # Errors
6495 /// As [`Self::delete_pool`].
6496 pub async fn delete_tx(
6497 &self,
6498 tx: &mut #root::sql::PoolTx<'_>,
6499 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6500 let _query = #root::core::DeleteQuery::by_pk(
6501 <Self as #root::core::Model>::SCHEMA,
6502 #pk_column_lit,
6503 ::core::convert::Into::<#root::core::SqlValue>::into(
6504 ::core::clone::Clone::clone(&self.#pk_ident)
6505 ),
6506 );
6507 #root::sql::delete_tx(tx, &_query).await
6508 }
6509 }
6510 } else {
6511 quote!()
6512 }
6513 };
6514
6515 // Update emission captures both BEFORE and AFTER state — runs an
6516 // extra SELECT against `_executor` BEFORE the UPDATE, captures
6517 // each tracked field's prior value, then after the UPDATE diffs
6518 // against the in-memory `&self`. `diff_changes` drops unchanged
6519 // columns so the JSON only contains the actual delta.
6520 //
6521 // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6522 // and binds `_audit_before_pairs`; `audit_update_post` runs
6523 // after the UPDATE and emits the PendingEntry.
6524 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6525 audited_fields
6526 {
6527 if tracked.is_empty() {
6528 (quote!(), quote!())
6529 } else {
6530 let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
6531 let pk_column_lit_for_select =
6532 primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6533 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6534 if fields.pk_is_auto {
6535 quote!(self.#pk_ident.get().copied().unwrap_or_default())
6536 } else {
6537 quote!(::core::clone::Clone::clone(&self.#pk_ident))
6538 }
6539 } else {
6540 quote!(0_i64)
6541 };
6542 let before_pairs = tracked.iter().map(|c| {
6543 let column_lit = c.column.as_str();
6544 let value_ty = &c.value_ty;
6545 quote! {
6546 (
6547 #column_lit,
6548 match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6549 &_audit_before_row, #column_lit,
6550 ) {
6551 ::core::result::Result::Ok(v) => {
6552 #root::__serde_json::to_value(&v)
6553 .unwrap_or(#root::__serde_json::Value::Null)
6554 }
6555 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6556 },
6557 )
6558 }
6559 });
6560 let pre = quote! {
6561 let _audit_before_query = #root::audit::before_image_query(
6562 <Self as #root::core::Model>::SCHEMA,
6563 #pk_column_lit_for_select,
6564 ::core::convert::Into::<#root::core::SqlValue>::into(#pk_value_for_bind),
6565 &[ #( #tracked_cols ),* ],
6566 );
6567 // A failed pre-read must not let the UPDATE run unaudited (#1907).
6568 let _audit_before_pairs:
6569 ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6570 #root::sql::select_rows_on(&mut *_executor, &_audit_before_query)
6571 .await?
6572 .into_iter()
6573 .next()
6574 .map(|_audit_before_row| ::std::vec![ #( #before_pairs ),* ]);
6575 };
6576 let post = quote! {
6577 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6578 let _audit_after = self.__rustango_audit_pairs();
6579 if let ::core::option::Option::Some(_audit_entry) =
6580 #root::audit::PendingEntry::update_diff(
6581 <Self as #root::core::Model>::SCHEMA.table,
6582 self.__rustango_audit_pk(),
6583 &_audit_before,
6584 &_audit_after,
6585 )
6586 {
6587 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6588 }
6589 }
6590 };
6591 (pre, post)
6592 }
6593 } else {
6594 (quote!(), quote!())
6595 };
6596
6597 // Bulk-insert audit: one `Create` entry per row once RETURNING has
6598 // set its PK, written in one batched INSERT.
6599 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6600 quote! {
6601 let _audit_entries: ::std::vec::Vec<#root::audit::PendingEntry> = rows
6602 .iter()
6603 .map(|_row| _row.__rustango_audit_entry(#root::audit::AuditOp::Create))
6604 .collect();
6605 #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6606 }
6607 } else {
6608 quote!()
6609 };
6610
6611 let save_method = if fields.pk_is_auto {
6612 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6613 let pk_column_lit = pk_column.as_str();
6614 let assignments = &fields.update_assignments;
6615 let upsert_cols = &fields.upsert_update_columns;
6616 let upsert_pushes = &fields.insert_pushes;
6617 let upsert_returning = &fields.returning_cols;
6618 let upsert_auto_assigns = &fields.auto_assigns;
6619 // Conflict target: prefer the first declared `unique_together`
6620 // when it exists. Plain `Auto<T>` PKs are server-assigned via
6621 // `BIGSERIAL` and never collide on insert, so a PK-only target
6622 // would silently turn `upsert()` into "always-insert" for
6623 // surrogate-PK models with composite UNIQUE constraints — see
6624 // `RolePermission` / `UserRole` / `UserPermission` in the
6625 // tenancy permission engine. When no `unique_together` is
6626 // declared we keep the PK target (the original behaviour).
6627 let upsert_target_columns: Vec<String> =
6628 upsert_unique.map_or_else(|| vec![pk_column.clone()], <[String]>::to_vec);
6629 let upsert_target_lits = upsert_target_columns
6630 .iter()
6631 .map(String::as_str)
6632 .collect::<Vec<_>>();
6633 let conflict_clause = if fields.upsert_update_columns.is_empty() {
6634 quote!(#root::core::ConflictClause::DoNothing)
6635 } else {
6636 quote!(#root::core::ConflictClause::DoUpdate {
6637 target: ::std::vec![ #( #upsert_target_lits ),* ],
6638 update_columns: ::std::vec![ #( #upsert_cols ),* ],
6639 })
6640 };
6641 // Audited: the upsert reports whether it inserted or updated (#1795).
6642 let (upsert_run, audit_upsert_emit) = if audited_fields.is_some() {
6643 (
6644 quote! {
6645 let (_returning_row_v, _audit_op) =
6646 #root::audit::upsert_returning_on(&mut *_executor, &query).await?;
6647 },
6648 quote! {
6649 let _audit_entry = self.__rustango_audit_entry(_audit_op);
6650 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6651 },
6652 )
6653 } else {
6654 (
6655 quote! {
6656 let _returning_row_v =
6657 #root::sql::__macro_internals::insert_returning_on(_executor, &query)
6658 .await?;
6659 },
6660 quote!(),
6661 )
6662 };
6663 Some(quote! {
6664 /// Insert this row if its `Auto<T>` primary key is
6665 /// `Unset`, otherwise update the existing row matching the
6666 /// PK. The caller doesn't need to pick `insert` vs the
6667 /// bulk-update path manually.
6668 ///
6669 /// On the insert branch, populates the PK from `RETURNING`
6670 /// (same behavior as `insert`). On the update branch,
6671 /// writes every non-PK column back; if no row matches the
6672 /// PK, returns `Ok(())` silently.
6673 ///
6674 /// Only generated when the primary key is declared as
6675 /// `Auto<T>`. Models with a manually-managed PK must use
6676 /// `insert` or the QuerySet update builder.
6677 ///
6678 /// **Postgres only — prefer [`Self::save_pool`].** This takes a
6679 /// driver-specific `PgPool`, so it does not exist at all on a
6680 /// build without the `postgres` feature: selecting `sqlite`
6681 /// makes the method vanish rather than fail with anything that
6682 /// names the cause.
6683 ///
6684 /// [`Self::save_pool`] takes `rustango::sql::Pool` and works on
6685 /// all three backends. The naming is inverted — the short name
6686 /// is the narrow one — and that is tracked in #1293.
6687 ///
6688 /// # Errors
6689 /// Returns [`#root::sql::ExecError`] for SQL-writing
6690 /// or driver failures.
6691 #[cfg(feature = "postgres")]
6692 pub async fn save(
6693 &mut self,
6694 pool: &#root::sql::sqlx::PgPool,
6695 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6696 #pool_to_save_on
6697 }
6698
6699 /// Like [`Self::save`] but accepts any sqlx executor —
6700 /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6701 /// escape hatch for tenant-scoped writes: schema-mode
6702 /// tenants share the registry pool but rely on a per-
6703 /// checkout `SET search_path`, so passing `&PgPool` would
6704 /// silently hit the wrong schema. Acquire a connection
6705 /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6706 ///
6707 /// # Errors
6708 /// As [`Self::save`].
6709 #[cfg(feature = "postgres")]
6710 pub async fn save_on #executor_generics (
6711 &mut self,
6712 #executor_param,
6713 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6714 #executor_where
6715 {
6716 // #1029 — INSERT writes exactly one row → 1; UPDATE returns
6717 // the rows-affected count (0 when the PK no longer exists).
6718 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6719 return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
6720 }
6721 #audit_update_pre
6722 let _query = #root::core::UpdateQuery::new(
6723 <Self as #root::core::Model>::SCHEMA,
6724 ::std::vec![ #( #assignments ),* ],
6725 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6726 #pk_column_lit,
6727 #root::core::Op::Eq,
6728 ::core::convert::Into::<#root::core::SqlValue>::into(
6729 ::core::clone::Clone::clone(&self.#pk_ident)
6730 ),
6731 )),
6732 );
6733 let _affected = #root::sql::__macro_internals::update_on(
6734 #executor_passes_to_data_write,
6735 &_query,
6736 ).await?;
6737 #audit_update_post
6738 ::core::result::Result::Ok(_affected)
6739 }
6740
6741 /// Per-call override for the audit source. Runs
6742 /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6743 /// scope so the resulting audit entry records `source`
6744 /// instead of the task-local default. Useful for seed
6745 /// scripts and one-off CLI tools that don't sit inside an
6746 /// admin handler. The override applies only to this call;
6747 /// no global state changes.
6748 ///
6749 /// # Errors
6750 /// As [`Self::save_on`].
6751 #[cfg(feature = "postgres")]
6752 pub async fn save_on_with #executor_generics (
6753 &mut self,
6754 #executor_param,
6755 source: #root::audit::AuditSource,
6756 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6757 #executor_where
6758 {
6759 #root::audit::with_source(source, self.save_on(_executor)).await
6760 }
6761
6762 /// Insert this row or update it in-place if the primary key already
6763 /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6764 ///
6765 /// With `Auto::Unset` PK the server assigns a new key and no conflict
6766 /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6767 /// inserted if absent or all non-PK columns are overwritten if present.
6768 ///
6769 /// # Errors
6770 /// As [`Self::insert_on`].
6771 #[cfg(feature = "postgres")]
6772 pub async fn upsert(
6773 &mut self,
6774 pool: &#root::sql::sqlx::PgPool,
6775 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6776 #pool_to_upsert_on
6777 }
6778
6779 /// Like [`Self::upsert`] but accepts any sqlx executor.
6780 /// See [`Self::save_on`] for tenancy-scoped rationale.
6781 ///
6782 /// # Errors
6783 /// As [`Self::upsert`].
6784 #[cfg(feature = "postgres")]
6785 pub async fn upsert_on #executor_generics (
6786 &mut self,
6787 #executor_param,
6788 ) -> ::core::result::Result<(), #root::sql::ExecError>
6789 #executor_where
6790 {
6791 let mut _columns: ::std::vec::Vec<&'static str> =
6792 ::std::vec::Vec::new();
6793 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6794 ::std::vec::Vec::new();
6795 #( #upsert_pushes )*
6796 let query = #root::core::InsertQuery::new(
6797 <Self as #root::core::Model>::SCHEMA,
6798 _columns,
6799 _values,
6800 )
6801 .returning(::std::vec![ #( #upsert_returning ),* ])
6802 .on_conflict(#conflict_clause);
6803 #upsert_run
6804 let _returning_row = &_returning_row_v;
6805 #( #upsert_auto_assigns )*
6806 #audit_upsert_emit
6807 ::core::result::Result::Ok(())
6808 }
6809 })
6810 } else {
6811 None
6812 };
6813
6814 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6815 let pk_column_lit = pk_column.as_str();
6816 // Optional `soft_delete_on` / `restore_on` companions when the
6817 // model has a `#[rustango(soft_delete)]` column. They land
6818 // alongside the regular `delete_on` so callers have both
6819 // options — a hard delete (audit-tracked as a real DELETE) and
6820 // a logical delete (audit-tracked as an UPDATE setting the
6821 // deleted_at column to NOW()).
6822 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6823 let col_lit = col;
6824 let sd_field_ident = fields
6825 .soft_delete_field_ident
6826 .clone()
6827 .expect("soft_delete_column without ident");
6828 quote! {
6829 /// Soft-delete this row by setting its
6830 /// `#[rustango(soft_delete)]` column to `NOW()`.
6831 /// The row stays in the table; query helpers can
6832 /// filter it out by checking the column for `IS NOT
6833 /// NULL`.
6834 ///
6835 /// # Errors
6836 /// As [`Self::delete`].
6837 #[cfg(feature = "postgres")]
6838 pub async fn soft_delete_on #executor_generics (
6839 &self,
6840 #executor_param,
6841 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6842 #executor_where
6843 {
6844 let _now = #root::__chrono::Utc::now();
6845 let _query = #root::soft_delete::__mark_query(
6846 <Self as #root::core::Model>::SCHEMA,
6847 #col_lit,
6848 #pk_column_lit,
6849 ::core::convert::Into::<#root::core::SqlValue>::into(
6850 ::core::clone::Clone::clone(&self.#pk_ident)
6851 ),
6852 ::core::option::Option::Some(_now),
6853 );
6854 let _affected = #root::sql::__macro_internals::update_on(
6855 #executor_passes_to_data_write,
6856 &_query,
6857 ).await?;
6858 #audit_softdelete_emit
6859 ::core::result::Result::Ok(_affected)
6860 }
6861
6862 /// Inverse of [`Self::soft_delete_on`] — clears the
6863 /// soft-delete column back to NULL so the row is
6864 /// considered live again.
6865 ///
6866 /// # Errors
6867 /// As [`Self::delete`].
6868 #[cfg(feature = "postgres")]
6869 pub async fn restore_on #executor_generics (
6870 &self,
6871 #executor_param,
6872 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6873 #executor_where
6874 {
6875 let _query = #root::soft_delete::__mark_query(
6876 <Self as #root::core::Model>::SCHEMA,
6877 #col_lit,
6878 #pk_column_lit,
6879 ::core::convert::Into::<#root::core::SqlValue>::into(
6880 ::core::clone::Clone::clone(&self.#pk_ident)
6881 ),
6882 ::core::option::Option::None,
6883 );
6884 let _affected = #root::sql::__macro_internals::update_on(
6885 #executor_passes_to_data_write,
6886 &_query,
6887 ).await?;
6888 #audit_restore_emit
6889 ::core::result::Result::Ok(_affected)
6890 }
6891
6892 /// Tri-dialect counterpart of [`Self::soft_delete_on`]
6893 /// — takes [`#root::sql::Pool`] and dispatches per
6894 /// backend. Eloquent `Model::delete()` semantics on
6895 /// soft-delete-enabled models (closes #821 partial).
6896 ///
6897 /// Sets the `#[rustango(soft_delete)]` column to
6898 /// `NOW()` on every backend. Query helpers
6899 /// (`QuerySet::active()` / `only_trashed()`,
6900 /// `soft_delete::active_filter` /
6901 /// `compose_with_active`) filter trashed rows out by
6902 /// reading `IS NULL` on the same column. Keyed on the PK;
6903 /// global scopes do not apply.
6904 ///
6905 /// # Errors
6906 /// As [`#root::sql::update_pool`].
6907 pub async fn soft_delete(
6908 &self,
6909 pool: &#root::sql::Pool,
6910 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6911 let _now = #root::__chrono::Utc::now();
6912 let _query = #root::soft_delete::__mark_query(
6913 <Self as #root::core::Model>::SCHEMA,
6914 #col_lit,
6915 #pk_column_lit,
6916 ::core::convert::Into::<#root::core::SqlValue>::into(
6917 ::core::clone::Clone::clone(&self.#pk_ident)
6918 ),
6919 ::core::option::Option::Some(_now),
6920 );
6921 #pool_softdelete_run
6922 }
6923
6924 /// Tri-dialect counterpart of [`Self::restore_on`].
6925 /// Clears the `#[rustango(soft_delete)]` column back
6926 /// to `NULL`, marking the row live again. Eloquent
6927 /// `Model::restore()` parity. Keyed on the PK; global
6928 /// scopes do not apply.
6929 ///
6930 /// # Errors
6931 /// As [`#root::sql::update_pool`].
6932 pub async fn restore(
6933 &self,
6934 pool: &#root::sql::Pool,
6935 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6936 let _query = #root::soft_delete::__mark_query(
6937 <Self as #root::core::Model>::SCHEMA,
6938 #col_lit,
6939 #pk_column_lit,
6940 ::core::convert::Into::<#root::core::SqlValue>::into(
6941 ::core::clone::Clone::clone(&self.#pk_ident)
6942 ),
6943 ::core::option::Option::None,
6944 );
6945 #pool_restore_run
6946 }
6947
6948 /// Hard-delete this row, ignoring the soft-delete
6949 /// column. Eloquent `Model::forceDelete()` parity —
6950 /// the escape hatch when you need to actually purge
6951 /// data (GDPR, fixture cleanup, etc.).
6952 ///
6953 /// Equivalent to [`Self::delete_pool`] (the framework's
6954 /// non-soft delete) — exposed under the Eloquent name
6955 /// for muscle-memory + so soft-delete-enabled models
6956 /// have all three operations (soft / restore / force)
6957 /// in one place.
6958 ///
6959 /// # Errors
6960 /// As [`Self::delete_pool`].
6961 pub async fn force_delete(
6962 &self,
6963 pool: &#root::sql::Pool,
6964 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6965 Self::delete_pool(self, pool).await
6966 }
6967
6968 /// Returns `true` when this row is soft-deleted (its
6969 /// `#[rustango(soft_delete)]` column is currently
6970 /// set — Eloquent `$model->trashed()` parity).
6971 ///
6972 /// Pure in-memory predicate over `&self`; does not
6973 /// hit the database. Useful in admin/template code
6974 /// like `{% if post.trashed() %}…{% endif %}` and in
6975 /// guard clauses on restore/force-delete flows.
6976 pub fn trashed(&self) -> bool {
6977 ::core::option::Option::is_some(&self.#sd_field_ident)
6978 }
6979
6980 /// Fetch every row whose `#[rustango(soft_delete)]`
6981 /// column is `NULL` (a.k.a. the live, non-trashed
6982 /// rows). Eloquent's default `Model::all()` behavior on
6983 /// a soft-delete model (Eloquent auto-scopes trashed
6984 /// rows out; rustango doesn't have auto-scoping yet —
6985 /// see issue #820 — so this is the explicit shortcut).
6986 ///
6987 /// One-liner over `QuerySet::<Self>::default()
6988 /// .active().fetch(pool)`. Closes #821 partial.
6989 ///
6990 /// # Errors
6991 /// As [`#root::sql::FetcherPool::fetch`].
6992 pub async fn active(
6993 pool: &#root::sql::Pool,
6994 ) -> ::core::result::Result<
6995 ::std::vec::Vec<Self>,
6996 #root::sql::ExecError,
6997 > {
6998 use #root::sql::FetcherPool as _;
6999 #root::query::QuerySet::<Self>::default()
7000 .active()
7001 .fetch(pool)
7002 .await
7003 }
7004
7005 /// Fetch ONLY soft-deleted rows. Eloquent
7006 /// `Model::onlyTrashed()->get()` parity — drives the
7007 /// admin "Trash" page, restore flows, GDPR purge
7008 /// scans, etc.
7009 ///
7010 /// One-liner over `QuerySet::<Self>::default()
7011 /// .only_trashed().fetch(pool)`. Closes #821
7012 /// partial.
7013 ///
7014 /// # Errors
7015 /// As [`#root::sql::FetcherPool::fetch`].
7016 pub async fn only_trashed(
7017 pool: &#root::sql::Pool,
7018 ) -> ::core::result::Result<
7019 ::std::vec::Vec<Self>,
7020 #root::sql::ExecError,
7021 > {
7022 use #root::sql::FetcherPool as _;
7023 #root::query::QuerySet::<Self>::default()
7024 .only_trashed()
7025 .fetch(pool)
7026 .await
7027 }
7028
7029 /// Fetch every row, both live and soft-deleted.
7030 /// Eloquent `Model::withTrashed()->get()` parity.
7031 ///
7032 /// Today every queryset already includes trashed rows
7033 /// (rustango has no global-scope tracking yet — issue
7034 /// #820), so this is functionally equivalent to
7035 /// [`Self::all`]. Exposed as a named shortcut so
7036 /// soft-delete-aware code reads `Model::with_trashed_pool`
7037 /// rather than `Model::all_pool` — keeps intent visible
7038 /// in callers and stays correct when auto-scoping lands.
7039 ///
7040 /// Closes #821 partial.
7041 ///
7042 /// # Errors
7043 /// As [`#root::sql::FetcherPool::fetch`].
7044 pub async fn with_trashed(
7045 pool: &#root::sql::Pool,
7046 ) -> ::core::result::Result<
7047 ::std::vec::Vec<Self>,
7048 #root::sql::ExecError,
7049 > {
7050 use #root::sql::FetcherPool as _;
7051 #root::query::QuerySet::<Self>::default()
7052 .with_trashed()
7053 .fetch(pool)
7054 .await
7055 }
7056
7057 }
7058 } else {
7059 quote!()
7060 };
7061 quote! {
7062 /// Delete the row identified by this instance's primary key.
7063 ///
7064 /// Returns the number of rows affected (0 or 1).
7065 ///
7066 /// **Postgres only — prefer [`Self::delete_pool`].** This takes
7067 /// a driver-specific `PgPool`, so it does not exist on a build
7068 /// without the `postgres` feature. [`Self::delete_pool`] takes
7069 /// `rustango::sql::Pool` and works on all three. The naming is
7070 /// inverted; tracked in #1293.
7071 ///
7072 /// # Errors
7073 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7074 /// driver failures.
7075 #[cfg(feature = "postgres")]
7076 pub async fn delete(
7077 &self,
7078 pool: &#root::sql::sqlx::PgPool,
7079 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7080 #pool_to_delete_on
7081 }
7082
7083 /// Like [`Self::delete`] but accepts any sqlx executor —
7084 /// for tenant-scoped deletes against an explicitly-acquired
7085 /// connection. See [`Self::save_on`] for the rationale.
7086 ///
7087 /// # Errors
7088 /// As [`Self::delete`].
7089 #[cfg(feature = "postgres")]
7090 pub async fn delete_on #executor_generics (
7091 &self,
7092 #executor_param,
7093 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7094 #executor_where
7095 {
7096 let query = #root::core::DeleteQuery::by_pk(
7097 <Self as #root::core::Model>::SCHEMA,
7098 #pk_column_lit,
7099 ::core::convert::Into::<#root::core::SqlValue>::into(
7100 ::core::clone::Clone::clone(&self.#pk_ident)
7101 ),
7102 );
7103 let _affected = #root::sql::__macro_internals::delete_on(
7104 #executor_passes_to_data_write,
7105 &query,
7106 ).await?;
7107 #audit_delete_emit
7108 ::core::result::Result::Ok(_affected)
7109 }
7110
7111 /// Per-call audit-source override for [`Self::delete_on`].
7112 /// See [`Self::save_on_with`] for shape rationale.
7113 ///
7114 /// # Errors
7115 /// As [`Self::delete_on`].
7116 #[cfg(feature = "postgres")]
7117 pub async fn delete_on_with #executor_generics (
7118 &self,
7119 #executor_param,
7120 source: #root::audit::AuditSource,
7121 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7122 #executor_where
7123 {
7124 #root::audit::with_source(source, self.delete_on(_executor)).await
7125 }
7126 #pool_delete_method
7127 #pool_insert_method
7128 #audit_entry_method
7129 #pool_save_method
7130 #refresh_replicate_methods
7131 #tx_delete_method
7132 #tx_insert_method
7133 #tx_save_method
7134 #soft_delete_methods
7135
7136 /// Returns `true` when `other` represents the same DB
7137 /// row as `self` — i.e. their primary keys compare
7138 /// equal. Eloquent `$model->is($other)` parity.
7139 ///
7140 /// Because both arguments are typed `&Self`, the
7141 /// model/table check is automatic — `Post::is` cannot
7142 /// be invoked against a `Comment` at compile time. Only
7143 /// the PK has to be compared at runtime.
7144 pub fn is(&self, other: &Self) -> bool {
7145 self.#pk_ident == other.#pk_ident
7146 }
7147
7148 /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7149 /// parity.
7150 pub fn is_not(&self, other: &Self) -> bool {
7151 self.#pk_ident != other.#pk_ident
7152 }
7153
7154 /// Returns this row's primary-key value as an
7155 /// [`#root::core::SqlValue`]. Eloquent
7156 /// `$model->getKey()` parity.
7157 ///
7158 /// Useful when you need to thread the PK through a
7159 /// generic `Into<SqlValue>`-bound API without knowing the
7160 /// concrete PK type (`i64` vs `Uuid` vs `String`).
7161 #[must_use]
7162 pub fn get_key(&self) -> #root::core::SqlValue {
7163 ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7164 }
7165
7166 }
7167 });
7168
7169 let insert_method = if fields.has_auto {
7170 let pushes = &fields.insert_pushes;
7171 let returning_cols = &fields.returning_cols;
7172 let auto_assigns = &fields.auto_assigns;
7173 quote! {
7174 /// Insert this row into its table. Skips columns whose
7175 /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7176 /// sequence fills them in, then reads each `Auto` column
7177 /// back via `RETURNING` and stores it on `self`.
7178 ///
7179 /// **Postgres only — prefer [`Self::insert_pool`].** This takes
7180 /// a driver-specific `PgPool`, so it does not exist on a build
7181 /// without the `postgres` feature. [`Self::insert_pool`] takes
7182 /// `rustango::sql::Pool` and works on all three. The naming is
7183 /// inverted; tracked in #1293.
7184 ///
7185 /// # Errors
7186 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7187 /// driver failures.
7188 #[cfg(feature = "postgres")]
7189 pub async fn insert(
7190 &mut self,
7191 pool: &#root::sql::sqlx::PgPool,
7192 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7193 #pool_to_insert_on
7194 }
7195
7196 /// Like [`Self::insert`] but accepts any sqlx executor.
7197 /// See [`Self::save_on`] for tenancy-scoped rationale.
7198 ///
7199 /// # Errors
7200 /// As [`Self::insert`].
7201 #[cfg(feature = "postgres")]
7202 pub async fn insert_on #executor_generics (
7203 &mut self,
7204 #executor_param,
7205 ) -> ::core::result::Result<(), #root::sql::ExecError>
7206 #executor_where
7207 {
7208 let mut _columns: ::std::vec::Vec<&'static str> =
7209 ::std::vec::Vec::new();
7210 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7211 ::std::vec::Vec::new();
7212 #( #pushes )*
7213 let query = #root::core::InsertQuery::new(
7214 <Self as #root::core::Model>::SCHEMA,
7215 _columns,
7216 _values,
7217 )
7218 .returning(::std::vec![ #( #returning_cols ),* ]);
7219 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7220 #executor_passes_to_data_write,
7221 &query,
7222 ).await?;
7223 let _returning_row = &_returning_row_v;
7224 #( #auto_assigns )*
7225 #audit_insert_emit
7226 ::core::result::Result::Ok(())
7227 }
7228
7229 /// Per-call audit-source override for [`Self::insert_on`].
7230 /// See [`Self::save_on_with`] for shape rationale.
7231 ///
7232 /// # Errors
7233 /// As [`Self::insert_on`].
7234 #[cfg(feature = "postgres")]
7235 pub async fn insert_on_with #executor_generics (
7236 &mut self,
7237 #executor_param,
7238 source: #root::audit::AuditSource,
7239 ) -> ::core::result::Result<(), #root::sql::ExecError>
7240 #executor_where
7241 {
7242 #root::audit::with_source(source, self.insert_on(_executor)).await
7243 }
7244 }
7245 } else {
7246 let insert_columns = &fields.insert_columns;
7247 let insert_values = &fields.insert_values;
7248 quote! {
7249 /// Insert this row into its table.
7250 ///
7251 /// # Errors
7252 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7253 /// driver failures.
7254 #[cfg(feature = "postgres")]
7255 pub async fn insert(
7256 &self,
7257 pool: &#root::sql::sqlx::PgPool,
7258 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7259 self.insert_on(pool).await
7260 }
7261
7262 /// Like [`Self::insert`] but accepts any sqlx executor.
7263 /// See [`Self::save_on`] for tenancy-scoped rationale.
7264 ///
7265 /// # Errors
7266 /// As [`Self::insert`].
7267 #[cfg(feature = "postgres")]
7268 pub async fn insert_on<'_c, _E>(
7269 &self,
7270 _executor: _E,
7271 ) -> ::core::result::Result<(), #root::sql::ExecError>
7272 where
7273 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7274 {
7275 let query = #root::core::InsertQuery::new(
7276 <Self as #root::core::Model>::SCHEMA,
7277 ::std::vec![ #( #insert_columns ),* ],
7278 ::std::vec![ #( #insert_values ),* ],
7279 );
7280 #root::sql::__macro_internals::insert_on(_executor, &query).await
7281 }
7282 }
7283 };
7284
7285 let bulk_insert_method = if fields.has_auto {
7286 let cols_no_auto = &fields.bulk_columns_no_auto;
7287 let cols_all = &fields.bulk_columns_all;
7288 let pushes_no_auto = &fields.bulk_pushes_no_auto;
7289 let pushes_all = &fields.bulk_pushes_all;
7290 let returning_cols = &fields.returning_cols;
7291 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7292 let uniformity = &fields.bulk_auto_uniformity;
7293 let fill_in_place = &fields.bulk_fill_in_place;
7294 // Nothing to read back when every Auto field is filled Rust-side (#1934).
7295 let read_back = if fields.returning_cols.is_empty() {
7296 quote!()
7297 } else {
7298 quote! {
7299 if _returned.len() != rows.len() {
7300 return ::core::result::Result::Err(
7301 #root::sql::ExecError::Sql(
7302 #root::sql::SqlError::BulkInsertReturningMismatch {
7303 expected: rows.len(),
7304 actual: _returned.len(),
7305 }
7306 )
7307 );
7308 }
7309 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7310 #auto_assigns_for_row
7311 }
7312 }
7313 };
7314 // The DB-filled `Auto` picks the path; Rust-filled ones fill per row (#1950).
7315 let first_auto_ident = fields
7316 .first_db_auto
7317 .as_ref()
7318 .map(|(ident, _)| ident)
7319 .or(fields.first_auto_ident.as_ref())
7320 .expect("has_auto implies first_auto_ident is Some");
7321 quote! {
7322 /// Bulk-insert `rows` in a single round-trip. Every row's
7323 /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7324 /// (sequence fills them in) or uniformly `Auto::Set(_)`
7325 /// (caller-supplied values). Mixed Set/Unset is rejected
7326 /// — call `insert` per row for that case.
7327 ///
7328 /// Empty slice is a no-op. Each row's `Auto` fields are
7329 /// populated from the `RETURNING` clause in input order
7330 /// before this returns.
7331 ///
7332 /// # Errors
7333 /// Returns [`#root::sql::ExecError`] for validation,
7334 /// SQL-writing, mixed-Auto rejection, or driver failures.
7335 #[cfg(feature = "postgres")]
7336 pub async fn bulk_insert(
7337 rows: &mut [Self],
7338 pool: &#root::sql::sqlx::PgPool,
7339 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7340 #pool_to_bulk_insert_on
7341 }
7342
7343 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7344 /// See [`Self::save_on`] for tenancy-scoped rationale.
7345 ///
7346 /// # Errors
7347 /// As [`Self::bulk_insert`].
7348 #[cfg(feature = "postgres")]
7349 pub async fn bulk_insert_on #executor_generics (
7350 rows: &mut [Self],
7351 #executor_param,
7352 ) -> ::core::result::Result<(), #root::sql::ExecError>
7353 #executor_where
7354 {
7355 if rows.is_empty() {
7356 return ::core::result::Result::Ok(());
7357 }
7358 let _first_unset = matches!(
7359 rows[0].#first_auto_ident,
7360 #root::sql::Auto::Unset
7361 );
7362 #( #uniformity )*
7363 for _row in rows.iter_mut() {
7364 #( #fill_in_place )*
7365 }
7366
7367 let mut _all_rows: ::std::vec::Vec<
7368 ::std::vec::Vec<#root::core::SqlValue>,
7369 > = ::std::vec::Vec::with_capacity(rows.len());
7370 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7371 for _row in rows.iter() {
7372 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7373 ::std::vec::Vec::new();
7374 #( #pushes_no_auto )*
7375 _all_rows.push(_row_vals);
7376 }
7377 ::std::vec![ #( #cols_no_auto ),* ]
7378 } else {
7379 for _row in rows.iter() {
7380 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7381 ::std::vec::Vec::new();
7382 #( #pushes_all )*
7383 _all_rows.push(_row_vals);
7384 }
7385 ::std::vec![ #( #cols_all ),* ]
7386 };
7387
7388 let _query = #root::core::BulkInsertQuery::new(
7389 <Self as #root::core::Model>::SCHEMA,
7390 _columns,
7391 _all_rows,
7392 )
7393 .returning(::std::vec![ #( #returning_cols ),* ]);
7394 let _returned = #root::sql::__macro_internals::bulk_insert_on(
7395 #executor_passes_to_data_write,
7396 &_query,
7397 ).await?;
7398 #read_back
7399 #audit_bulk_insert_emit
7400 ::core::result::Result::Ok(())
7401 }
7402 }
7403 } else {
7404 let cols_all = &fields.bulk_columns_all;
7405 let pushes_all = &fields.bulk_pushes_all;
7406 quote! {
7407 /// Bulk-insert `rows` in a single round-trip. Every row's
7408 /// fields are written verbatim — there are no `Auto<T>`
7409 /// fields on this model.
7410 ///
7411 /// Empty slice is a no-op.
7412 ///
7413 /// # Errors
7414 /// Returns [`#root::sql::ExecError`] for validation,
7415 /// SQL-writing, or driver failures.
7416 #[cfg(feature = "postgres")]
7417 pub async fn bulk_insert(
7418 rows: &[Self],
7419 pool: &#root::sql::sqlx::PgPool,
7420 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7421 #pool_to_bulk_insert_on
7422 }
7423
7424 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7425 /// See [`Self::save_on`] for tenancy-scoped rationale.
7426 ///
7427 /// # Errors
7428 /// As [`Self::bulk_insert`].
7429 #[cfg(feature = "postgres")]
7430 pub async fn bulk_insert_on #executor_generics (
7431 rows: &[Self],
7432 #executor_param,
7433 ) -> ::core::result::Result<(), #root::sql::ExecError>
7434 #executor_where
7435 {
7436 if rows.is_empty() {
7437 return ::core::result::Result::Ok(());
7438 }
7439 let mut _all_rows: ::std::vec::Vec<
7440 ::std::vec::Vec<#root::core::SqlValue>,
7441 > = ::std::vec::Vec::with_capacity(rows.len());
7442 for _row in rows.iter() {
7443 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7444 ::std::vec::Vec::new();
7445 #( #pushes_all )*
7446 _all_rows.push(_row_vals);
7447 }
7448 let _query = #root::core::BulkInsertQuery::new(
7449 <Self as #root::core::Model>::SCHEMA,
7450 ::std::vec![ #( #cols_all ),* ],
7451 _all_rows,
7452 );
7453 let _ = #root::sql::__macro_internals::bulk_insert_on(
7454 #executor_passes_to_data_write,
7455 &_query,
7456 ).await?;
7457 #audit_bulk_insert_emit
7458 ::core::result::Result::Ok(())
7459 }
7460 }
7461 };
7462
7463 // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7464 // (no postgres-feature gate); routes through the existing
7465 // `bulk_insert_pool` + per-dialect conflict writer.
7466 //
7467 // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7468 // sequence picks the PK for fresh inserts; the UPDATE branch never
7469 // touches the Auto column.
7470 // Audited models audit each written row; MySQL, with no RETURNING,
7471 // is refused there (#1747, #1795).
7472 let run_conflict_bulk_insert = match (audited_fields.is_some(), primary_key.is_some()) {
7473 (false, _) => quote!(#root::sql::bulk_insert_pool(pool, &_query).await),
7474 (true, true) => quote! {
7475 #root::audit::bulk_insert_with_audit::<Self>(pool, &_query, |_r: &Self, _op| {
7476 _r.__rustango_audit_entry(_op)
7477 })
7478 .await
7479 },
7480 (true, false) => quote! {
7481 ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
7482 table: <Self as #root::core::Model>::SCHEMA.table,
7483 })
7484 },
7485 };
7486 let bulk_upsert_pool_method = {
7487 // Pick the "no Auto" columns when the model has Auto fields,
7488 // else every column.
7489 let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7490 (
7491 fields.bulk_columns_no_auto.clone(),
7492 fields.bulk_pushes_no_auto.clone(),
7493 )
7494 } else {
7495 (
7496 fields.bulk_columns_all.clone(),
7497 fields.bulk_pushes_all.clone(),
7498 )
7499 };
7500 quote! {
7501 /// Tri-dialect bulk insert that updates on conflict — the
7502 /// "import a batch idempotently" shape. Issue #267 / T1.5.
7503 ///
7504 /// Per-row values are extracted and lowered into a
7505 /// [`#root::core::BulkInsertQuery`] with
7506 /// `on_conflict = DoUpdate { target, update_columns }`. The
7507 /// writer dispatches per-dialect:
7508 /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7509 /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)` (target ignored — MySQL matches every UNIQUE index)
7510 ///
7511 /// `target` names the column(s) whose unique constraint
7512 /// defines the conflict (typically a `unique` or
7513 /// `unique_together` natural-key column, NOT the `Auto<T>`
7514 /// PK). `update_cols` names the columns to overwrite on
7515 /// conflict — every other column is left untouched on the
7516 /// existing row.
7517 ///
7518 /// Auto-PK rows must all have `Auto::Unset` (the sequence
7519 /// picks the PK on insert; the update path never touches
7520 /// the Auto column). Auto-set rows trigger a hard error.
7521 /// Empty slice is a no-op. On an audited model each written
7522 /// row gets a `create` or `update` audit row; MySQL refuses it.
7523 ///
7524 /// # Errors
7525 /// Returns [`#root::sql::ExecError`] for validation,
7526 /// SQL-writing, or driver failures.
7527 pub async fn bulk_upsert_pool(
7528 rows: &[Self],
7529 target: &[&'static str],
7530 update_cols: &[&'static str],
7531 pool: &#root::sql::Pool,
7532 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7533 if rows.is_empty() {
7534 return ::core::result::Result::Ok(());
7535 }
7536 let mut _all_rows: ::std::vec::Vec<
7537 ::std::vec::Vec<#root::core::SqlValue>,
7538 > = ::std::vec::Vec::with_capacity(rows.len());
7539 for _row in rows.iter() {
7540 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7541 ::std::vec::Vec::new();
7542 #( #upsert_pushes )*
7543 _all_rows.push(_row_vals);
7544 }
7545 let _query = #root::core::BulkInsertQuery::new(
7546 <Self as #root::core::Model>::SCHEMA,
7547 ::std::vec![ #( #upsert_cols ),* ],
7548 _all_rows,
7549 )
7550 .on_conflict_do_update(target, update_cols);
7551 #run_conflict_bulk_insert
7552 }
7553
7554 /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7555 /// skip rows that would violate a unique constraint. Issue
7556 /// #267 / T1.5. Same per-dialect dispatch as
7557 /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7558 /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7559 /// <pivot> = <pivot>` (MySQL no-op write).
7560 ///
7561 /// # Errors
7562 /// As [`Self::bulk_upsert_pool`].
7563 pub async fn bulk_insert_or_ignore_pool(
7564 rows: &[Self],
7565 pool: &#root::sql::Pool,
7566 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7567 if rows.is_empty() {
7568 return ::core::result::Result::Ok(());
7569 }
7570 let mut _all_rows: ::std::vec::Vec<
7571 ::std::vec::Vec<#root::core::SqlValue>,
7572 > = ::std::vec::Vec::with_capacity(rows.len());
7573 for _row in rows.iter() {
7574 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7575 ::std::vec::Vec::new();
7576 #( #upsert_pushes )*
7577 _all_rows.push(_row_vals);
7578 }
7579 let _query = #root::core::BulkInsertQuery::new(
7580 <Self as #root::core::Model>::SCHEMA,
7581 ::std::vec![ #( #upsert_cols ),* ],
7582 _all_rows,
7583 )
7584 .on_conflict_do_nothing();
7585 #run_conflict_bulk_insert
7586 }
7587 }
7588 };
7589
7590 // Ergonomic `Model::bulk_update(objs, fields)`.
7591 // The SQL/IR/executor stack
7592 // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7593 // `write_bulk_update_*` writers) already existed; what was missing
7594 // was the per-model constructor that maps `&[Self]` + a runtime
7595 // column list into rows of `[pk, col_vals…]` so callers don't
7596 // hand-build the IR. Emitted only when the model has a primary key
7597 // (the PK is the join key and can't itself be updated).
7598 let bulk_update_rows_run = if audited_fields.is_some() {
7599 quote!(#root::audit::bulk_update_with_audit::<Self>(pool, &_query, |_r: &Self| {
7600 _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
7601 })
7602 .await)
7603 } else {
7604 quote!(#root::sql::bulk_update_pool(pool, &_query).await)
7605 };
7606 let bulk_update_method = match &fields.primary_key {
7607 None => quote! {},
7608 Some((pk_ident, pk_col)) => {
7609 // One pair of match arms per non-PK column: a validation arm
7610 // resolving the runtime name to its `&'static str` column,
7611 // and a value arm pushing that field off the row.
7612 let mut col_arms: Vec<TokenStream2> = Vec::new();
7613 let mut val_arms: Vec<TokenStream2> = Vec::new();
7614 for entry in &fields.column_entries {
7615 if &entry.column == pk_col {
7616 continue;
7617 }
7618 let col = &entry.column;
7619 let ident = &entry.ident;
7620 col_arms.push(quote! { #col => #col, });
7621 val_arms.push(quote! {
7622 #col => _row_vals.push(
7623 ::core::convert::Into::<#root::core::SqlValue>::into(
7624 ::core::clone::Clone::clone(&_o.#ident)
7625 )
7626 ),
7627 });
7628 }
7629 quote! {
7630 /// Write per-row-different values for the named `fields`
7631 /// across every object in `objs`, matched by primary key.
7632 /// Past the backend's bind cap the rows go in batches inside
7633 /// one transaction, so the update is all or none.
7634 ///
7635 /// `fields` names the **columns** to update. The primary
7636 /// key identifies each row and cannot itself be updated
7637 /// (pass it and you get
7638 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7639 /// Empty `objs` or `fields` is a no-op returning `0`.
7640 /// Objects whose PK matches no row are simply not updated.
7641 /// Returns the number of rows affected.
7642 ///
7643 /// Tri-dialect: lowers to a
7644 /// [`#root::core::BulkUpdateQuery`] and dispatches
7645 /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7646 /// a CTE + correlated subquery on SQLite, an inner
7647 /// `JOIN (VALUES …)` on MySQL.
7648 ///
7649 /// # Errors
7650 /// [`#root::core::QueryError::UnknownField`] for a name
7651 /// that isn't a column on this model,
7652 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7653 /// `fields` names the PK, or [`#root::sql::ExecError`] for
7654 /// SQL-writing / driver failures.
7655 pub async fn bulk_update(
7656 objs: &[Self],
7657 fields: &[&str],
7658 pool: &#root::sql::Pool,
7659 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7660 if objs.is_empty() || fields.is_empty() {
7661 return ::core::result::Result::Ok(0);
7662 }
7663 let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7664 let mut _update_columns: ::std::vec::Vec<&'static str> =
7665 ::std::vec::Vec::with_capacity(fields.len());
7666 for &_f in fields {
7667 let _col: &'static str = match _f {
7668 #pk_col => {
7669 return ::core::result::Result::Err(
7670 ::core::convert::Into::into(
7671 #root::core::QueryError::BulkUpdatePrimaryKey {
7672 model: _model_name,
7673 field: ::std::string::ToString::to_string(_f),
7674 }
7675 )
7676 );
7677 }
7678 #( #col_arms )*
7679 _ => {
7680 return ::core::result::Result::Err(
7681 ::core::convert::Into::into(
7682 #root::core::QueryError::UnknownField {
7683 model: _model_name,
7684 field: ::std::string::ToString::to_string(_f),
7685 }
7686 )
7687 );
7688 }
7689 };
7690 // A model whose only field is the primary key emits no
7691 // non-PK arms, so every arm above diverges and this push
7692 // is genuinely unreachable — correct, since such a model
7693 // has nothing to bulk-update. Scoped to the statement
7694 // rather than the fn so a real unreachability elsewhere
7695 // in this body still warns. Without it every PK-only
7696 // model warns in the *user's* crate, which they cannot
7697 // act on (#1290).
7698 #[allow(unreachable_code)]
7699 _update_columns.push(_col);
7700 }
7701 let mut _rows: ::std::vec::Vec<
7702 ::std::vec::Vec<#root::core::SqlValue>,
7703 > = ::std::vec::Vec::with_capacity(objs.len());
7704 for _o in objs.iter() {
7705 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7706 ::std::vec::Vec::with_capacity(fields.len() + 1);
7707 // PK first — the writers expect `[pk, …update cols]`.
7708 _row_vals.push(
7709 ::core::convert::Into::<#root::core::SqlValue>::into(
7710 ::core::clone::Clone::clone(&_o.#pk_ident)
7711 )
7712 );
7713 for &_f in fields {
7714 match _f {
7715 #( #val_arms )*
7716 // Unreachable: every name was validated
7717 // against the same arm set above.
7718 _ => {}
7719 }
7720 }
7721 _rows.push(_row_vals);
7722 }
7723 let _query = #root::core::BulkUpdateQuery::new(
7724 <Self as #root::core::Model>::SCHEMA,
7725 _update_columns,
7726 _rows,
7727 );
7728 #bulk_update_rows_run
7729 }
7730 }
7731 }
7732 };
7733
7734 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7735 quote! {
7736 /// Hidden runtime accessor for the primary-key value as a
7737 /// [`SqlValue`]. Used by reverse-relation helpers
7738 /// (`<parent>::<child>_set`) emitted from sibling models'
7739 /// FK fields. Not part of the public API.
7740 #[doc(hidden)]
7741 pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7742 ::core::convert::Into::<#root::core::SqlValue>::into(
7743 ::core::clone::Clone::clone(&self.#pk_ident)
7744 )
7745 }
7746 }
7747 });
7748
7749 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7750 quote! {
7751 impl #root::sql::HasPkValue for #struct_name {
7752 fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7753 ::core::convert::Into::<#root::core::SqlValue>::into(
7754 ::core::clone::Clone::clone(&self.#pk_ident)
7755 )
7756 }
7757 }
7758 }
7759 });
7760
7761 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7762
7763 // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7764 // dispatch to the right per-backend body without the macro emitting
7765 // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7766 // so audited models with non-Auto PKs (which still go through
7767 // `insert_one_with_audit` → `apply_auto_pk`) link.
7768 let assign_auto_pk_pool_impl = {
7769 let auto_assigns = &fields.auto_assigns;
7770 // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7771 // so the body is structurally identical to `auto_assigns` —
7772 // only the helper name swaps from `try_get_returning` to
7773 // `try_get_returning_sqlite` so the closure typechecks against
7774 // a `SqliteRow` instead of a `PgRow`.
7775 let auto_assigns_sqlite: Vec<TokenStream2> = fields
7776 .auto_field_idents
7777 .iter()
7778 .map(|(ident, column)| {
7779 quote! {
7780 self.#ident = #root::sql::try_get_returning_sqlite(
7781 _returning_row, #column
7782 )?;
7783 }
7784 })
7785 .collect();
7786 // #1028 — decode each `generated_as` column from the same
7787 // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7788 // own type drives the decode (no `Auto<T>` wrapper).
7789 let generated_assigns: Vec<TokenStream2> = fields
7790 .generated_field_idents
7791 .iter()
7792 .map(|(ident, column)| {
7793 quote! {
7794 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7795 }
7796 })
7797 .collect();
7798 let generated_assigns_sqlite: Vec<TokenStream2> = fields
7799 .generated_field_idents
7800 .iter()
7801 .map(|(ident, column)| {
7802 quote! {
7803 self.#ident = #root::sql::try_get_returning_sqlite(
7804 _returning_row, #column
7805 )?;
7806 }
7807 })
7808 .collect();
7809 let mysql_body = if let Some((first, value_ty)) = fields.first_db_auto.as_ref() {
7810 // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7811 // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7812 // Auto<Uuid>/etc. fail to link against MySQL (intended —
7813 // those models can't use AUTO_INCREMENT). The trait is only
7814 // touched on the MySQL arm at runtime, so PG-only consumers
7815 // never see the bound failure.
7816 //
7817 // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7818 // Auto<i64> PK + auto_now_add timestamp) errored hard at
7819 // runtime with "multi-column RETURNING". MySQL has no
7820 // multi-column RETURNING semantic and a follow-up SELECT
7821 // would need cross-trait plumbing. Pragmatic shape: succeed
7822 // with the FIRST Auto field populated from LAST_INSERT_ID();
7823 // any other Auto fields stay `Auto::Unset`. Callers that
7824 // need the DB-defaulted timestamp / UUID can re-fetch the
7825 // row by PK after `save_pool`. Fixes the cookbook chapter
7826 // 12 dialect divergence.
7827 quote! {
7828 let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7829 ::rustango_from_mysql_auto_id(_id)?;
7830 self.#first = #root::sql::Auto::Set(_converted);
7831 ::core::result::Result::Ok(())
7832 }
7833 } else {
7834 quote! {
7835 let _ = _id;
7836 ::core::result::Result::Ok(())
7837 }
7838 };
7839 quote! {
7840 impl #root::sql::AssignAutoPkPool for #struct_name {
7841 fn __rustango_assign_from_pg_row(
7842 &mut self,
7843 _returning_row: &#root::sql::PgReturningRow,
7844 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7845 #( #auto_assigns )*
7846 #( #generated_assigns )*
7847 ::core::result::Result::Ok(())
7848 }
7849 fn __rustango_assign_from_mysql_id(
7850 &mut self,
7851 _id: i64,
7852 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7853 #mysql_body
7854 }
7855 fn __rustango_assign_from_sqlite_row(
7856 &mut self,
7857 _returning_row: &#root::sql::SqliteReturningRow,
7858 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7859 #( #auto_assigns_sqlite )*
7860 #( #generated_assigns_sqlite )*
7861 ::core::result::Result::Ok(())
7862 }
7863 }
7864 }
7865 };
7866
7867 let from_aliased_row_inits = &fields.from_aliased_row_inits;
7868 let aliased_row_helper = quote! {
7869 /// Decode a row's aliased target columns (produced by
7870 /// `select_related`'s LEFT JOIN) into a fresh instance of
7871 /// this model. Reads each column via
7872 /// `format!("{prefix}__{col}")`, matching the alias the
7873 /// SELECT writer emitted. Slice 9.0d.
7874 #[doc(hidden)]
7875 #[cfg(feature = "postgres")]
7876 pub fn __rustango_from_aliased_row(
7877 row: &#root::sql::sqlx::postgres::PgRow,
7878 prefix: &str,
7879 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7880 ::core::result::Result::Ok(Self {
7881 #( #from_aliased_row_inits ),*
7882 })
7883 }
7884 };
7885 // v0.23.0-batch8 — MySQL counterpart, gated through the
7886 // cfg-aware macro_rules so PG-only builds expand to nothing.
7887 let from_aliased_row_inits_my = &fields.from_aliased_row_inits_my;
7888 let aliased_row_helper_my = quote! {
7889 #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7890 #( #from_aliased_row_inits_my ),*
7891 });
7892 };
7893
7894 // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7895 // pattern + cfg gate on the `sqlite` feature.
7896 let aliased_row_helper_sqlite = quote! {
7897 #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7898 #( #from_aliased_row_inits ),*
7899 });
7900 };
7901
7902 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7903 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7904 let load_related_impl_sqlite =
7905 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7906
7907 // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7908 // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7909 // default `Self::objects()`. Each accessor returns a fresh
7910 // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7911 // methods the user defined.
7912 let extra_manager_fns: Vec<TokenStream2> = manager_fns
7913 .iter()
7914 .map(|fn_ident| {
7915 let model_name_str = struct_name.to_string();
7916 let fn_name_str = fn_ident.to_string();
7917 let doc = format!(
7918 "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7919 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7920 equivalent to `Self::objects()`. Chains with any \
7921 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7922 extension methods."
7923 );
7924 quote! {
7925 #[doc = #doc]
7926 #[must_use]
7927 pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7928 #root::query::QuerySet::new()
7929 }
7930 }
7931 })
7932 .collect();
7933
7934 quote! {
7935 impl #struct_name {
7936 /// Start a new `QuerySet` over this model.
7937 #[must_use]
7938 pub fn objects() -> #root::query::QuerySet<#struct_name> {
7939 #root::query::QuerySet::new()
7940 }
7941
7942 /// Eloquent-shape alias of [`Self::objects`]. Returns
7943 /// a fresh `QuerySet<Self>` ready for `.filter()` /
7944 /// `.where_()` / etc. Matches Laravel muscle-memory:
7945 ///
7946 /// ```ignore
7947 /// // Eloquent: Post::query()->where('published', true)
7948 /// // rustango: Post::query().filter("published", true)
7949 /// // or: Post::objects().filter("published", true)
7950 /// ```
7951 ///
7952 /// Both names point at the same underlying constructor;
7953 /// neither is preferred.
7954 #[must_use]
7955 pub fn query() -> #root::query::QuerySet<#struct_name> {
7956 #root::query::QuerySet::new()
7957 }
7958
7959 #( #extra_manager_fns )*
7960
7961 #insert_method
7962
7963 #bulk_insert_method
7964
7965 #bulk_upsert_pool_method
7966
7967 #bulk_update_method
7968
7969 #save_method
7970
7971 #pk_methods
7972
7973 #pk_value_helper
7974
7975 #aliased_row_helper
7976
7977 #column_consts
7978 }
7979
7980 #aliased_row_helper_my
7981
7982 #aliased_row_helper_sqlite
7983
7984 #load_related_impl
7985
7986 #load_related_impl_my
7987
7988 #load_related_impl_sqlite
7989
7990 #has_pk_value_impl
7991
7992 #fk_pk_access_impl
7993
7994 #assign_auto_pk_pool_impl
7995 }
7996}
7997
7998/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
7999/// `auto_assigns` but reading from `_returning_row` and writing to
8000/// `_row_mut` instead of `self`.
8001fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
8002 let root = rustango_root();
8003 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
8004 let col_lit = column.as_str();
8005 quote! {
8006 _row_mut.#ident = #root::sql::sqlx::Row::try_get(
8007 _returning_row,
8008 #col_lit,
8009 )?;
8010 }
8011 });
8012 quote! { #( #lines )* }
8013}
8014
8015/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
8016fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
8017 let lines = entries.iter().map(|e| {
8018 let ident = &e.ident;
8019 let col_ty = column_type_ident(ident);
8020 quote! {
8021 #[allow(non_upper_case_globals)]
8022 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
8023 }
8024 });
8025 quote! { #(#lines)* }
8026}
8027
8028/// Emit a hidden per-model module carrying one zero-sized type per field,
8029/// each with a `Column` impl pointing back at the model.
8030fn column_module_tokens(
8031 module_ident: &syn::Ident,
8032 struct_name: &syn::Ident,
8033 entries: &[ColumnEntry],
8034) -> TokenStream2 {
8035 let root = rustango_root();
8036 let items = entries.iter().map(|e| {
8037 let col_ty = column_type_ident(&e.ident);
8038 let value_ty = &e.value_ty;
8039 let name = &e.name;
8040 let column = &e.column;
8041 let field_type_tokens = &e.field_type_tokens;
8042 quote! {
8043 #[derive(::core::clone::Clone, ::core::marker::Copy)]
8044 pub struct #col_ty;
8045
8046 impl #root::core::Column for #col_ty {
8047 type Model = super::#struct_name;
8048 type Value = #value_ty;
8049 const NAME: &'static str = #name;
8050 const COLUMN: &'static str = #column;
8051 const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
8052 }
8053 }
8054 });
8055 quote! {
8056 #[doc(hidden)]
8057 #[allow(non_camel_case_types, non_snake_case)]
8058 pub mod #module_ident {
8059 // Re-import the parent scope so field types referencing
8060 // sibling models (e.g. `ForeignKey<Author>`) resolve
8061 // inside this submodule. Without this we'd hit
8062 // `proc_macro_derive_resolution_fallback` warnings.
8063 #[allow(unused_imports)]
8064 use super::*;
8065 #(#items)*
8066 }
8067 }
8068}
8069
8070fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
8071 let name = ident_name(field_ident);
8072 syn::Ident::new(&format!("{name}_col"), field_ident.span())
8073}
8074
8075/// A field's name without the `r#` of a raw ident: `r#type` is `type` (#1937).
8076fn ident_name(ident: &syn::Ident) -> String {
8077 syn::ext::IdentExt::unraw(ident).to_string()
8078}
8079
8080/// The ident for a field named `name`, raw when it is a keyword (`type` → `r#type`).
8081fn ident_from_name(name: &str, span: proc_macro2::Span) -> syn::Ident {
8082 match syn::parse_str::<syn::Ident>(name) {
8083 Ok(mut ident) => {
8084 ident.set_span(span);
8085 ident
8086 }
8087 Err(_) => syn::Ident::new_raw(name, span),
8088 }
8089}
8090
8091fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8092 syn::Ident::new(
8093 &format!("__rustango_cols_{struct_name}"),
8094 struct_name.span(),
8095 )
8096}
8097
8098fn from_row_impl_tokens(
8099 struct_name: &syn::Ident,
8100 from_row_inits: &[TokenStream2],
8101 from_row_inits_my: &[TokenStream2],
8102) -> TokenStream2 {
8103 let root = rustango_root();
8104 // The Postgres impl is always emitted — every rustango build pulls in
8105 // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8106 // routed through `#root::__impl_my_from_row!`, a cfg-gated
8107 // macro_rules whose body collapses to nothing when rustango is built
8108 // without the `mysql` feature. No user-facing feature shim required.
8109 //
8110 // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8111 // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8112 // back into a comma-separated list inside square brackets. Since each
8113 // entry is already in `field: expr` shape, the existing tokens slot in.
8114 quote! {
8115 #[cfg(feature = "postgres")]
8116 impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8117 for #struct_name
8118 {
8119 fn from_row(
8120 row: &'r #root::sql::sqlx::postgres::PgRow,
8121 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8122 ::core::result::Result::Ok(Self {
8123 #( #from_row_inits ),*
8124 })
8125 }
8126 }
8127
8128 #root::__impl_my_from_row!(#struct_name, |row| {
8129 #( #from_row_inits_my ),*
8130 });
8131
8132 #root::__impl_sqlite_from_row!(#struct_name, |row| {
8133 #( #from_row_inits ),*
8134 });
8135 }
8136}
8137
8138struct ContainerAttrs {
8139 table: Option<String>,
8140 display: Option<(String, proc_macro2::Span)>,
8141 /// Explicit app label from `#[rustango(app = "blog")]`.
8142 /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8143 /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8144 /// at runtime — this attribute is the override for cases where
8145 /// the inference is wrong (e.g. a model that conceptually belongs
8146 /// to one app but is physically in another module).
8147 app: Option<String>,
8148 /// Per-model admin knobs from
8149 /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8150 /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8151 /// admin code falls back to `AdminConfig::DEFAULT`.
8152 admin: Option<AdminAttrs>,
8153 /// Per-model audit configuration from `#[rustango(audit(...))]`.
8154 /// `None` when the model isn't audited — write paths emit no
8155 /// audit entries. When present, single-row writes capture
8156 /// before/after for the listed fields and bulk writes batch
8157 /// snapshots into one INSERT into `rustango_audit_log`.
8158 audit: Option<AuditAttrs>,
8159 /// `true` when `#[rustango(permissions)]` is present. Signals that
8160 /// `auto_create_permissions` should seed the four CRUD codenames for
8161 /// this model.
8162 permissions: bool,
8163 /// Many-to-many relations declared via
8164 /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8165 /// src = "post_id", dst = "tag_id"))]`.
8166 m2m: Vec<M2MAttr>,
8167 /// Polymorphic M2M relations declared via
8168 /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8169 /// pk_column = "taggable_id", ct_column = "taggable_type",
8170 /// related_column = "tag_id"))]` (issue #818).
8171 generic_m2m: Vec<GenericM2MAttr>,
8172 /// Composite indexes declared via
8173 /// `#[rustango(index("col1, col2"))]` or
8174 /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8175 /// Single-column indexes from `#[rustango(index)]` on fields are
8176 /// accumulated here during field collection.
8177 indexes: Vec<IndexAttr>,
8178 /// Table-level CHECK constraints declared via
8179 /// `#[rustango(check(name = "…", expr = "…"))]`.
8180 checks: Vec<CheckAttr>,
8181 /// Table-level PG `EXCLUDE` constraints declared via
8182 /// `#[rustango(exclude(name = "…", using = "gist", elements =
8183 /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8184 /// migration writer renders them on Postgres and skips with a
8185 /// warning on MySQL/SQLite. Issue #319.
8186 excludes: Vec<ExcludeAttr>,
8187 /// Composite (multi-column) FKs declared via
8188 /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8189 /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8190 composite_fks: Vec<CompositeFkAttr>,
8191 /// Generic ("any model") FKs declared via
8192 /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8193 /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8194 generic_fks: Vec<GenericFkAttr>,
8195 /// Where this model lives in a tenancy deployment, declared via
8196 /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8197 /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8198 /// to partition diff output between registry-scoped and
8199 /// tenant-scoped migration files.
8200 scope: Option<String>,
8201 /// Extra QuerySet accessor names from
8202 /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8203 /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8204 /// the default `Self::objects()`. Multiple attributes allowed.
8205 manager_fns: Vec<syn::Ident>,
8206 /// Default ordering declared via `#[rustango(default_order =
8207 /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8208 /// `(column_name, desc_flag, span_for_error_reporting)` — the
8209 /// `-` prefix means descending; the `+` prefix or no prefix means
8210 /// ascending.
8211 default_order: Vec<(String, bool, proc_macro2::Span)>,
8212 /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8213 /// Routes the emitted schema's `is_view = true` so the migration
8214 /// snapshot skips this model (its underlying SQL view is operator-
8215 /// managed, not rustango-managed).
8216 is_view: bool,
8217 /// `#[rustango(managed = false)]` — who owns the table.
8218 /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8219 /// the migration snapshot skips this model so `makemigrations` /
8220 /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8221 /// TABLE` against it (operator-managed schema).
8222 managed: bool,
8223 /// Name of the model's base manager, from
8224 /// `#[rustango(base_manager_name = "...")]`. Threaded into
8225 /// `ModelSchema::base_manager_name`. Declarative-only today.
8226 base_manager_name: Option<String>,
8227 /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8228 /// field this model's instances are ordered relative to.
8229 /// Declarative-only today; threaded onto
8230 /// `ModelSchema::order_with_respect_to`.
8231 order_with_respect_to: Option<String>,
8232 /// `#[rustango(proxy)]` / `#[rustango(proxy = true)]`.
8233 /// Marks the model as a proxy that
8234 /// shares its DB table with another struct. Threaded into
8235 /// `ModelSchema::proxy` so future codegen can skip table-owning
8236 /// behavior for proxies.
8237 proxy: bool,
8238 /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8239 /// Each comma-separated capability token surfaces on
8240 /// `ModelSchema::required_db_features` so `manage check --deploy`
8241 /// can warn when the active dialect lacks one.
8242 required_db_features: Vec<String>,
8243 /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8244 /// Normalized to the dialect name `manage check --deploy`
8245 /// compares against `Settings.database.backend`. Aliases
8246 /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8247 /// stored under the canonical name.
8248 required_db_vendor: Option<String>,
8249 /// `#[rustango(default_related_name = "...")]`. Threaded into
8250 /// `ModelSchema::default_related_name`. Reverse-relation accessor
8251 /// name to use when an FK / M2M field doesn't override it.
8252 /// Today rustango doesn't auto-emit reverse managers; the
8253 /// metadata is the foundation for that work.
8254 default_related_name: Option<String>,
8255 /// `#[rustango(db_table_comment = "...")]`. Threaded into
8256 /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8257 /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8258 /// inline `COMMENT='...'`, SQLite: no-op).
8259 db_table_comment: Option<String>,
8260 /// `#[rustango(get_latest_by = "created_at")]` /
8261 /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8262 /// `(column, descending)` where `descending = true` when the
8263 /// attribute value starts with `-`. Threaded into
8264 /// `ModelSchema::get_latest_by`.
8265 get_latest_by: Option<(String, bool)>,
8266 /// Extra `(codename, label)` permissions
8267 /// from `#[rustango(extra_permissions = "approve:Can approve,
8268 /// archive:Can archive")]`. Comma-separated `codename:label`
8269 /// pairs. Threaded into `ModelSchema::extra_permissions`.
8270 extra_permissions: Vec<(String, String)>,
8271 /// Which CRUD codenames (`"add"` / `"change"` / `"delete"` /
8272 /// `"view"`) the framework
8273 /// auto-creates. Empty `Vec` (default) means **all four**. Set via
8274 /// `#[rustango(default_permissions = "view,change")]` to opt out.
8275 /// Validated at parse time; unknown actions fail with a span-pointing
8276 /// error.
8277 default_permissions: Vec<String>,
8278 /// `#[rustango(verbose_name = "blog post")]` — the
8279 /// human-readable singular label for the model. Threaded into
8280 /// `ModelSchema::verbose_name` so admin section headers /
8281 /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8282 /// over the Rust struct identifier.
8283 verbose_name: Option<String>,
8284 /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8285 /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8286 /// When unset, `display_label_plural()` auto-suffixes `s`.
8287 verbose_name_plural: Option<String>,
8288 /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8289 /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8290 /// a name (used by `QuerySet::without_global_scope`) with a
8291 /// `fn() -> WhereExpr` path that the macro emits into
8292 /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8293 global_scopes: Vec<GlobalScopeAttr>,
8294 /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8295 /// `#[rustango(through(name, far, far_fk_column, intermediate,
8296 /// intermediate_fk_column, intermediate_pk_column))]` — issue
8297 /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8298 /// entry emits an inherent `<name>_through(&self)` accessor that
8299 /// returns a `QuerySet<Far>` filtered via a correlated subquery
8300 /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8301 /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8302 through_relations: Vec<ThroughAttr>,
8303 /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8304 /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8305 /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8306 /// Each entry emits two associated functions on the parent —
8307 /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8308 /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8309 /// over a correlated subquery against the child table. Users
8310 /// drop the result into `QuerySet::where_raw(...)`.
8311 reverse_has_relations: Vec<ReverseHasAttr>,
8312 /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8313 /// issue #830. Each emits a `Model::generic_reverse_relations()`
8314 /// entry so the relation-existence family resolves polymorphic
8315 /// children by name.
8316 generic_has_relations: Vec<GenericHasAttr>,
8317}
8318
8319/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8320/// declaration. Each entry becomes one `core::GlobalScope` in the
8321/// emitted schema literal; `apply` resolves at macro-expand time
8322/// against the consumer's scope so the function must be in scope at
8323/// the use site. Issue #820.
8324struct GlobalScopeAttr {
8325 name: String,
8326 apply: syn::Path,
8327}
8328
8329/// Parsed `#[rustango(through(...))]` declaration. Issue
8330/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8331/// `hasManyThrough` / `hasOneThrough` parity.
8332///
8333/// `Country hasManyThrough Post via User` declares as:
8334///
8335/// ```ignore
8336/// #[rustango(through(
8337/// name = "posts",
8338/// far = "Post",
8339/// far_fk_column = "author_id",
8340/// intermediate = "User",
8341/// intermediate_fk_column = "country_id",
8342/// ))]
8343/// struct Country { ... }
8344/// ```
8345///
8346/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8347/// which returns a queryset filtered via a correlated subquery —
8348/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8349/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8350/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8351/// because the subquery lives inside a `WhereExpr::InSubquery` node
8352/// and the outer queryset's pending list stays empty.
8353///
8354/// All four required arguments use **SQL column / table names**
8355/// (not Rust field names) to sidestep the multi-hop-filter substrate
8356/// gap. Once that substrate lands, a higher-level Rust-field-name
8357/// shorthand can be added without breaking this surface.
8358struct ThroughAttr {
8359 /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8360 name: String,
8361 /// Far model type identifier. `far = "Post"` → returns
8362 /// `QuerySet<Post>`. Resolved verbatim against the scope where
8363 /// the derive expands.
8364 far: syn::Ident,
8365 /// SQL column on the far model's table that references the
8366 /// intermediate model's primary key. For `Post`'s
8367 /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8368 /// default `<field>_id` convention) or whatever the user
8369 /// declared via `#[rustango(db_column = "...")]`.
8370 far_fk_column: String,
8371 /// Intermediate model type identifier. Needed to look up its
8372 /// `SCHEMA` so the subquery's `FROM` clause points at the
8373 /// intermediate table. `intermediate = "User"`.
8374 intermediate: syn::Ident,
8375 /// SQL column on the intermediate's table that references the
8376 /// source (this) model's primary key. For `User`'s
8377 /// `country: ForeignKey<Country>` the column is `"country_id"`.
8378 intermediate_fk_column: String,
8379 /// SQL primary-key column on the intermediate's table — the
8380 /// column the subquery projects. Optional; defaults to `"id"`
8381 /// (rustango's default PK column name). Override when the
8382 /// intermediate declares a custom PK column.
8383 intermediate_pk_column: String,
8384}
8385
8386/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8387/// child_fk_column = "..."))]` declaration. Issue
8388/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8389/// `whereHas` / `whereDoesntHave` parity.
8390///
8391/// `Post hasMany Comment` declares as:
8392///
8393/// ```ignore
8394/// #[rustango(reverse_has(
8395/// name = "comments",
8396/// child = "Comment",
8397/// child_fk_column = "post_id",
8398/// ))]
8399/// struct Post { ... }
8400/// ```
8401///
8402/// The macro emits two associated functions on `Post`:
8403///
8404/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8405/// `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8406/// <outer>.<self_pk_column>)`.
8407/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8408/// but `NOT EXISTS`, the `whereDoesntHave` analog.
8409///
8410/// User code:
8411///
8412/// ```ignore
8413/// // Posts with at least one comment:
8414/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8415/// // Posts with no comments:
8416/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8417/// ```
8418///
8419/// As with #817, all column / table identifiers are **SQL names**
8420/// (not Rust field names) so the substrate is independent of the
8421/// outstanding multi-hop filter resolver gap. The emitted
8422/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8423/// SQL-emit time via the writer's scope stack.
8424struct ReverseHasAttr {
8425 /// Accessor name. `name = "comments"` → emits
8426 /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8427 name: String,
8428 /// Child model type identifier. `child = "Comment"` — needed to
8429 /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8430 /// points at the child table.
8431 child: syn::Ident,
8432 /// SQL column on the child's table that references this model's
8433 /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8434 /// column is `"post_id"`.
8435 child_fk_column: String,
8436 /// SQL primary-key column on **this** model's table — the column
8437 /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8438 /// (rustango's default PK column name).
8439 self_pk_column: String,
8440}
8441
8442/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8443/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8444/// relation-existence family (issue #830). The child is a polymorphic,
8445/// content-type-discriminated model; emits a `Model::
8446/// generic_reverse_relations()` entry the queryset resolves by name.
8447struct GenericHasAttr {
8448 /// Accessor name, e.g. `name = "tags"`.
8449 name: String,
8450 /// Child model type identifier (`child = "Tag"`) — looked up for its
8451 /// `SCHEMA` so the subquery's `FROM` points at the child table.
8452 child: syn::Ident,
8453 /// Column on the child table holding the parent's content-type id.
8454 /// Optional; defaults to `"content_type_id"`.
8455 ct_column: String,
8456 /// Column on the child table holding the parent's PK value.
8457 /// Optional; defaults to `"object_pk"`.
8458 pk_column: String,
8459 /// SQL primary-key column on **this** (parent) model's table.
8460 /// Optional; defaults to `"id"`.
8461 self_pk_column: String,
8462}
8463
8464/// Parsed form of one index declaration (field-level or container-level).
8465struct IndexAttr {
8466 /// Index name; auto-derived when `None` at parse time.
8467 name: Option<String>,
8468 /// Column names in the index.
8469 columns: Vec<String>,
8470 /// `true` for `CREATE UNIQUE INDEX`.
8471 unique: bool,
8472 /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8473 /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8474 /// `"btree"` when the attribute is absent — the DDL writer omits
8475 /// the `USING` clause and the backend uses its own default
8476 /// (btree on every supported dialect).
8477 method: String,
8478 /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8479 /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8480 /// condition = "...", name = "..."))]`. `None` for plain indexes.
8481 where_clause: Option<String>,
8482 /// Covering-index columns (PG 11+ `INCLUDE (...)` clause).
8483 /// Empty `Vec` (the default) means "no covering columns".
8484 include: Vec<String>,
8485}
8486
8487/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8488struct CheckAttr {
8489 name: String,
8490 expr: String,
8491}
8492
8493/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8494/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8495/// PG-only — surfaced on every backend in the macro emit; the migration
8496/// writer skips the op on MySQL/SQLite. Issue #319.
8497struct ExcludeAttr {
8498 /// Constraint name (free-form Rust identifier).
8499 name: String,
8500 /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8501 using: String,
8502 /// Comma-separated `(column, operator)` pairs, in declaration
8503 /// order. Parsed from `"col WITH op, col WITH op"`.
8504 elements: Vec<(String, String)>,
8505 /// Optional `WHERE` predicate (raw SQL).
8506 where_clause: Option<String>,
8507}
8508
8509/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8510/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8511/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8512/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8513/// the model, not the field.
8514struct CompositeFkAttr {
8515 /// Logical relation name (free-form Rust identifier).
8516 name: String,
8517 /// SQL table name of the target.
8518 to: String,
8519 /// Source-side column names, in declaration order.
8520 from: Vec<String>,
8521 /// Target-side column names, same length / order as `from`.
8522 on: Vec<String>,
8523}
8524
8525/// Parsed form of one `#[rustango(generic_fk(name = "target",
8526/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8527/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8528/// generic ("any model") FKs live on the model, not the field.
8529struct GenericFkAttr {
8530 /// Logical relation name (free-form Rust identifier).
8531 name: String,
8532 /// Source-side column carrying the `content_type_id` value.
8533 ct_column: String,
8534 /// Source-side column carrying the target row's primary key.
8535 pk_column: String,
8536}
8537
8538/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8539struct M2MAttr {
8540 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8541 name: String,
8542 /// Target table (e.g. `"app_tags"`).
8543 to: String,
8544 /// Junction table (e.g. `"post_tags"`).
8545 through: String,
8546 /// Source FK column in the junction table (e.g. `"post_id"`).
8547 src: String,
8548 /// Destination FK column in the junction table (e.g. `"tag_id"`).
8549 dst: String,
8550 /// Whether the migration writer should auto-create the junction
8551 /// table. Default `true`. Set `auto_create = false` (#324) when
8552 /// the operator declares the through table as its own
8553 /// `#[derive(Model)]` struct with extra columns.
8554 auto_create: bool,
8555}
8556
8557/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8558/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8559/// junction carries a ContentType discriminator so unrelated models
8560/// share one pivot + related set.
8561struct GenericM2MAttr {
8562 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8563 name: String,
8564 /// Polymorphic junction table (e.g. `"taggables"`).
8565 through: String,
8566 /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8567 pk_column: String,
8568 /// Junction column holding the owning model's `content_type_id`
8569 /// discriminator (e.g. `"taggable_type"`).
8570 ct_column: String,
8571 /// Junction column holding the related model PK (e.g. `"tag_id"`).
8572 related_column: String,
8573}
8574
8575/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8576/// "user"))]`. `track` is a comma-separated list of field names whose
8577/// before/after values land in the JSONB `changes` column. `source`
8578/// is informational only — it pins a default source when the model
8579/// is written outside any `audit::with_source(...)` scope (rare).
8580#[derive(Default)]
8581struct AuditAttrs {
8582 /// Field names to capture in the `changes` JSONB. Validated
8583 /// against declared scalar fields at compile time. Empty means
8584 /// "track every scalar field".
8585 track: Option<(Vec<String>, proc_macro2::Span)>,
8586}
8587
8588/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8589/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8590/// comma-separated strings; we validate each ident against the model's
8591/// declared fields at compile time.
8592#[derive(Default)]
8593struct AdminAttrs {
8594 list_display: Option<(Vec<String>, proc_macro2::Span)>,
8595 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8596 list_per_page: Option<usize>,
8597 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8598 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8599 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8600 /// Bulk action names. No field-validation against model fields —
8601 /// these are action handlers, not column references.
8602 actions: Option<(Vec<String>, proc_macro2::Span)>,
8603 /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8604 /// sections, comma-separated fields per section, optional
8605 /// `Title:` prefix. Empty title omits the `<legend>`.
8606 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8607 /// `admin(list_display_links = "title")`. Names
8608 /// from `list_display` whose cells should link to detail/edit.
8609 /// Issue #350.
8610 list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8611 /// `admin(search_help_text = "...")`. Short
8612 /// caption rendered beside the admin list view's search box.
8613 /// Issue #353.
8614 search_help_text: Option<String>,
8615 /// `admin(actions_on_top = false)`. Hides the
8616 /// action-bar above the table. Default `true`. Issue #354.
8617 actions_on_top: Option<bool>,
8618 /// `admin(actions_on_bottom = true)`. Renders an
8619 /// additional action-bar below the table. Default `false`.
8620 /// Issue #354.
8621 actions_on_bottom: Option<bool>,
8622 /// `admin(date_hierarchy = "created_at")`. Name of
8623 /// a date / datetime field whose values render as a clickable
8624 /// year / month / day drill-down strip above the list table.
8625 /// Empty / unset disables the strip. Issue #355.
8626 date_hierarchy: Option<String>,
8627 /// `admin(prepopulated_fields = "slug:title")`.
8628 /// Each entry is `target:source[+source2]`; multiple entries are
8629 /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8630 /// The admin change-form emits JS that slugifies the source values
8631 /// into the target field on every keystroke. Issue #356.
8632 prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8633 /// `admin(raw_id_fields = "parent, owner")`. Names
8634 /// of FK fields whose change-form widget renders a lookup link
8635 /// next to the input. Issue #357.
8636 raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8637 /// `admin(autocomplete_fields = "author_id")`.
8638 /// Names of FK fields whose change-form widget renders an
8639 /// Ajax-driven typeahead populated from a `__autocomplete`
8640 /// endpoint on the target model. Issue #358.
8641 autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8642 /// `admin(list_select_related = "all" | "none" | "author, …")`
8643 /// — tunes the admin list view's FK auto-JOIN
8644 /// policy. Default `"all"` matches rustango's join-everything
8645 /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8646 list_select_related: Option<String>,
8647 /// `admin(formfield_overrides = "field:widget, field2:widget2")`.
8648 /// Each entry is `field_name:widget_name`; multiple
8649 /// entries comma-separated. Empty / unset → no overrides. The
8650 /// list of widget names supported is documented on
8651 /// `AdminConfig::formfield_overrides`. Issue #359.
8652 formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8653}
8654
8655fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8656 let mut out = ContainerAttrs {
8657 table: None,
8658 display: None,
8659 app: None,
8660 admin: None,
8661 audit: None,
8662 // Default `permissions = true` so every `#[derive(Model)]`
8663 // gets the four CRUD codenames seeded by `auto_create_permissions`
8664 // and is visible to non-superusers in the tenant admin without
8665 // manual per-model annotation. Models that intentionally don't
8666 // want permission rows (registry-internal types, framework
8667 // tables operators shouldn't manage directly) opt out via
8668 // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8669 // out-of-the-box admin invisibility regression (#62).
8670 permissions: true,
8671 m2m: Vec::new(),
8672 generic_m2m: Vec::new(),
8673 indexes: Vec::new(),
8674 checks: Vec::new(),
8675 excludes: Vec::new(),
8676 composite_fks: Vec::new(),
8677 generic_fks: Vec::new(),
8678 scope: None,
8679 manager_fns: Vec::new(),
8680 default_order: Vec::new(),
8681 is_view: false,
8682 managed: true,
8683 verbose_name: None,
8684 verbose_name_plural: None,
8685 base_manager_name: None,
8686 order_with_respect_to: None,
8687 proxy: false,
8688 required_db_features: Vec::new(),
8689 required_db_vendor: None,
8690 default_related_name: None,
8691 db_table_comment: None,
8692 get_latest_by: None,
8693 extra_permissions: Vec::new(),
8694 default_permissions: Vec::new(),
8695 global_scopes: Vec::new(),
8696 through_relations: Vec::new(),
8697 reverse_has_relations: Vec::new(),
8698 generic_has_relations: Vec::new(),
8699 };
8700 for attr in &input.attrs {
8701 if !attr.path().is_ident("rustango") {
8702 continue;
8703 }
8704 attr.parse_nested_meta(|meta| {
8705 if meta.path.is_ident("table") {
8706 let s: LitStr = meta.value()?.parse()?;
8707 let name = s.value();
8708 // v0.27.3 (#65) — macro-time guard against table names
8709 // that compile but break SQL downstream. Hyphens are
8710 // the common footgun: PostgreSQL accepts a quoted
8711 // `"intermediate-region"` in CREATE TABLE, but the
8712 // FK / index name derivation in `migrate::ddl`
8713 // emits `intermediate-region_field_fkey` unquoted,
8714 // which then fails the SQL parser. Same shape rule
8715 // as Postgres regular identifiers so the safe path
8716 // is the only path.
8717 validate_table_name(&name, s.span())?;
8718 out.table = Some(name);
8719 return Ok(());
8720 }
8721 if meta.path.is_ident("display") {
8722 let s: LitStr = meta.value()?.parse()?;
8723 out.display = Some((s.value(), s.span()));
8724 return Ok(());
8725 }
8726 if meta.path.is_ident("app") {
8727 let s: LitStr = meta.value()?.parse()?;
8728 out.app = Some(s.value());
8729 return Ok(());
8730 }
8731 if meta.path.is_ident("scope") {
8732 let s: LitStr = meta.value()?.parse()?;
8733 let val = s.value();
8734 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8735 return Err(meta.error(format!(
8736 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8737 )));
8738 }
8739 out.scope = Some(val);
8740 return Ok(());
8741 }
8742 if meta.path.is_ident("admin") {
8743 let mut admin = AdminAttrs::default();
8744 meta.parse_nested_meta(|inner| {
8745 if inner.path.is_ident("list_display") {
8746 let s: LitStr = inner.value()?.parse()?;
8747 admin.list_display =
8748 Some((split_field_list(&s.value()), s.span()));
8749 return Ok(());
8750 }
8751 if inner.path.is_ident("search_fields") {
8752 let s: LitStr = inner.value()?.parse()?;
8753 admin.search_fields =
8754 Some((split_field_list(&s.value()), s.span()));
8755 return Ok(());
8756 }
8757 if inner.path.is_ident("readonly_fields") {
8758 let s: LitStr = inner.value()?.parse()?;
8759 admin.readonly_fields =
8760 Some((split_field_list(&s.value()), s.span()));
8761 return Ok(());
8762 }
8763 if inner.path.is_ident("list_per_page") {
8764 let lit: syn::LitInt = inner.value()?.parse()?;
8765 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8766 return Ok(());
8767 }
8768 if inner.path.is_ident("ordering") {
8769 let s: LitStr = inner.value()?.parse()?;
8770 admin.ordering = Some((
8771 parse_ordering_list(&s.value()),
8772 s.span(),
8773 ));
8774 return Ok(());
8775 }
8776 if inner.path.is_ident("list_filter") {
8777 let s: LitStr = inner.value()?.parse()?;
8778 admin.list_filter =
8779 Some((split_field_list(&s.value()), s.span()));
8780 return Ok(());
8781 }
8782 if inner.path.is_ident("actions") {
8783 let s: LitStr = inner.value()?.parse()?;
8784 admin.actions =
8785 Some((split_field_list(&s.value()), s.span()));
8786 return Ok(());
8787 }
8788 if inner.path.is_ident("fieldsets") {
8789 let s: LitStr = inner.value()?.parse()?;
8790 admin.fieldsets =
8791 Some((parse_fieldset_list(&s.value()), s.span()));
8792 return Ok(());
8793 }
8794 if inner.path.is_ident("list_display_links") {
8795 let s: LitStr = inner.value()?.parse()?;
8796 admin.list_display_links =
8797 Some((split_field_list(&s.value()), s.span()));
8798 return Ok(());
8799 }
8800 if inner.path.is_ident("search_help_text") {
8801 let s: LitStr = inner.value()?.parse()?;
8802 admin.search_help_text = Some(s.value());
8803 return Ok(());
8804 }
8805 if inner.path.is_ident("actions_on_top") {
8806 let lit: syn::LitBool = inner.value()?.parse()?;
8807 admin.actions_on_top = Some(lit.value);
8808 return Ok(());
8809 }
8810 if inner.path.is_ident("actions_on_bottom") {
8811 let lit: syn::LitBool = inner.value()?.parse()?;
8812 admin.actions_on_bottom = Some(lit.value);
8813 return Ok(());
8814 }
8815 if inner.path.is_ident("date_hierarchy") {
8816 let s: LitStr = inner.value()?.parse()?;
8817 admin.date_hierarchy = Some(s.value());
8818 return Ok(());
8819 }
8820 if inner.path.is_ident("prepopulated_fields") {
8821 let s: LitStr = inner.value()?.parse()?;
8822 admin.prepopulated_fields =
8823 Some((parse_prepopulated_list(&s.value()), s.span()));
8824 return Ok(());
8825 }
8826 if inner.path.is_ident("raw_id_fields") {
8827 let s: LitStr = inner.value()?.parse()?;
8828 admin.raw_id_fields =
8829 Some((split_field_list(&s.value()), s.span()));
8830 return Ok(());
8831 }
8832 if inner.path.is_ident("autocomplete_fields") {
8833 let s: LitStr = inner.value()?.parse()?;
8834 admin.autocomplete_fields =
8835 Some((split_field_list(&s.value()), s.span()));
8836 return Ok(());
8837 }
8838 if inner.path.is_ident("list_select_related") {
8839 let s: LitStr = inner.value()?.parse()?;
8840 admin.list_select_related = Some(s.value());
8841 return Ok(());
8842 }
8843 if inner.path.is_ident("formfield_overrides") {
8844 let s: LitStr = inner.value()?.parse()?;
8845 admin.formfield_overrides =
8846 Some((parse_formfield_overrides(&s.value()), s.span()));
8847 return Ok(());
8848 }
8849 Err(inner.error(
8850 "unknown admin attribute (supported: \
8851 `list_display`, `list_display_links`, \
8852 `search_fields`, `search_help_text`, \
8853 `readonly_fields`, \
8854 `list_filter`, `list_per_page`, `ordering`, `actions`, \
8855 `actions_on_top`, `actions_on_bottom`, \
8856 `date_hierarchy`, \
8857 `prepopulated_fields`, \
8858 `raw_id_fields`, \
8859 `autocomplete_fields`, \
8860 `list_select_related`, \
8861 `formfield_overrides`, \
8862 `fieldsets`)",
8863 ))
8864 })?;
8865 out.admin = Some(admin);
8866 return Ok(());
8867 }
8868 if meta.path.is_ident("manager") {
8869 // Its empty trait could not take methods (#2132).
8870 return Err(meta.error(
8871 "`manager(ext = ...)` was removed (#2132): declare your own \
8872 `trait FooManagerExt: Sized { … }` and implement it for `QuerySet<Foo>`",
8873 ));
8874 }
8875 if meta.path.is_ident("manager_fn") {
8876 // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8877 // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8878 // next to the default `Self::objects()`. Multiple
8879 // attributes accumulate.
8880 let s: LitStr = meta.value()?.parse()?;
8881 let name = s.value();
8882 if name.is_empty() {
8883 return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8884 }
8885 if name == "objects" {
8886 return Err(meta.error(
8887 "`manager_fn = \"objects\"` collides with the default \
8888 accessor — pick a different name",
8889 ));
8890 }
8891 let ident = syn::Ident::new(&name, s.span());
8892 if out.manager_fns.iter().any(|prev| *prev == ident) {
8893 return Err(meta.error(format!(
8894 "duplicate `manager_fn = \"{name}\"`"
8895 )));
8896 }
8897 out.manager_fns.push(ident);
8898 return Ok(());
8899 }
8900 if meta.path.is_ident("default_order") {
8901 // `#[rustango(default_order = "-created_at, status")]`
8902 // — issue #291 / T2.5. Comma-separated list; `-prefix`
8903 // means descending, `+prefix` or bare name means ascending.
8904 // Per-query opt-in via `QuerySet::with_default_order()`.
8905 let s: LitStr = meta.value()?.parse()?;
8906 let raw = s.value();
8907 let span = s.span();
8908 let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
8909 Vec::new();
8910 for entry in raw.split(',') {
8911 let trimmed = entry.trim();
8912 if trimmed.is_empty() {
8913 return Err(syn::Error::new(
8914 span,
8915 "`default_order = \"...\"` has an empty entry — \
8916 check for a stray comma",
8917 ));
8918 }
8919 let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
8920 (true, rest.trim().to_owned())
8921 } else if let Some(rest) = trimmed.strip_prefix('+') {
8922 (false, rest.trim().to_owned())
8923 } else {
8924 (false, trimmed.to_owned())
8925 };
8926 if name.is_empty() {
8927 return Err(syn::Error::new(
8928 span,
8929 "`default_order` entry has no column name after the prefix",
8930 ));
8931 }
8932 if parsed.iter().any(|(n, _, _)| *n == name) {
8933 return Err(syn::Error::new(
8934 span,
8935 format!("duplicate column `{name}` in `default_order`"),
8936 ));
8937 }
8938 parsed.push((name, desc, span));
8939 }
8940 if parsed.is_empty() {
8941 return Err(syn::Error::new(
8942 span,
8943 "`default_order = \"...\"` cannot be empty",
8944 ));
8945 }
8946 out.default_order = parsed;
8947 return Ok(());
8948 }
8949 if meta.path.is_ident("global_scope") {
8950 // `#[rustango(global_scope(name = "active", apply =
8951 // path::to::fn))]` — issue #820. The apply function
8952 // path resolves at macro-expand time in the consumer's
8953 // scope; the macro re-emits it verbatim into the
8954 // `ModelSchema::global_scopes` slice literal.
8955 let span = meta.path.span();
8956 let mut scope_name: Option<String> = None;
8957 let mut apply_path: Option<syn::Path> = None;
8958 meta.parse_nested_meta(|inner| {
8959 if inner.path.is_ident("name") {
8960 let s: LitStr = inner.value()?.parse()?;
8961 let raw = s.value();
8962 if raw.trim().is_empty() {
8963 return Err(syn::Error::new(
8964 s.span(),
8965 "`global_scope(name = \"...\")` must not be empty",
8966 ));
8967 }
8968 scope_name = Some(raw);
8969 return Ok(());
8970 }
8971 if inner.path.is_ident("apply") {
8972 let p: syn::Path = inner.value()?.parse()?;
8973 apply_path = Some(p);
8974 return Ok(());
8975 }
8976 Err(inner.error(
8977 "unknown `global_scope` attribute (supported: \
8978 `name`, `apply`)",
8979 ))
8980 })?;
8981 let Some(name) = scope_name else {
8982 return Err(syn::Error::new(
8983 span,
8984 "`global_scope` requires `name = \"...\"`",
8985 ));
8986 };
8987 let Some(apply) = apply_path else {
8988 return Err(syn::Error::new(
8989 span,
8990 "`global_scope` requires `apply = fn_path`",
8991 ));
8992 };
8993 if out.global_scopes.iter().any(|s| s.name == name) {
8994 return Err(syn::Error::new(
8995 span,
8996 format!(
8997 "duplicate global scope name `{name}` — \
8998 pick a unique identifier so \
8999 `QuerySet::without_global_scope(\"{name}\")` \
9000 is unambiguous"
9001 ),
9002 ));
9003 }
9004 out.global_scopes.push(GlobalScopeAttr { name, apply });
9005 return Ok(());
9006 }
9007 if meta.path.is_ident("through") {
9008 // `#[rustango(through(name = "posts", far = "Post",
9009 // far_fk_column = "author_id", intermediate = "User",
9010 // intermediate_fk_column = "country_id"
9011 // [, intermediate_pk_column = "id"]))]` — issue #817.
9012 let span = meta.path.span();
9013 let mut accessor_name: Option<String> = None;
9014 let mut far_ident: Option<syn::Ident> = None;
9015 let mut far_fk_column: Option<String> = None;
9016 let mut intermediate_ident: Option<syn::Ident> = None;
9017 let mut intermediate_fk_column: Option<String> = None;
9018 let mut intermediate_pk_column: Option<String> = None;
9019 fn parse_nonempty_string(
9020 inner: &syn::meta::ParseNestedMeta<'_>,
9021 field: &str,
9022 ) -> syn::Result<String> {
9023 let s: LitStr = inner.value()?.parse()?;
9024 let raw = s.value();
9025 let trimmed = raw.trim();
9026 if trimmed.is_empty() {
9027 return Err(syn::Error::new(
9028 s.span(),
9029 format!("`through({field} = \"...\")` must not be empty"),
9030 ));
9031 }
9032 Ok(trimmed.to_owned())
9033 }
9034 meta.parse_nested_meta(|inner| {
9035 if inner.path.is_ident("name") {
9036 accessor_name = Some(parse_nonempty_string(&inner, "name")?);
9037 return Ok(());
9038 }
9039 if inner.path.is_ident("far") {
9040 // Far model name accepted as a string literal
9041 // so the attribute fits inside the existing
9042 // `parse_nested_meta` shape; parsed back into a
9043 // `syn::Ident` so the emitted accessor can
9044 // reference the type directly.
9045 let s: LitStr = inner.value()?.parse()?;
9046 let raw = s.value();
9047 let trimmed = raw.trim();
9048 if trimmed.is_empty() {
9049 return Err(syn::Error::new(
9050 s.span(),
9051 "`through(far = \"...\")` must not be empty",
9052 ));
9053 }
9054 far_ident = Some(syn::Ident::new(trimmed, s.span()));
9055 return Ok(());
9056 }
9057 if inner.path.is_ident("far_fk_column") {
9058 far_fk_column =
9059 Some(parse_nonempty_string(&inner, "far_fk_column")?);
9060 return Ok(());
9061 }
9062 if inner.path.is_ident("intermediate") {
9063 let s: LitStr = inner.value()?.parse()?;
9064 let raw = s.value();
9065 let trimmed = raw.trim();
9066 if trimmed.is_empty() {
9067 return Err(syn::Error::new(
9068 s.span(),
9069 "`through(intermediate = \"...\")` must not be empty",
9070 ));
9071 }
9072 intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9073 return Ok(());
9074 }
9075 if inner.path.is_ident("intermediate_fk_column") {
9076 intermediate_fk_column =
9077 Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9078 return Ok(());
9079 }
9080 if inner.path.is_ident("intermediate_pk_column") {
9081 intermediate_pk_column =
9082 Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9083 return Ok(());
9084 }
9085 Err(inner.error(
9086 "unknown `through` attribute (supported: \
9087 `name`, `far`, `far_fk_column`, \
9088 `intermediate`, `intermediate_fk_column`, \
9089 `intermediate_pk_column`)",
9090 ))
9091 })?;
9092 let Some(name) = accessor_name else {
9093 return Err(syn::Error::new(
9094 span,
9095 "`through` requires `name = \"...\"`",
9096 ));
9097 };
9098 let Some(far) = far_ident else {
9099 return Err(syn::Error::new(
9100 span,
9101 "`through` requires `far = \"FarModelType\"`",
9102 ));
9103 };
9104 let Some(far_fk_column) = far_fk_column else {
9105 return Err(syn::Error::new(
9106 span,
9107 "`through` requires `far_fk_column = \"<column>\"`",
9108 ));
9109 };
9110 let Some(intermediate) = intermediate_ident else {
9111 return Err(syn::Error::new(
9112 span,
9113 "`through` requires `intermediate = \"IntermediateModelType\"`",
9114 ));
9115 };
9116 let Some(intermediate_fk_column) = intermediate_fk_column else {
9117 return Err(syn::Error::new(
9118 span,
9119 "`through` requires `intermediate_fk_column = \"<column>\"`",
9120 ));
9121 };
9122 let intermediate_pk_column =
9123 intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9124 if out.through_relations.iter().any(|t| t.name == name) {
9125 return Err(syn::Error::new(
9126 span,
9127 format!(
9128 "duplicate `through(name = \"{name}\")` — \
9129 pick a unique accessor name"
9130 ),
9131 ));
9132 }
9133 out.through_relations.push(ThroughAttr {
9134 name,
9135 far,
9136 far_fk_column,
9137 intermediate,
9138 intermediate_fk_column,
9139 intermediate_pk_column,
9140 });
9141 return Ok(());
9142 }
9143 if meta.path.is_ident("reverse_has") {
9144 // `#[rustango(reverse_has(name = "comments",
9145 // child = "Comment", child_fk_column = "post_id"
9146 // [, self_pk_column = "id"]))]` — issue #830.
9147 let span = meta.path.span();
9148 let mut accessor_name: Option<String> = None;
9149 let mut child_ident: Option<syn::Ident> = None;
9150 let mut child_fk_column: Option<String> = None;
9151 let mut self_pk_column: Option<String> = None;
9152 meta.parse_nested_meta(|inner| {
9153 if inner.path.is_ident("name") {
9154 let s: LitStr = inner.value()?.parse()?;
9155 let raw = s.value();
9156 if raw.trim().is_empty() {
9157 return Err(syn::Error::new(
9158 s.span(),
9159 "`reverse_has(name = \"...\")` must not be empty",
9160 ));
9161 }
9162 accessor_name = Some(raw);
9163 return Ok(());
9164 }
9165 if inner.path.is_ident("child") {
9166 let s: LitStr = inner.value()?.parse()?;
9167 let raw = s.value();
9168 let trimmed = raw.trim();
9169 if trimmed.is_empty() {
9170 return Err(syn::Error::new(
9171 s.span(),
9172 "`reverse_has(child = \"...\")` must not be empty",
9173 ));
9174 }
9175 child_ident = Some(syn::Ident::new(trimmed, s.span()));
9176 return Ok(());
9177 }
9178 if inner.path.is_ident("child_fk_column") {
9179 let s: LitStr = inner.value()?.parse()?;
9180 let raw = s.value();
9181 let trimmed = raw.trim();
9182 if trimmed.is_empty() {
9183 return Err(syn::Error::new(
9184 s.span(),
9185 "`reverse_has(child_fk_column = \"...\")` must not be empty",
9186 ));
9187 }
9188 child_fk_column = Some(trimmed.to_owned());
9189 return Ok(());
9190 }
9191 if inner.path.is_ident("self_pk_column") {
9192 let s: LitStr = inner.value()?.parse()?;
9193 let raw = s.value();
9194 let trimmed = raw.trim();
9195 if trimmed.is_empty() {
9196 return Err(syn::Error::new(
9197 s.span(),
9198 "`reverse_has(self_pk_column = \"...\")` must not be empty",
9199 ));
9200 }
9201 self_pk_column = Some(trimmed.to_owned());
9202 return Ok(());
9203 }
9204 Err(inner.error(
9205 "unknown `reverse_has` attribute (supported: \
9206 `name`, `child`, `child_fk_column`, \
9207 `self_pk_column`)",
9208 ))
9209 })?;
9210 let Some(name) = accessor_name else {
9211 return Err(syn::Error::new(
9212 span,
9213 "`reverse_has` requires `name = \"...\"`",
9214 ));
9215 };
9216 let Some(child) = child_ident else {
9217 return Err(syn::Error::new(
9218 span,
9219 "`reverse_has` requires `child = \"ChildModelType\"`",
9220 ));
9221 };
9222 let Some(child_fk_column) = child_fk_column else {
9223 return Err(syn::Error::new(
9224 span,
9225 "`reverse_has` requires `child_fk_column = \"<column>\"`",
9226 ));
9227 };
9228 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9229 if out.reverse_has_relations.iter().any(|r| r.name == name) {
9230 return Err(syn::Error::new(
9231 span,
9232 format!(
9233 "duplicate `reverse_has(name = \"{name}\")` — \
9234 pick a unique accessor name"
9235 ),
9236 ));
9237 }
9238 out.reverse_has_relations.push(ReverseHasAttr {
9239 name,
9240 child,
9241 child_fk_column,
9242 self_pk_column,
9243 });
9244 return Ok(());
9245 }
9246 if meta.path.is_ident("generic_has") {
9247 // `#[rustango(generic_has(name = "tags",
9248 // child = "Tag", ct_column = "content_type_id",
9249 // pk_column = "object_pk" [, self_pk_column = "id"]))]`
9250 // — issue #830, the reverse generic-FK (GFK) arm of the
9251 // relation-existence family.
9252 let span = meta.path.span();
9253 let mut accessor_name: Option<String> = None;
9254 let mut child_ident: Option<syn::Ident> = None;
9255 let mut ct_column: Option<String> = None;
9256 let mut pk_column: Option<String> = None;
9257 let mut self_pk_column: Option<String> = None;
9258 meta.parse_nested_meta(|inner| {
9259 if inner.path.is_ident("name") {
9260 let s: LitStr = inner.value()?.parse()?;
9261 let raw = s.value();
9262 if raw.trim().is_empty() {
9263 return Err(syn::Error::new(
9264 s.span(),
9265 "`generic_has(name = \"...\")` must not be empty",
9266 ));
9267 }
9268 accessor_name = Some(raw);
9269 return Ok(());
9270 }
9271 if inner.path.is_ident("child") {
9272 let s: LitStr = inner.value()?.parse()?;
9273 let trimmed = s.value().trim().to_owned();
9274 if trimmed.is_empty() {
9275 return Err(syn::Error::new(
9276 s.span(),
9277 "`generic_has(child = \"...\")` must not be empty",
9278 ));
9279 }
9280 child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9281 return Ok(());
9282 }
9283 if inner.path.is_ident("ct_column") {
9284 let s: LitStr = inner.value()?.parse()?;
9285 let trimmed = s.value().trim().to_owned();
9286 if trimmed.is_empty() {
9287 return Err(syn::Error::new(
9288 s.span(),
9289 "`generic_has(ct_column = \"...\")` must not be empty",
9290 ));
9291 }
9292 ct_column = Some(trimmed);
9293 return Ok(());
9294 }
9295 if inner.path.is_ident("pk_column") {
9296 let s: LitStr = inner.value()?.parse()?;
9297 let trimmed = s.value().trim().to_owned();
9298 if trimmed.is_empty() {
9299 return Err(syn::Error::new(
9300 s.span(),
9301 "`generic_has(pk_column = \"...\")` must not be empty",
9302 ));
9303 }
9304 pk_column = Some(trimmed);
9305 return Ok(());
9306 }
9307 if inner.path.is_ident("self_pk_column") {
9308 let s: LitStr = inner.value()?.parse()?;
9309 let trimmed = s.value().trim().to_owned();
9310 if trimmed.is_empty() {
9311 return Err(syn::Error::new(
9312 s.span(),
9313 "`generic_has(self_pk_column = \"...\")` must not be empty",
9314 ));
9315 }
9316 self_pk_column = Some(trimmed);
9317 return Ok(());
9318 }
9319 Err(inner.error(
9320 "unknown `generic_has` attribute (supported: \
9321 `name`, `child`, `ct_column`, `pk_column`, \
9322 `self_pk_column`)",
9323 ))
9324 })?;
9325 let Some(name) = accessor_name else {
9326 return Err(syn::Error::new(
9327 span,
9328 "`generic_has` requires `name = \"...\"`",
9329 ));
9330 };
9331 let Some(child) = child_ident else {
9332 return Err(syn::Error::new(
9333 span,
9334 "`generic_has` requires `child = \"ChildModelType\"`",
9335 ));
9336 };
9337 let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9338 let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9339 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9340 if out.generic_has_relations.iter().any(|r| r.name == name) {
9341 return Err(syn::Error::new(
9342 span,
9343 format!(
9344 "duplicate `generic_has(name = \"{name}\")` — \
9345 pick a unique accessor name"
9346 ),
9347 ));
9348 }
9349 out.generic_has_relations.push(GenericHasAttr {
9350 name,
9351 child,
9352 ct_column,
9353 pk_column,
9354 self_pk_column,
9355 });
9356 return Ok(());
9357 }
9358 if meta.path.is_ident("audit") {
9359 let mut audit = AuditAttrs::default();
9360 meta.parse_nested_meta(|inner| {
9361 if inner.path.is_ident("track") {
9362 let s: LitStr = inner.value()?.parse()?;
9363 audit.track =
9364 Some((split_field_list(&s.value()), s.span()));
9365 return Ok(());
9366 }
9367 Err(inner.error(
9368 "unknown audit attribute (supported: `track`)",
9369 ))
9370 })?;
9371 out.audit = Some(audit);
9372 return Ok(());
9373 }
9374 if meta.path.is_ident("permissions") {
9375 // Two forms accepted:
9376 // #[rustango(permissions)] — flag form, true
9377 // #[rustango(permissions = false)] — explicit opt-out
9378 // #[rustango(permissions = true)] — explicit opt-in
9379 if let Ok(v) = meta.value() {
9380 let lit: syn::LitBool = v.parse()?;
9381 out.permissions = lit.value;
9382 } else {
9383 out.permissions = true;
9384 }
9385 return Ok(());
9386 }
9387 if meta.path.is_ident("view") {
9388 // Issue #293 / T2.10. Two forms accepted, matching
9389 // the `permissions` flag pattern:
9390 // #[rustango(view)] — flag form, true
9391 // #[rustango(view = false)] — explicit opt-out
9392 // #[rustango(view = true)] — explicit opt-in
9393 if let Ok(v) = meta.value() {
9394 let lit: syn::LitBool = v.parse()?;
9395 out.is_view = lit.value;
9396 } else {
9397 out.is_view = true;
9398 }
9399 return Ok(());
9400 }
9401 if meta.path.is_ident("managed") {
9402 // Who owns the table. Issue #321.
9403 // #[rustango(managed = false)] — operator-managed table
9404 // #[rustango(managed = true)] — rustango-managed (the default)
9405 // Bare-flag form is intentionally not accepted: writing
9406 // `#[rustango(managed)]` reads as "yes please manage it"
9407 // which is already the default. The opt-out is the only
9408 // useful state, so it must be explicit.
9409 let v = meta.value()?;
9410 let lit: syn::LitBool = v.parse()?;
9411 out.managed = lit.value;
9412 return Ok(());
9413 }
9414 if meta.path.is_ident("verbose_name") {
9415 let s: LitStr = meta.value()?.parse()?;
9416 out.verbose_name = Some(s.value());
9417 return Ok(());
9418 }
9419 if meta.path.is_ident("verbose_name_plural") {
9420 let s: LitStr = meta.value()?.parse()?;
9421 out.verbose_name_plural = Some(s.value());
9422 return Ok(());
9423 }
9424 if meta.path.is_ident("db_table_comment") {
9425 // Free-form table-level comment attached to the DB
9426 // catalog.
9427 let s: LitStr = meta.value()?.parse()?;
9428 out.db_table_comment = Some(s.value());
9429 return Ok(());
9430 }
9431 if meta.path.is_ident("proxy") {
9432 // Declarative flag
9433 // marking the struct as a proxy of another model that
9434 // shares its DB table. Stored on `ModelSchema::proxy`
9435 // so future codegen can skip `CreateTable` emission
9436 // for proxies (parent owns the table) and route
9437 // per-instance method resolution to the proxy class.
9438 //
9439 // Accepts `proxy` (bare → true) and `proxy = true/false`.
9440 let value = if meta.input.peek(syn::Token![=]) {
9441 meta.value()?.parse::<syn::LitBool>()?.value
9442 } else {
9443 true
9444 };
9445 out.proxy = value;
9446 return Ok(());
9447 }
9448 if meta.path.is_ident("order_with_respect_to") {
9449 // `order_with_respect_to = "parent_fk"` — the model's
9450 // instances are intrinsically ordered relative to
9451 // their parent FK.
9452 //
9453 // rustango stores the FK field name on
9454 // `ModelSchema::order_with_respect_to`. Declarative-only
9455 // today: the migration writer + admin surfaces still
9456 // treat every model identically. Future codegen will
9457 // key off the metadata to auto-emit the `_order`
9458 // column and reorder helpers.
9459 //
9460 // Validated as a Rust-shape identifier so the macro
9461 // can reject typos at derive time.
9462 let s: LitStr = meta.value()?.parse()?;
9463 let raw = s.value();
9464 if raw.is_empty() {
9465 return Err(syn::Error::new(
9466 s.span(),
9467 "`order_with_respect_to` must be a non-empty FK field name",
9468 ));
9469 }
9470 let valid = raw
9471 .chars()
9472 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9473 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9474 if !valid {
9475 return Err(syn::Error::new(
9476 s.span(),
9477 format!(
9478 "`order_with_respect_to` must be a valid Rust \
9479 identifier (letters / digits / underscores, \
9480 not starting with a digit); got `{raw}`"
9481 ),
9482 ));
9483 }
9484 out.order_with_respect_to = Some(raw);
9485 return Ok(());
9486 }
9487 if meta.path.is_ident("required_db_features") {
9488 // Capability tokens the model needs (e.g.
9489 // `"json_extract"`, `"window_functions"`,
9490 // `"row_security"`). Comma-separated.
9491 // `manage check --deploy` walks every model and warns
9492 // when the active backend doesn't advertise the
9493 // capability.
9494 //
9495 // rustango ships a small registry of capability tokens
9496 // each dialect supports — see `Dialect::supports`.
9497 // Unknown tokens still parse (they end up on the
9498 // schema and show up in the warning) so projects can
9499 // declare aspirational capabilities and the check
9500 // verb will keep nagging until the dialect implements
9501 // them.
9502 let s: LitStr = meta.value()?.parse()?;
9503 let raw = s.value();
9504 let features: Vec<String> = raw
9505 .split(',')
9506 .map(str::trim)
9507 .filter(|s| !s.is_empty())
9508 .map(str::to_owned)
9509 .collect();
9510 if features.is_empty() {
9511 return Err(syn::Error::new(
9512 s.span(),
9513 "`required_db_features` must list at least one \
9514 comma-separated capability token",
9515 ));
9516 }
9517 out.required_db_features = features;
9518 return Ok(());
9519 }
9520 if meta.path.is_ident("required_db_vendor") {
9521 // The model is only meant to run against the named DB
9522 // backend. `manage check --deploy` flags a mismatch so
9523 // ops catches "I forgot to switch DATABASE_URL" at
9524 // deploy time rather than runtime.
9525 //
9526 // Restricted to the three backends rustango ships
9527 // dialects for so the check verb can compare reliably.
9528 let s: LitStr = meta.value()?.parse()?;
9529 let raw = s.value().to_ascii_lowercase();
9530 match raw.as_str() {
9531 "postgresql" | "postgres" | "pg" => {
9532 out.required_db_vendor = Some("postgres".to_owned());
9533 }
9534 "mysql" | "mariadb" => {
9535 out.required_db_vendor = Some("mysql".to_owned());
9536 }
9537 "sqlite" | "sqlite3" => {
9538 out.required_db_vendor = Some("sqlite".to_owned());
9539 }
9540 _ => {
9541 return Err(syn::Error::new(
9542 s.span(),
9543 format!(
9544 "unknown required_db_vendor `{raw}` — \
9545 expected `postgres` (aliases: `postgresql`, `pg`), \
9546 `mysql` (alias: `mariadb`), or `sqlite` \
9547 (alias: `sqlite3`)"
9548 ),
9549 ));
9550 }
9551 }
9552 return Ok(());
9553 }
9554 if meta.path.is_ident("base_manager_name") {
9555 // Name of the manager that `<instance>.<relation>_set`
9556 // uses when resolving reverse-relation managers.
9557 // Distinct from `default_manager_name` (what
9558 // `Self::objects()` returns). Stored on
9559 // `ModelSchema::base_manager_name`.
9560 //
9561 // Validated as a Rust identifier so it stays safe to
9562 // re-emit as code in future reverse-manager codegen.
9563 let s: LitStr = meta.value()?.parse()?;
9564 let raw = s.value();
9565 if raw.is_empty() {
9566 return Err(syn::Error::new(
9567 s.span(),
9568 "`base_manager_name` must be a non-empty string",
9569 ));
9570 }
9571 let valid = raw
9572 .chars()
9573 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9574 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9575 if !valid {
9576 return Err(syn::Error::new(
9577 s.span(),
9578 format!(
9579 "`base_manager_name` must be a valid Rust \
9580 identifier (letters / digits / underscores, \
9581 not starting with a digit); got `{raw}`"
9582 ),
9583 ));
9584 }
9585 out.base_manager_name = Some(raw);
9586 return Ok(());
9587 }
9588 if meta.path.is_ident("default_related_name") {
9589 // The name reverse-relation accessors use when callers
9590 // don't override `related_name = "..."` on the FK / M2M
9591 // field. Stored on `ModelSchema::default_related_name`
9592 // so future reverse-manager codegen / schema emit /
9593 // admin templates can pick the right accessor name
9594 // (today rustango doesn't auto-emit reverse managers;
9595 // the metadata is the foundation for that work).
9596 //
9597 // Enforced non-empty + ASCII identifier-shape so the
9598 // string is safe to use as a Rust ident later.
9599 let s: LitStr = meta.value()?.parse()?;
9600 let raw = s.value();
9601 if raw.is_empty() {
9602 return Err(syn::Error::new(
9603 s.span(),
9604 "`default_related_name` must be a non-empty string",
9605 ));
9606 }
9607 let valid = raw
9608 .chars()
9609 .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9610 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9611 if !valid {
9612 return Err(syn::Error::new(
9613 s.span(),
9614 format!(
9615 "`default_related_name` must be snake_case ASCII \
9616 (lowercase letters / digits / underscores, not \
9617 starting with a digit); got `{raw}`"
9618 ),
9619 ));
9620 }
9621 out.default_related_name = Some(raw);
9622 return Ok(());
9623 }
9624 if meta.path.is_ident("extra_permissions") {
9625 // Comma-separated `codename:label` pairs.
9626 let s: LitStr = meta.value()?.parse()?;
9627 let raw = s.value();
9628 let mut pairs = Vec::new();
9629 for entry in raw.split(',') {
9630 let entry = entry.trim();
9631 if entry.is_empty() {
9632 continue;
9633 }
9634 let (codename, label) = match entry.split_once(':') {
9635 Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9636 None => (entry.to_owned(), entry.to_owned()),
9637 };
9638 if codename.is_empty() {
9639 return Err(meta.error(
9640 "`extra_permissions` entries must be `codename:label` pairs",
9641 ));
9642 }
9643 pairs.push((codename, label));
9644 }
9645 if pairs.is_empty() {
9646 return Err(meta
9647 .error("`extra_permissions = \"…\"` must list at least one pair"));
9648 }
9649 out.extra_permissions = pairs;
9650 return Ok(());
9651 }
9652 if meta.path.is_ident("default_permissions") {
9653 // Comma-separated subset of the CRUD action set.
9654 // Empty means all four (the framework default).
9655 let s: LitStr = meta.value()?.parse()?;
9656 let raw = s.value();
9657 let mut actions: Vec<String> = Vec::new();
9658 for entry in raw.split(',') {
9659 let action = entry.trim().to_ascii_lowercase();
9660 if action.is_empty() {
9661 continue;
9662 }
9663 match action.as_str() {
9664 "add" | "change" | "delete" | "view" => {}
9665 other => {
9666 return Err(syn::Error::new(
9667 s.span(),
9668 format!(
9669 "unknown default_permissions action `{other}` — \
9670 expected one of `add`, `change`, `delete`, `view`"
9671 ),
9672 ));
9673 }
9674 }
9675 if !actions.contains(&action) {
9676 actions.push(action);
9677 }
9678 }
9679 if actions.is_empty() {
9680 return Err(syn::Error::new(
9681 s.span(),
9682 "`default_permissions = \"…\"` must list at least one action; \
9683 use `permissions = false` on the container if you want NO \
9684 permissions seeded for this model.",
9685 ));
9686 }
9687 out.default_permissions = actions;
9688 return Ok(());
9689 }
9690 if meta.path.is_ident("get_latest_by") {
9691 // The `-` prefix selects descending order.
9692 let s: LitStr = meta.value()?.parse()?;
9693 let raw = s.value();
9694 let trimmed = raw.trim();
9695 if trimmed.is_empty() {
9696 return Err(meta.error("`get_latest_by` must name a column"));
9697 }
9698 let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9699 (stripped.to_owned(), true)
9700 } else if let Some(stripped) = trimmed.strip_prefix('+') {
9701 (stripped.to_owned(), false)
9702 } else {
9703 (trimmed.to_owned(), false)
9704 };
9705 if col.is_empty() {
9706 return Err(meta.error("`get_latest_by` must name a column"));
9707 }
9708 out.get_latest_by = Some((col, desc));
9709 return Ok(());
9710 }
9711 if meta.path.is_ident("unique_together") {
9712 // Composite UNIQUE index. Two syntaxes:
9713 //
9714 // #[rustango(unique_together = "org_id, user_id")] — auto-derived name
9715 // #[rustango(unique_together(columns = "org_id, user_id", name = "x"))] — explicit name
9716 //
9717 // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9718 // (col1, col2)`, where <name> defaults to
9719 // `<table>_<col1>_<col2>_uq` when not supplied.
9720 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9721 out.indexes.push(IndexAttr {
9722 name,
9723 columns,
9724 unique: true,
9725 method: "btree".to_owned(),
9726 where_clause: None,
9727 include: Vec::new(),
9728 });
9729 return Ok(());
9730 }
9731 if meta.path.is_ident("index_together") {
9732 // Composite (non-unique) index. Two syntaxes
9733 // mirroring `unique_together`.
9734 //
9735 // #[rustango(index_together = "created_at, status")]
9736 // #[rustango(index_together(columns = "created_at, status", name = "x"))]
9737 let (columns, name) = parse_together_attr(&meta, "index_together")?;
9738 out.indexes.push(IndexAttr {
9739 name,
9740 columns,
9741 unique: false,
9742 method: "btree".to_owned(),
9743 where_clause: None,
9744 include: Vec::new(),
9745 });
9746 return Ok(());
9747 }
9748 if meta.path.is_ident("unique_when") {
9749 // Partial unique index. Issue #265 / T1.3.
9750 //
9751 // #[rustango(unique_when(
9752 // columns = "email",
9753 // condition = "deleted_at IS NULL",
9754 // name = "unique_active_email"
9755 // ))]
9756 //
9757 // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9758 // on PG / SQLite (both ship partial indexes natively).
9759 // MySQL falls back to a plain UNIQUE index — the
9760 // condition is lost; document the limitation in the
9761 // generated migration.
9762 let mut columns: Option<Vec<String>> = None;
9763 let mut condition: Option<String> = None;
9764 let mut name: Option<String> = None;
9765 let mut include: Vec<String> = Vec::new();
9766 meta.parse_nested_meta(|inner| {
9767 if inner.path.is_ident("columns") {
9768 let s: LitStr = inner.value()?.parse()?;
9769 columns = Some(split_field_list(&s.value()));
9770 return Ok(());
9771 }
9772 if inner.path.is_ident("condition") {
9773 let s: LitStr = inner.value()?.parse()?;
9774 condition = Some(s.value());
9775 return Ok(());
9776 }
9777 if inner.path.is_ident("name") {
9778 let s: LitStr = inner.value()?.parse()?;
9779 name = Some(s.value());
9780 return Ok(());
9781 }
9782 if inner.path.is_ident("include") {
9783 // PG 11+ covering-index columns. Non-key columns
9784 // travel with the index leaf for index-only
9785 // scans. Dropped on MySQL/SQLite by the writer.
9786 let s: LitStr = inner.value()?.parse()?;
9787 include = split_field_list(&s.value());
9788 return Ok(());
9789 }
9790 Err(inner.error(
9791 "unknown unique_when attribute (supported: \
9792 `columns = \"...\"`, `condition = \"...\"`, \
9793 `name = \"...\"`, `include = \"...\"`)",
9794 ))
9795 })?;
9796 let columns = columns.ok_or_else(|| {
9797 meta.error("`unique_when(...)` requires `columns = \"...\"`")
9798 })?;
9799 let condition = condition.ok_or_else(|| {
9800 meta.error("`unique_when(...)` requires `condition = \"...\"`")
9801 })?;
9802 if columns.is_empty() {
9803 return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9804 }
9805 out.indexes.push(IndexAttr {
9806 name,
9807 columns,
9808 unique: true,
9809 method: "btree".to_owned(),
9810 where_clause: Some(condition),
9811 include,
9812 });
9813 return Ok(());
9814 }
9815 if meta.path.is_ident("index_when") {
9816 // Non-unique partial index. Sibling of `unique_when`
9817 // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9818 //
9819 // #[rustango(index_when(
9820 // columns = "status, created_at",
9821 // condition = "deleted_at IS NULL",
9822 // name = "active_status_created_idx"
9823 // ))]
9824 //
9825 // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9826 // on PG / SQLite (both ship partial indexes natively).
9827 // MySQL has no native partial-index support — the writer
9828 // emits a plain CREATE INDEX and the condition is lost;
9829 // operators wanting that selectivity on MySQL should
9830 // declare a covering index plus an application-level
9831 // filter.
9832 let mut columns: Option<Vec<String>> = None;
9833 let mut condition: Option<String> = None;
9834 let mut name: Option<String> = None;
9835 let mut method: String = "btree".to_owned();
9836 let mut include: Vec<String> = Vec::new();
9837 meta.parse_nested_meta(|inner| {
9838 if inner.path.is_ident("columns") {
9839 let s: LitStr = inner.value()?.parse()?;
9840 columns = Some(split_field_list(&s.value()));
9841 return Ok(());
9842 }
9843 if inner.path.is_ident("condition") {
9844 let s: LitStr = inner.value()?.parse()?;
9845 condition = Some(s.value());
9846 return Ok(());
9847 }
9848 if inner.path.is_ident("name") {
9849 let s: LitStr = inner.value()?.parse()?;
9850 name = Some(s.value());
9851 return Ok(());
9852 }
9853 if inner.path.is_ident("method") {
9854 let s: LitStr = inner.value()?.parse()?;
9855 method = s.value();
9856 return Ok(());
9857 }
9858 if inner.path.is_ident("include") {
9859 // PG 11+ covering-index columns; non-key
9860 // columns travel with the index leaf. Dropped
9861 // on MySQL/SQLite.
9862 let s: LitStr = inner.value()?.parse()?;
9863 include = split_field_list(&s.value());
9864 return Ok(());
9865 }
9866 Err(inner.error(
9867 "unknown index_when attribute (supported: \
9868 `columns = \"...\"`, `condition = \"...\"`, \
9869 `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9870 `include = \"...\"`)",
9871 ))
9872 })?;
9873 let columns = columns
9874 .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9875 let condition = condition.ok_or_else(|| {
9876 meta.error("`index_when(...)` requires `condition = \"...\"`")
9877 })?;
9878 if columns.is_empty() {
9879 return Err(meta.error("`index_when(columns = \"\")` is empty"));
9880 }
9881 out.indexes.push(IndexAttr {
9882 name,
9883 columns,
9884 unique: false,
9885 method,
9886 where_clause: Some(condition),
9887 include,
9888 });
9889 return Ok(());
9890 }
9891 if meta.path.is_ident("index") {
9892 // Container-level composite index. Two syntaxes:
9893 // #[rustango(index = "col1, col2")] — bare, non-unique btree
9894 // #[rustango(index("col1, col2"))] — call form (same result)
9895 // #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
9896 // The call form takes the column list as a leading string
9897 // literal, then optional `unique` / `name = "..."` /
9898 // `method = "..."` flags (a leading literal can't compose
9899 // under `parse_nested_meta`, so the paren body is parsed by
9900 // hand). `unique_together` / `index_together` remain
9901 // aliases for the same feature.
9902 let cols_lit: LitStr;
9903 let mut unique = false;
9904 let mut name: Option<String> = None;
9905 let mut method = "btree".to_owned();
9906 if meta.input.peek(syn::token::Paren) {
9907 let content;
9908 syn::parenthesized!(content in meta.input);
9909 cols_lit = content.parse()?;
9910 while content.peek(syn::Token![,]) {
9911 content.parse::<syn::Token![,]>()?;
9912 if content.is_empty() {
9913 break;
9914 }
9915 let flag: syn::Ident = content.parse()?;
9916 if flag == "unique" {
9917 unique = true;
9918 } else if flag == "name" {
9919 content.parse::<syn::Token![=]>()?;
9920 let s: LitStr = content.parse()?;
9921 name = Some(s.value());
9922 } else if flag == "method" {
9923 content.parse::<syn::Token![=]>()?;
9924 let s: LitStr = content.parse()?;
9925 let v = s.value();
9926 match v.as_str() {
9927 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
9928 | "bloom" => method = v,
9929 other => {
9930 return Err(syn::Error::new(
9931 s.span(),
9932 format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
9933 ));
9934 }
9935 }
9936 } else {
9937 return Err(syn::Error::new(
9938 flag.span(),
9939 "unknown index flag (supported: `unique`, `name`, `method`)",
9940 ));
9941 }
9942 }
9943 } else {
9944 cols_lit = meta.value()?.parse()?;
9945 }
9946 let columns = split_field_list(&cols_lit.value());
9947 out.indexes.push(IndexAttr {
9948 name,
9949 columns,
9950 unique,
9951 method,
9952 where_clause: None,
9953 include: Vec::new(),
9954 });
9955 return Ok(());
9956 }
9957 if meta.path.is_ident("check") {
9958 // #[rustango(check(name = "…", expr = "…"))]
9959 let mut name: Option<String> = None;
9960 let mut expr: Option<String> = None;
9961 meta.parse_nested_meta(|inner| {
9962 if inner.path.is_ident("name") {
9963 let s: LitStr = inner.value()?.parse()?;
9964 name = Some(s.value());
9965 return Ok(());
9966 }
9967 if inner.path.is_ident("expr") {
9968 let s: LitStr = inner.value()?.parse()?;
9969 expr = Some(s.value());
9970 return Ok(());
9971 }
9972 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
9973 })?;
9974 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
9975 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
9976 out.checks.push(CheckAttr { name, expr });
9977 return Ok(());
9978 }
9979 if meta.path.is_ident("exclude") {
9980 // #[rustango(exclude(name = "…", using = "gist",
9981 // elements = "col WITH op, col WITH op",
9982 // where = "…"))]
9983 let mut name: Option<String> = None;
9984 let mut using: Option<String> = None;
9985 let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
9986 let mut where_clause: Option<String> = None;
9987 meta.parse_nested_meta(|inner| {
9988 if inner.path.is_ident("name") {
9989 let s: LitStr = inner.value()?.parse()?;
9990 name = Some(s.value());
9991 return Ok(());
9992 }
9993 if inner.path.is_ident("using") {
9994 let s: LitStr = inner.value()?.parse()?;
9995 using = Some(s.value());
9996 return Ok(());
9997 }
9998 if inner.path.is_ident("elements") {
9999 let s: LitStr = inner.value()?.parse()?;
10000 elements_raw = Some((s.value(), s.span()));
10001 return Ok(());
10002 }
10003 if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
10004 let s: LitStr = inner.value()?.parse()?;
10005 where_clause = Some(s.value());
10006 return Ok(());
10007 }
10008 Err(inner.error(
10009 "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
10010 ))
10011 })?;
10012 let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
10013 let using = using.unwrap_or_else(|| "gist".to_owned());
10014 let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
10015 meta.error(
10016 "exclude requires `elements = \"col WITH op, col WITH op\"`",
10017 )
10018 })?;
10019 // Parse `col WITH op` pairs separated by commas.
10020 let mut elements: Vec<(String, String)> = Vec::new();
10021 for pair in elements_str.split(',') {
10022 let pair = pair.trim();
10023 if pair.is_empty() {
10024 continue;
10025 }
10026 let mut split = pair.splitn(2, |c: char| c.is_whitespace());
10027 let col = split.next().unwrap_or("").trim();
10028 let rest = split.next().unwrap_or("").trim();
10029 // `WITH op` — case-insensitive on `WITH`, then op.
10030 let rest_lc = rest.to_ascii_lowercase();
10031 let op = rest_lc
10032 .strip_prefix("with")
10033 .map(|r| r.trim_start())
10034 .filter(|r| !r.is_empty())
10035 .map(|_| {
10036 // Pull the original-case op from `rest` after the
10037 // `WITH ` token (5 chars).
10038 rest[4..].trim_start().to_owned()
10039 });
10040 let Some(op) = op else {
10041 return Err(syn::Error::new(
10042 elements_span,
10043 format!(
10044 "exclude elements: `{pair}` must be `<col> WITH <op>` \
10045 (e.g. `room_id WITH =` or `during WITH &&`)"
10046 ),
10047 ));
10048 };
10049 if col.is_empty() || op.is_empty() {
10050 return Err(syn::Error::new(
10051 elements_span,
10052 format!(
10053 "exclude elements: `{pair}` must be `<col> WITH <op>` \
10054 (both sides non-empty)"
10055 ),
10056 ));
10057 }
10058 elements.push((col.to_owned(), op));
10059 }
10060 if elements.is_empty() {
10061 return Err(syn::Error::new(
10062 elements_span,
10063 "exclude requires at least one `col WITH op` element",
10064 ));
10065 }
10066 out.excludes.push(ExcludeAttr {
10067 name,
10068 using,
10069 elements,
10070 where_clause,
10071 });
10072 return Ok(());
10073 }
10074 if meta.path.is_ident("generic_fk") {
10075 let mut gfk = GenericFkAttr {
10076 name: String::new(),
10077 ct_column: String::new(),
10078 pk_column: String::new(),
10079 };
10080 meta.parse_nested_meta(|inner| {
10081 if inner.path.is_ident("name") {
10082 let s: LitStr = inner.value()?.parse()?;
10083 gfk.name = s.value();
10084 return Ok(());
10085 }
10086 if inner.path.is_ident("ct_column") {
10087 let s: LitStr = inner.value()?.parse()?;
10088 gfk.ct_column = s.value();
10089 return Ok(());
10090 }
10091 if inner.path.is_ident("pk_column") {
10092 let s: LitStr = inner.value()?.parse()?;
10093 gfk.pk_column = s.value();
10094 return Ok(());
10095 }
10096 Err(inner.error(
10097 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10098 ))
10099 })?;
10100 if gfk.name.is_empty() {
10101 return Err(meta.error("generic_fk requires `name = \"...\"`"));
10102 }
10103 if gfk.ct_column.is_empty() {
10104 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10105 }
10106 if gfk.pk_column.is_empty() {
10107 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10108 }
10109 out.generic_fks.push(gfk);
10110 return Ok(());
10111 }
10112 if meta.path.is_ident("fk_composite") {
10113 let mut fk = CompositeFkAttr {
10114 name: String::new(),
10115 to: String::new(),
10116 from: Vec::new(),
10117 on: Vec::new(),
10118 };
10119 meta.parse_nested_meta(|inner| {
10120 if inner.path.is_ident("name") {
10121 let s: LitStr = inner.value()?.parse()?;
10122 fk.name = s.value();
10123 return Ok(());
10124 }
10125 if inner.path.is_ident("to") {
10126 let s: LitStr = inner.value()?.parse()?;
10127 fk.to = s.value();
10128 return Ok(());
10129 }
10130 // `on = ("col1", "col2", ...)` — parse a parenthesised
10131 // comma-list of string literals.
10132 if inner.path.is_ident("on") || inner.path.is_ident("from") {
10133 let value = inner.value()?;
10134 let content;
10135 syn::parenthesized!(content in value);
10136 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10137 content.parse_terminated(
10138 |p| p.parse::<syn::LitStr>(),
10139 syn::Token![,],
10140 )?;
10141 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10142 if inner.path.is_ident("on") {
10143 fk.on = cols;
10144 } else {
10145 fk.from = cols;
10146 }
10147 return Ok(());
10148 }
10149 Err(inner.error(
10150 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10151 ))
10152 })?;
10153 if fk.name.is_empty() {
10154 return Err(meta.error("fk_composite requires `name = \"...\"`"));
10155 }
10156 if fk.to.is_empty() {
10157 return Err(meta.error("fk_composite requires `to = \"...\"`"));
10158 }
10159 if fk.from.is_empty() || fk.on.is_empty() {
10160 return Err(meta.error(
10161 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10162 ));
10163 }
10164 if fk.from.len() != fk.on.len() {
10165 return Err(meta.error(format!(
10166 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10167 fk.from.len(),
10168 fk.on.len(),
10169 )));
10170 }
10171 out.composite_fks.push(fk);
10172 return Ok(());
10173 }
10174 if meta.path.is_ident("m2m") {
10175 let mut m2m = M2MAttr {
10176 name: String::new(),
10177 to: String::new(),
10178 through: String::new(),
10179 src: String::new(),
10180 dst: String::new(),
10181 auto_create: true,
10182 };
10183 meta.parse_nested_meta(|inner| {
10184 if inner.path.is_ident("name") {
10185 let s: LitStr = inner.value()?.parse()?;
10186 m2m.name = s.value();
10187 return Ok(());
10188 }
10189 if inner.path.is_ident("to") {
10190 let s: LitStr = inner.value()?.parse()?;
10191 m2m.to = s.value();
10192 return Ok(());
10193 }
10194 if inner.path.is_ident("through") {
10195 let s: LitStr = inner.value()?.parse()?;
10196 m2m.through = s.value();
10197 return Ok(());
10198 }
10199 if inner.path.is_ident("src") {
10200 let s: LitStr = inner.value()?.parse()?;
10201 m2m.src = s.value();
10202 return Ok(());
10203 }
10204 if inner.path.is_ident("dst") {
10205 let s: LitStr = inner.value()?.parse()?;
10206 m2m.dst = s.value();
10207 return Ok(());
10208 }
10209 if inner.path.is_ident("auto_create") {
10210 let lit: syn::LitBool = inner.value()?.parse()?;
10211 m2m.auto_create = lit.value;
10212 return Ok(());
10213 }
10214 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10215 })?;
10216 if m2m.name.is_empty() {
10217 return Err(meta.error("m2m requires `name = \"...\"`"));
10218 }
10219 if m2m.to.is_empty() {
10220 return Err(meta.error("m2m requires `to = \"...\"`"));
10221 }
10222 if m2m.through.is_empty() {
10223 return Err(meta.error("m2m requires `through = \"...\"`"));
10224 }
10225 if m2m.src.is_empty() {
10226 return Err(meta.error("m2m requires `src = \"...\"`"));
10227 }
10228 if m2m.dst.is_empty() {
10229 return Err(meta.error("m2m requires `dst = \"...\"`"));
10230 }
10231 out.m2m.push(m2m);
10232 return Ok(());
10233 }
10234 if meta.path.is_ident("generic_m2m") {
10235 let mut gm = GenericM2MAttr {
10236 name: String::new(),
10237 through: String::new(),
10238 pk_column: String::new(),
10239 ct_column: String::new(),
10240 related_column: String::new(),
10241 };
10242 meta.parse_nested_meta(|inner| {
10243 let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10244 let s: LitStr = inner.value()?.parse()?;
10245 Ok(s.value())
10246 };
10247 if inner.path.is_ident("name") {
10248 gm.name = field(&inner)?;
10249 return Ok(());
10250 }
10251 if inner.path.is_ident("through") {
10252 gm.through = field(&inner)?;
10253 return Ok(());
10254 }
10255 if inner.path.is_ident("pk_column") {
10256 gm.pk_column = field(&inner)?;
10257 return Ok(());
10258 }
10259 if inner.path.is_ident("ct_column") {
10260 gm.ct_column = field(&inner)?;
10261 return Ok(());
10262 }
10263 if inner.path.is_ident("related_column") {
10264 gm.related_column = field(&inner)?;
10265 return Ok(());
10266 }
10267 Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10268 })?;
10269 for (val, label) in [
10270 (&gm.name, "name"),
10271 (&gm.through, "through"),
10272 (&gm.pk_column, "pk_column"),
10273 (&gm.ct_column, "ct_column"),
10274 (&gm.related_column, "related_column"),
10275 ] {
10276 if val.is_empty() {
10277 return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10278 }
10279 }
10280 out.generic_m2m.push(gm);
10281 return Ok(());
10282 }
10283 Err(meta.error("unknown rustango container attribute"))
10284 })?;
10285 }
10286 Ok(out)
10287}
10288
10289/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10290/// owned field names, trimming whitespace and skipping empty entries.
10291/// Field-name validation against the model is done by the caller.
10292fn split_field_list(raw: &str) -> Vec<String> {
10293 raw.split(',')
10294 .map(str::trim)
10295 .filter(|s| !s.is_empty())
10296 .map(str::to_owned)
10297 .collect()
10298}
10299
10300/// Shared parser for `unique_together` and `index_together` container
10301/// attrs. Accepts both shapes:
10302///
10303/// * `attr = "col1, col2"` — auto-derived index name.
10304/// * `attr(columns = "col1, col2", name = "...")` — explicit name.
10305///
10306/// Returns `(columns, name)`.
10307fn parse_together_attr(
10308 meta: &syn::meta::ParseNestedMeta<'_>,
10309 attr: &str,
10310) -> syn::Result<(Vec<String>, Option<String>)> {
10311 // Disambiguate by whether the next token is `=` (key-value) or
10312 // `(` (parenthesized).
10313 if meta.input.peek(syn::Token![=]) {
10314 let cols_lit: LitStr = meta.value()?.parse()?;
10315 let columns = split_field_list(&cols_lit.value());
10316 check_together_columns(meta, attr, &columns)?;
10317 return Ok((columns, None));
10318 }
10319 let mut columns: Option<Vec<String>> = None;
10320 let mut name: Option<String> = None;
10321 meta.parse_nested_meta(|inner| {
10322 if inner.path.is_ident("columns") {
10323 let s: LitStr = inner.value()?.parse()?;
10324 columns = Some(split_field_list(&s.value()));
10325 return Ok(());
10326 }
10327 if inner.path.is_ident("name") {
10328 let s: LitStr = inner.value()?.parse()?;
10329 name = Some(s.value());
10330 return Ok(());
10331 }
10332 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10333 })?;
10334 let columns = columns.ok_or_else(|| {
10335 meta.error(format!(
10336 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10337 ))
10338 })?;
10339 check_together_columns(meta, attr, &columns)?;
10340 Ok((columns, name))
10341}
10342
10343fn check_together_columns(
10344 meta: &syn::meta::ParseNestedMeta<'_>,
10345 attr: &str,
10346 columns: &[String],
10347) -> syn::Result<()> {
10348 if columns.len() < 2 {
10349 let single = if attr == "unique_together" {
10350 "#[rustango(unique)] on the field"
10351 } else {
10352 "#[rustango(index)] on the field"
10353 };
10354 return Err(meta.error(format!(
10355 "{attr} expects two or more columns; for a single-column equivalent use {single}",
10356 )));
10357 }
10358 Ok(())
10359}
10360
10361/// Parse the fieldsets DSL: pipe-separated sections, optional
10362/// `"Title:"` prefix on each, comma-separated field names after.
10363/// Examples:
10364/// * `"name, office"` → one untitled section with two fields
10365/// * `"Identity: name, office | Metadata: created_at"` → two titled
10366/// sections
10367///
10368/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10369fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10370 raw.split('|')
10371 .map(str::trim)
10372 .filter(|s| !s.is_empty())
10373 .map(|section| {
10374 // Split off an optional `Title:` prefix (first colon).
10375 let (title, rest) = match section.split_once(':') {
10376 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10377 _ => (String::new(), section),
10378 };
10379 let fields = split_field_list(rest);
10380 (title, fields)
10381 })
10382 .collect()
10383}
10384
10385/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10386/// comma-separated entry maps a target field to one or more source
10387/// fields joined with `+`. Whitespace around tokens is trimmed.
10388/// Entries missing `:` or with empty target/source lists are dropped.
10389fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10390 raw.split(',')
10391 .map(str::trim)
10392 .filter(|s| !s.is_empty())
10393 .filter_map(|entry| {
10394 let (target, sources_raw) = entry.split_once(':')?;
10395 let target = target.trim().to_owned();
10396 if target.is_empty() {
10397 return None;
10398 }
10399 let sources: Vec<String> = sources_raw
10400 .split('+')
10401 .map(|s| s.trim().to_owned())
10402 .filter(|s| !s.is_empty())
10403 .collect();
10404 if sources.is_empty() {
10405 return None;
10406 }
10407 Some((target, sources))
10408 })
10409 .collect()
10410}
10411
10412/// Parse `formfield_overrides` — `"field:widget,field2:widget2"`
10413/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10414/// and empty halves drop silently — the macro layer only enforces shape,
10415/// not field-name vs. widget-name validity (those checks happen at
10416/// `AdminConfig` consumption time). Issue #359.
10417fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10418 raw.split(',')
10419 .map(str::trim)
10420 .filter(|s| !s.is_empty())
10421 .filter_map(|entry| {
10422 let (field, widget) = entry.split_once(':')?;
10423 let field = field.trim().to_owned();
10424 let widget = widget.trim().to_owned();
10425 if field.is_empty() || widget.is_empty() {
10426 return None;
10427 }
10428 Some((field, widget))
10429 })
10430 .collect()
10431}
10432
10433/// Parse an ordering list — `"name"` is ASC, `"-name"` is DESC.
10434/// Returns `(field_name, desc)` pairs in the same order as the input.
10435fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10436 raw.split(',')
10437 .map(str::trim)
10438 .filter(|s| !s.is_empty())
10439 .map(|spec| {
10440 spec.strip_prefix('-')
10441 .map_or((spec.to_owned(), false), |rest| {
10442 (rest.trim().to_owned(), true)
10443 })
10444 })
10445 .collect()
10446}
10447
10448struct FieldAttrs {
10449 column: Option<String>,
10450 primary_key: bool,
10451 fk: Option<String>,
10452 o2o: Option<String>,
10453 on: Option<String>,
10454 /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10455 /// "set_default" | "no_action")]` — what happens to this row when
10456 /// the row it points at is deleted.
10457 /// Only meaningful when `fk` / `o2o` is
10458 /// also set; the macro errors at compile time if applied to a
10459 /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10460 /// DDL writer renders `ON DELETE <action>` after the constraint
10461 /// clause when this is `Some`; `None` falls back to the database
10462 /// default (NO ACTION on every backend rustango supports).
10463 on_delete: Option<String>,
10464 /// `#[rustango(related_name = "...")]` — per-FK
10465 /// reverse-accessor override. When set, the derive emits
10466 /// `Parent::<related_name>[_pool]` instead of the container-level
10467 /// `default_related_name` or the `<child_snake>_set[_pool]`
10468 /// fallback. Only meaningful when `fk` / `o2o` is also set;
10469 /// silently ignored on non-FK fields. Follow-up to #816.
10470 related_name: Option<String>,
10471 max_length: Option<u32>,
10472 /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10473 /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10474 /// unconstrained `vector` column.
10475 vector_dims: Option<u32>,
10476 /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10477 /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10478 /// unconstrained `geometry(Point)` column (SRID 0).
10479 geometry_srid: Option<u32>,
10480 min: Option<i64>,
10481 max: Option<i64>,
10482 default: Option<String>,
10483 /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10484 /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10485 /// "gen_random_uuid()"`. The Rust field type must be
10486 /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10487 /// INSERTs so the DB DEFAULT fires.
10488 auto_uuid: bool,
10489 /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10490 /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10491 /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10492 /// the field is `Auto::Unset`, then bound as a normal parameter
10493 /// rather than relying on a per-dialect DB function. Issue #823
10494 /// (Eloquent `HasUuids`).
10495 ///
10496 /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10497 /// Composes with every backend (PG / MySQL / SQLite) — no
10498 /// `gen_random_uuid()` requirement on the database.
10499 default_uuid_v7: bool,
10500 /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10501 /// Server-set on insert, immutable from app code afterwards.
10502 /// Implies `auto + default = "now()"`. Field type must be
10503 /// `DateTime<Utc>`.
10504 auto_now_add: bool,
10505 /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10506 /// every insert AND every update. Implies `auto + default =
10507 /// "now()"`; the macro additionally rewrites `update_on` /
10508 /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10509 /// field value.
10510 auto_now: bool,
10511 /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10512 /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10513 /// `soft_delete_on(executor)` and `restore_on(executor)`
10514 /// methods on the model.
10515 soft_delete: bool,
10516 /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10517 /// the column in the generated DDL.
10518 unique: bool,
10519 /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10520 /// generates a `CREATE INDEX` for this column. `unique` here means
10521 /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10522 index: bool,
10523 index_unique: bool,
10524 index_name: Option<String>,
10525 /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10526 /// `"btree"`. Issue #34.
10527 index_method: String,
10528 /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10529 /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10530 /// the macro skips this column from every INSERT and UPDATE
10531 /// path, so the database always recomputes the value from
10532 /// `EXPR`. Backlog item #35.
10533 generated_as: Option<String>,
10534 /// `#[rustango(help_text = "…")]` — help text
10535 /// rendered below the admin form's input. Threaded into
10536 /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10537 /// layers can read it.
10538 help_text: Option<String>,
10539 /// `#[rustango(choices = "value:Label, value:Label")]` — the
10540 /// enumerated allowed values. Threaded into `FieldSchema::choices`
10541 /// as a `&'static [(&'static str, &'static str)]` slice. When
10542 /// present, the admin form renders a `<select>` instead of `<input>`
10543 /// and the validator rejects values not in the list. Only meaningful
10544 /// for `FieldType::String`; the macro errors at compile time if
10545 /// applied to a non-string field.
10546 choices: Option<Vec<(String, String)>>,
10547 /// `#[rustango(db_comment = "…")]` — DB-side column
10548 /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10549 /// the comment in CREATE TABLE; Postgres emits a separate
10550 /// `COMMENT ON COLUMN` statement after the table is created;
10551 /// SQLite silently drops the value (no native column comments).
10552 db_comment: Option<String>,
10553 /// `#[rustango(verbose_name = "…")]` — human-readable
10554 /// label for the field. Threaded into `FieldSchema::verbose_name`
10555 /// so admin column headers, form labels, and other display
10556 /// surfaces can prefer the friendly caption over the Rust
10557 /// identifier. `None` means renderers fall back to the field name.
10558 verbose_name: Option<String>,
10559 /// `#[rustango(editable = false)]` — opt out of
10560 /// auto-generated form rendering. Defaults to `true` so existing
10561 /// fields keep their current admin / form behavior; setting
10562 /// `false` removes the field from the admin change-form entirely
10563 /// (the value is still visible on detail / list views, just not
10564 /// editable).
10565 editable: bool,
10566 /// `#[rustango(blank)]` / `#[rustango(blank = true)]` —
10567 /// "form may submit empty even when DB is NOT NULL". Threaded into
10568 /// `FieldSchema::blank`. Defaults to `false`.
10569 blank: bool,
10570 /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10571 /// case-insensitive text column. Threaded into
10572 /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10573 /// fields; the macro errors at derive time if applied elsewhere.
10574 case_insensitive: bool,
10575 /// `#[rustango(validators = "email,url")]` — the
10576 /// model-level validator chain. Comma-separated names that
10577 /// dispatch to the `validators::*` family in `validate_value`.
10578 /// Empty by default; fires on every typed INSERT/UPDATE.
10579 validators: Vec<String>,
10580}
10581
10582fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10583 let mut out = FieldAttrs {
10584 column: None,
10585 primary_key: false,
10586 fk: None,
10587 o2o: None,
10588 on: None,
10589 on_delete: None,
10590 related_name: None,
10591 max_length: None,
10592 vector_dims: None,
10593 geometry_srid: None,
10594 min: None,
10595 max: None,
10596 default: None,
10597 auto_uuid: false,
10598 default_uuid_v7: false,
10599 auto_now_add: false,
10600 auto_now: false,
10601 soft_delete: false,
10602 unique: false,
10603 index: false,
10604 index_unique: false,
10605 index_name: None,
10606 index_method: "btree".to_owned(),
10607 generated_as: None,
10608 help_text: None,
10609 choices: None,
10610 db_comment: None,
10611 verbose_name: None,
10612 editable: true,
10613 blank: false,
10614 case_insensitive: false,
10615 validators: Vec::new(),
10616 };
10617 for attr in &field.attrs {
10618 if !attr.path().is_ident("rustango") {
10619 continue;
10620 }
10621 attr.parse_nested_meta(|meta| {
10622 if meta.path.is_ident("column") {
10623 let s: LitStr = meta.value()?.parse()?;
10624 let name = s.value();
10625 validate_sql_identifier(&name, "column", s.span())?;
10626 out.column = Some(name);
10627 return Ok(());
10628 }
10629 if meta.path.is_ident("primary_key") {
10630 out.primary_key = true;
10631 return Ok(());
10632 }
10633 if meta.path.is_ident("fk") {
10634 let s: LitStr = meta.value()?.parse()?;
10635 out.fk = Some(s.value());
10636 return Ok(());
10637 }
10638 if meta.path.is_ident("o2o") {
10639 let s: LitStr = meta.value()?.parse()?;
10640 out.o2o = Some(s.value());
10641 return Ok(());
10642 }
10643 if meta.path.is_ident("on") {
10644 let s: LitStr = meta.value()?.parse()?;
10645 out.on = Some(s.value());
10646 return Ok(());
10647 }
10648 if meta.path.is_ident("on_delete") {
10649 let s: LitStr = meta.value()?.parse()?;
10650 let raw = s.value();
10651 let normalized = raw.trim().to_ascii_lowercase();
10652 // Validate at parse time so the user gets a clear span
10653 // rather than a downstream compile error in the emit.
10654 match normalized.as_str() {
10655 "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10656 _ => {
10657 return Err(syn::Error::new(
10658 s.span(),
10659 format!(
10660 "unknown on_delete action `{raw}`; expected one of \
10661 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10662 ),
10663 ));
10664 }
10665 }
10666 out.on_delete = Some(normalized);
10667 return Ok(());
10668 }
10669 if meta.path.is_ident("related_name") {
10670 let s: LitStr = meta.value()?.parse()?;
10671 let raw = s.value();
10672 if raw.trim().is_empty() {
10673 return Err(syn::Error::new(
10674 s.span(),
10675 "`related_name` must be a non-empty identifier",
10676 ));
10677 }
10678 // Validate as a Rust-ident shape — the value becomes
10679 // a method name on the parent type. Same rule as
10680 // `default_related_name` so the two surfaces match.
10681 if !raw
10682 .chars()
10683 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10684 || raw.starts_with(char::is_numeric)
10685 {
10686 return Err(syn::Error::new(
10687 s.span(),
10688 "`related_name` must be snake_case ASCII (lowercase letters, \
10689 digits, underscores; no leading digit)",
10690 ));
10691 }
10692 out.related_name = Some(raw);
10693 return Ok(());
10694 }
10695 if meta.path.is_ident("max_length") {
10696 let lit: syn::LitInt = meta.value()?.parse()?;
10697 out.max_length = Some(lit.base10_parse::<u32>()?);
10698 return Ok(());
10699 }
10700 // `#[rustango(vector(dims = N))]` — pgvector column
10701 // dimension (#824). Nested-meta form so it reads like the
10702 // other typed-column attrs.
10703 if meta.path.is_ident("vector") {
10704 meta.parse_nested_meta(|inner| {
10705 if inner.path.is_ident("dims") {
10706 let lit: syn::LitInt = inner.value()?.parse()?;
10707 out.vector_dims = Some(lit.base10_parse::<u32>()?);
10708 return Ok(());
10709 }
10710 Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10711 })?;
10712 return Ok(());
10713 }
10714 // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10715 // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10716 if meta.path.is_ident("geometry") {
10717 meta.parse_nested_meta(|inner| {
10718 if inner.path.is_ident("srid") {
10719 let lit: syn::LitInt = inner.value()?.parse()?;
10720 out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10721 return Ok(());
10722 }
10723 Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10724 })?;
10725 return Ok(());
10726 }
10727 if meta.path.is_ident("min") {
10728 out.min = Some(parse_signed_i64(&meta)?);
10729 return Ok(());
10730 }
10731 if meta.path.is_ident("max") {
10732 out.max = Some(parse_signed_i64(&meta)?);
10733 return Ok(());
10734 }
10735 if meta.path.is_ident("default") {
10736 let s: LitStr = meta.value()?.parse()?;
10737 out.default = Some(s.value());
10738 return Ok(());
10739 }
10740 if meta.path.is_ident("generated_as") {
10741 let s: LitStr = meta.value()?.parse()?;
10742 out.generated_as = Some(s.value());
10743 return Ok(());
10744 }
10745 if meta.path.is_ident("help_text") {
10746 let s: LitStr = meta.value()?.parse()?;
10747 out.help_text = Some(s.value());
10748 return Ok(());
10749 }
10750 if meta.path.is_ident("choices") {
10751 let s: LitStr = meta.value()?.parse()?;
10752 let raw = s.value();
10753 let mut pairs: Vec<(String, String)> = Vec::new();
10754 for chunk in raw.split(',') {
10755 let chunk = chunk.trim();
10756 if chunk.is_empty() {
10757 continue;
10758 }
10759 let (value, label) = match chunk.split_once(':') {
10760 Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10761 None => (chunk.to_owned(), chunk.to_owned()),
10762 };
10763 if value.is_empty() {
10764 return Err(syn::Error::new(
10765 s.span(),
10766 "`choices` entry has empty value before `:`",
10767 ));
10768 }
10769 pairs.push((value, label));
10770 }
10771 if pairs.is_empty() {
10772 return Err(syn::Error::new(
10773 s.span(),
10774 "`choices = \"…\"` must contain at least one value",
10775 ));
10776 }
10777 out.choices = Some(pairs);
10778 return Ok(());
10779 }
10780 if meta.path.is_ident("db_comment") {
10781 let s: LitStr = meta.value()?.parse()?;
10782 out.db_comment = Some(s.value());
10783 return Ok(());
10784 }
10785 if meta.path.is_ident("verbose_name") {
10786 let s: LitStr = meta.value()?.parse()?;
10787 out.verbose_name = Some(s.value());
10788 return Ok(());
10789 }
10790 if meta.path.is_ident("editable") {
10791 // Two forms accepted:
10792 // #[rustango(editable = false)] / true — explicit
10793 // #[rustango(editable)] — flag form (= true, the
10794 // default, so harmless; included for symmetry)
10795 if let Ok(v) = meta.value() {
10796 let lit: syn::LitBool = v.parse()?;
10797 out.editable = lit.value;
10798 } else {
10799 out.editable = true;
10800 }
10801 return Ok(());
10802 }
10803 if meta.path.is_ident("blank") {
10804 // Two forms accepted:
10805 // #[rustango(blank)] — flag form, true
10806 // #[rustango(blank = false)] / true — explicit
10807 if let Ok(v) = meta.value() {
10808 let lit: syn::LitBool = v.parse()?;
10809 out.blank = lit.value;
10810 } else {
10811 out.blank = true;
10812 }
10813 return Ok(());
10814 }
10815 if meta.path.is_ident("citext") {
10816 // Case-insensitive text column (#344). Two forms:
10817 // #[rustango(citext)] — flag form, true
10818 // #[rustango(citext = true)] — explicit
10819 // #[rustango(citext = false)] — explicit opt-out
10820 // String-only validation lives in the field-type
10821 // emitter (the FieldType discriminant is computed in
10822 // `detect_type`); the macro records the flag and the
10823 // DDL writer emits dialect-specific COLLATE / CITEXT.
10824 if let Ok(v) = meta.value() {
10825 let lit: syn::LitBool = v.parse()?;
10826 out.case_insensitive = lit.value;
10827 } else {
10828 out.case_insensitive = true;
10829 }
10830 return Ok(());
10831 }
10832 if meta.path.is_ident("validators") {
10833 let s: LitStr = meta.value()?.parse()?;
10834 let raw = s.value();
10835 out.validators = raw
10836 .split(',')
10837 .map(str::trim)
10838 .filter(|s| !s.is_empty())
10839 .map(str::to_owned)
10840 .collect();
10841 if out.validators.is_empty() {
10842 return Err(syn::Error::new(
10843 s.span(),
10844 "`validators = \"…\"` must list at least one name",
10845 ));
10846 }
10847 return Ok(());
10848 }
10849 if meta.path.is_ident("auto_uuid") {
10850 out.auto_uuid = true;
10851 // Implied: PK + auto + DEFAULT gen_random_uuid().
10852 // Each is also explicitly settable; the explicit
10853 // value wins if conflicting.
10854 out.primary_key = true;
10855 if out.default.is_none() {
10856 out.default = Some("gen_random_uuid()".into());
10857 }
10858 return Ok(());
10859 }
10860 if meta.path.is_ident("default_uuid_v7") {
10861 // Backend-neutral counterpart of `auto_uuid` — issue #823.
10862 // No SQL DEFAULT (the macro fills the value Rust-side
10863 // before binding); just mark the field as PK + Auto
10864 // so the insert path is the `Auto::Unset → generate`
10865 // branch.
10866 out.default_uuid_v7 = true;
10867 out.primary_key = true;
10868 return Ok(());
10869 }
10870 if meta.path.is_ident("auto_now_add") {
10871 out.auto_now_add = true;
10872 if out.default.is_none() {
10873 out.default = Some("now()".into());
10874 }
10875 return Ok(());
10876 }
10877 if meta.path.is_ident("auto_now") {
10878 out.auto_now = true;
10879 if out.default.is_none() {
10880 out.default = Some("now()".into());
10881 }
10882 return Ok(());
10883 }
10884 if meta.path.is_ident("soft_delete") {
10885 out.soft_delete = true;
10886 return Ok(());
10887 }
10888 if meta.path.is_ident("unique") {
10889 out.unique = true;
10890 return Ok(());
10891 }
10892 if meta.path.is_ident("index") {
10893 out.index = true;
10894 // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
10895 if meta.input.peek(syn::token::Paren) {
10896 meta.parse_nested_meta(|inner| {
10897 if inner.path.is_ident("unique") {
10898 out.index_unique = true;
10899 return Ok(());
10900 }
10901 if inner.path.is_ident("name") {
10902 let s: LitStr = inner.value()?.parse()?;
10903 out.index_name = Some(s.value());
10904 return Ok(());
10905 }
10906 if inner.path.is_ident("method") {
10907 let s: LitStr = inner.value()?.parse()?;
10908 let v = s.value();
10909 match v.as_str() {
10910 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
10911 out.index_method = v;
10912 }
10913 other => {
10914 return Err(inner.error(format!(
10915 "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
10916 )));
10917 }
10918 }
10919 return Ok(());
10920 }
10921 Err(inner.error(
10922 "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
10923 ))
10924 })?;
10925 }
10926 return Ok(());
10927 }
10928 Err(meta.error("unknown rustango field attribute"))
10929 })?;
10930 }
10931 Ok(out)
10932}
10933
10934/// Parse a signed integer literal, accepting optional leading `-`.
10935fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
10936 let expr: syn::Expr = meta.value()?.parse()?;
10937 match expr {
10938 syn::Expr::Lit(syn::ExprLit {
10939 lit: syn::Lit::Int(lit),
10940 ..
10941 }) => lit.base10_parse::<i64>(),
10942 syn::Expr::Unary(syn::ExprUnary {
10943 op: syn::UnOp::Neg(_),
10944 expr,
10945 ..
10946 }) => {
10947 if let syn::Expr::Lit(syn::ExprLit {
10948 lit: syn::Lit::Int(lit),
10949 ..
10950 }) = *expr
10951 {
10952 let v: i64 = lit.base10_parse()?;
10953 Ok(-v)
10954 } else {
10955 Err(syn::Error::new_spanned(expr, "expected integer literal"))
10956 }
10957 }
10958 other => Err(syn::Error::new_spanned(
10959 other,
10960 "expected integer literal (signed)",
10961 )),
10962 }
10963}
10964
10965struct FieldInfo<'a> {
10966 ident: &'a syn::Ident,
10967 column: String,
10968 primary_key: bool,
10969 /// `true` when the Rust type was `Auto<T>` — the INSERT path will
10970 /// skip this column when `Auto::Unset` and emit it under
10971 /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
10972 auto: bool,
10973 /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
10974 /// the `Column::Value` associated type for typed-column tokens.
10975 value_ty: &'a Type,
10976 /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
10977 field_type_tokens: TokenStream2,
10978 schema: TokenStream2,
10979 from_row_init: TokenStream2,
10980 /// Variant of [`Self::from_row_init`] that reads the column via
10981 /// `format!("{prefix}__{col}")` so a model can be decoded out of
10982 /// the aliased columns of a JOINed row. Drives slice 9.0d's
10983 /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
10984 /// helper that `select_related` calls when stitching loaded FKs.
10985 from_aliased_row_init: TokenStream2,
10986 /// MySQL twins of the two inits above. They differ only for a UUID
10987 /// column, which MySQL keeps as `CHAR(36)` text (#1733).
10988 from_row_init_my: TokenStream2,
10989 from_aliased_row_init_my: TokenStream2,
10990 /// A bare or optional `Uuid` field.
10991 uuid_column: bool,
10992 /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
10993 /// relation helper emit (`Author::<child>_set`) needs to know `T`
10994 /// to point the generated method at the right child model.
10995 fk_inner: Option<Type>,
10996 /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
10997 /// `kind` (since ForeignKey detection sets `kind` to K's
10998 /// underlying type) but stored separately for clarity at the
10999 /// `FkRelation` construction site, which only sees the FK's
11000 /// surface fields.
11001 fk_pk_kind: DetectedKind,
11002 /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
11003 /// the bare `ForeignKey<T, K>`. Routes the load_related and
11004 /// fk_pk_access emitters to wrap assignments / accessors in
11005 /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
11006 /// FK column compiles end-to-end. The DDL writer reads this off
11007 /// the field schema (`nullable` flag); the macro just needs to
11008 /// keep the Rust-side codegen consistent.
11009 nullable: bool,
11010 /// `true` when this column was marked `#[rustango(auto_now)]` —
11011 /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
11012 /// column instead of the user-supplied value, so `updated_at`
11013 /// always reflects the latest write without the caller having
11014 /// to remember to set it.
11015 auto_now: bool,
11016 /// `true` when this column was marked `#[rustango(auto_now_add)]`
11017 /// — the column is server-set on INSERT (DB DEFAULT) and
11018 /// **immutable** afterwards. `update_on` / `save_on` skip the
11019 /// column entirely so a stale `created_at` value in memory never
11020 /// rewrites the persisted timestamp.
11021 auto_now_add: bool,
11022 /// `true` when this column was marked `#[rustango(soft_delete)]`.
11023 /// Triggers emission of `soft_delete_on(executor)` and
11024 /// `restore_on(executor)` on the model's inherent impl. There is
11025 /// at most one such column per model — emission asserts this.
11026 soft_delete: bool,
11027 /// `Some` when this column was marked
11028 /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
11029 /// every INSERT and UPDATE path; the database recomputes the
11030 /// value from `EXPR`. Backlog item #35.
11031 generated_as: Option<String>,
11032 /// `true` when this column was marked
11033 /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
11034 /// emit an `insert_push` that auto-fills an `Auto::Unset` value
11035 /// with `Uuid::now_v7()` before binding, so the PK is generated
11036 /// Rust-side and the column is always present in the INSERT
11037 /// statement (no DB DEFAULT requirement). Issue #823.
11038 default_uuid_v7: bool,
11039 /// `Some` when this FK field carried `#[rustango(related_name =
11040 /// "...")]`. Threaded into [`FkRelation::related_name`] and
11041 /// consumed by [`reverse_helper_tokens`] to override the default
11042 /// `<child_snake>_set` accessor name. Follow-up to #816.
11043 related_name: Option<String>,
11044}
11045
11046/// Reject table names that won't survive SQL identifier
11047/// derivation downstream. Postgres' regular-identifier rule
11048/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
11049/// through the framework's unquoted FK / index / constraint name
11050/// emission without surprises. We also disallow leading-digit and
11051/// the empty string for clarity.
11052///
11053/// Reserved-word collisions (`select`, `from`, …) aren't flagged
11054/// here — those produce a runtime error from the SQL parser,
11055/// which is loud enough; statically enumerating reserved words
11056/// across the three supported dialects is more friction than help.
11057///
11058/// Backlog item #65.
11059fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11060 validate_sql_identifier(name, "table", span)
11061}
11062
11063/// Reject SQL identifiers that compile but break downstream SQL
11064/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11065/// `kind` is "table" / "column" — used for the error message so users
11066/// see which attribute caused the failure.
11067fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11068 if name.is_empty() {
11069 return Err(syn::Error::new(
11070 span,
11071 format!("`{kind} = \"\"` is not a valid SQL identifier"),
11072 ));
11073 }
11074 let mut chars = name.chars();
11075 let first = chars.next().unwrap();
11076 if !(first.is_ascii_alphabetic() || first == '_') {
11077 return Err(syn::Error::new(
11078 span,
11079 format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11080 ));
11081 }
11082 for c in chars {
11083 if !(c.is_ascii_alphanumeric() || c == '_') {
11084 return Err(syn::Error::new(
11085 span,
11086 format!(
11087 "{kind} name `{name}` contains invalid character {c:?} — \
11088 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11089 Hyphens in particular break FK / index name derivation \
11090 downstream; use underscores instead (e.g. `{}`)",
11091 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11092 ),
11093 ));
11094 }
11095 }
11096 Ok(())
11097}
11098
11099fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11100 let root = rustango_root();
11101 let attrs = parse_field_attrs(field)?;
11102 let ident = field
11103 .ident
11104 .as_ref()
11105 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11106 let name = ident_name(ident);
11107 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11108 let primary_key = attrs.primary_key;
11109 let DetectedType {
11110 kind,
11111 nullable,
11112 auto: detected_auto,
11113 fk_inner,
11114 } = detect_type(&field.ty)?;
11115 check_bound_compatibility(field, &attrs, kind)?;
11116 let auto = detected_auto;
11117 // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11118 // INSERT path: the user must wrap the field in `Auto<T>`, which
11119 // marks the column as DB-default-supplied. The mixin attrs then
11120 // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11121 // for `auto_now`, force the value on UPDATE too.
11122 if attrs.auto_uuid {
11123 if kind != DetectedKind::Uuid {
11124 return Err(syn::Error::new_spanned(
11125 field,
11126 "`#[rustango(auto_uuid)]` requires the field type to be \
11127 `Auto<uuid::Uuid>`",
11128 ));
11129 }
11130 if !detected_auto {
11131 return Err(syn::Error::new_spanned(
11132 field,
11133 "`#[rustango(auto_uuid)]` requires the field type to be \
11134 wrapped in `Auto<...>` so the macro skips the column on \
11135 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11136 ));
11137 }
11138 }
11139 if attrs.default_uuid_v7 {
11140 if kind != DetectedKind::Uuid {
11141 return Err(syn::Error::new_spanned(
11142 field,
11143 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11144 `Auto<uuid::Uuid>`",
11145 ));
11146 }
11147 if !detected_auto {
11148 return Err(syn::Error::new_spanned(
11149 field,
11150 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11151 wrapped in `Auto<...>` so the macro can detect the \
11152 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11153 ));
11154 }
11155 if attrs.auto_uuid {
11156 return Err(syn::Error::new_spanned(
11157 field,
11158 "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11159 `#[rustango(auto_uuid)]` — the former generates the UUID \
11160 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11161 Pick one.",
11162 ));
11163 }
11164 }
11165 if attrs.auto_now_add || attrs.auto_now {
11166 if kind != DetectedKind::DateTime {
11167 return Err(syn::Error::new_spanned(
11168 field,
11169 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11170 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11171 ));
11172 }
11173 if !detected_auto {
11174 return Err(syn::Error::new_spanned(
11175 field,
11176 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11177 the field type to be wrapped in `Auto<...>` so the macro skips \
11178 the column on INSERT and the DB DEFAULT (`now()`) fires",
11179 ));
11180 }
11181 }
11182 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11183 return Err(syn::Error::new_spanned(
11184 field,
11185 "`#[rustango(soft_delete)]` requires the field type to be \
11186 `Option<chrono::DateTime<chrono::Utc>>`",
11187 ));
11188 }
11189 let is_mixin_auto =
11190 attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11191 if detected_auto && !primary_key && !is_mixin_auto {
11192 return Err(syn::Error::new_spanned(
11193 field,
11194 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11195 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11196 `auto_now`",
11197 ));
11198 }
11199 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11200 return Err(syn::Error::new_spanned(
11201 field,
11202 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11203 SERIAL / BIGSERIAL already supplies a default sequence.",
11204 ));
11205 }
11206 if fk_inner.is_some() && primary_key {
11207 return Err(syn::Error::new_spanned(
11208 field,
11209 "`ForeignKey<T>` is not allowed on a primary-key field — \
11210 a row's PK is its own identity, not a reference to a parent.",
11211 ));
11212 }
11213 if attrs.generated_as.is_some() {
11214 if primary_key {
11215 return Err(syn::Error::new_spanned(
11216 field,
11217 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11218 primary-key field — a PK must be writable so the row \
11219 has an identity at INSERT time.",
11220 ));
11221 }
11222 if attrs.default.is_some() {
11223 return Err(syn::Error::new_spanned(
11224 field,
11225 "`#[rustango(generated_as = \"…\")]` cannot combine with \
11226 `default = \"…\"` — Postgres rejects DEFAULT on \
11227 generated columns. The expression IS the default.",
11228 ));
11229 }
11230 if detected_auto {
11231 return Err(syn::Error::new_spanned(
11232 field,
11233 "`#[rustango(generated_as = \"…\")]` is not allowed on \
11234 an `Auto<T>` field — generated columns are computed \
11235 by the DB, not server-assigned via a sequence. Use a \
11236 plain Rust type (e.g. `f64`).",
11237 ));
11238 }
11239 if fk_inner.is_some() {
11240 return Err(syn::Error::new_spanned(
11241 field,
11242 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11243 ForeignKey field.",
11244 ));
11245 }
11246 }
11247 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11248 let column_lit = column.as_str();
11249 // pgvector (#824): the `vector(dims = N)` attribute supplies the
11250 // dimension that the bare `Vector` Rust type can't carry, so emit
11251 // `FieldType::Vector(N)` here rather than the `variant_tokens`
11252 // fallback of `Vector(0)`.
11253 let field_type_tokens = if kind == DetectedKind::Vector {
11254 let root = rustango_root();
11255 let dims = attrs.vector_dims.unwrap_or(0);
11256 quote!(#root::core::FieldType::Vector(#dims))
11257 } else if kind == DetectedKind::Geometry {
11258 // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11259 // SRID that the bare `Point` Rust type can't carry, so emit
11260 // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11261 let root = rustango_root();
11262 let srid = attrs.geometry_srid.unwrap_or(0);
11263 quote!(#root::core::FieldType::Geometry(#srid))
11264 } else {
11265 kind.variant_tokens()
11266 };
11267 let max_length = optional_u32(attrs.max_length);
11268 let min = optional_i64(attrs.min);
11269 let max = optional_i64(attrs.max);
11270 let default = optional_str(attrs.default.as_deref());
11271
11272 let unique = attrs.unique;
11273 let generated_as = optional_str(attrs.generated_as.as_deref());
11274 let help_text = optional_str(attrs.help_text.as_deref());
11275 let choices = optional_choices(attrs.choices.as_deref());
11276 let db_comment = optional_str(attrs.db_comment.as_deref());
11277 let verbose_name = optional_str(attrs.verbose_name.as_deref());
11278 let editable = attrs.editable;
11279 let blank = attrs.blank;
11280 let case_insensitive = attrs.case_insensitive;
11281 let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11282 if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() {
11283 return Err(syn::Error::new_spanned(
11284 field,
11285 "`#[rustango(on_delete = \"…\")]` requires either `fk = \"<table>\"` \
11286 or `o2o = \"<table>\"` on the same field — it has no meaning on a \
11287 non-FK column.",
11288 ));
11289 }
11290 let fk_on_delete = match attrs.on_delete.as_deref() {
11291 None => quote!(::core::option::Option::None),
11292 Some(action) => {
11293 let variant = match action {
11294 "cascade" => quote!(Cascade),
11295 "restrict" => quote!(Restrict),
11296 "set_null" => quote!(SetNull),
11297 "set_default" => quote!(SetDefault),
11298 "no_action" => quote!(NoAction),
11299 // parse_field_attrs already validated this — guard against future drift.
11300 other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11301 };
11302 quote!(::core::option::Option::Some(
11303 #root::core::OnDeleteAction::#variant
11304 ))
11305 }
11306 };
11307 let auto_now_flag = attrs.auto_now;
11308 let schema = quote! {{
11309 #root::core::FieldSchema::from_parts(#root::core::FieldSchemaParts {
11310 name: #name,
11311 column: #column_lit,
11312 ty: #field_type_tokens,
11313 nullable: #nullable,
11314 primary_key: #primary_key,
11315 relation: #relation,
11316 max_length: #max_length,
11317 min: #min,
11318 max: #max,
11319 default: #default,
11320 auto: #auto,
11321 auto_now: #auto_now_flag,
11322 unique: #unique,
11323 generated_as: #generated_as,
11324 help_text: #help_text,
11325 choices: #choices,
11326 db_comment: #db_comment,
11327 verbose_name: #verbose_name,
11328 editable: #editable,
11329 blank: #blank,
11330 case_insensitive: #case_insensitive,
11331 fk_on_delete: #fk_on_delete,
11332 validators: &[ #(#validators_lits),* ],
11333 })
11334 }};
11335
11336 let from_row_init = quote! {
11337 #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11338 };
11339 let from_aliased_row_init = quote! {
11340 #ident: #root::sql::sqlx::Row::try_get(
11341 row,
11342 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11343 )?
11344 };
11345 // `Auto<Uuid>` and `ForeignKey<_, Uuid>` decode through their own
11346 // MySQL impls; a bare or optional `Uuid` needs `FlatScalar`'s cell.
11347 let uuid_column = kind == DetectedKind::Uuid && !detected_auto && fk_inner.is_none();
11348 let (from_row_init_my, from_aliased_row_init_my) = if uuid_column {
11349 (
11350 quote! { #ident: #root::sql::try_get_flat_my(row, #column_lit)? },
11351 quote! {
11352 #ident: #root::sql::try_get_flat_my(
11353 row,
11354 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11355 )?
11356 },
11357 )
11358 } else {
11359 (from_row_init.clone(), from_aliased_row_init.clone())
11360 };
11361
11362 Ok(FieldInfo {
11363 ident,
11364 column,
11365 primary_key,
11366 auto,
11367 value_ty: &field.ty,
11368 field_type_tokens,
11369 schema,
11370 from_row_init,
11371 from_aliased_row_init,
11372 from_row_init_my,
11373 from_aliased_row_init_my,
11374 uuid_column,
11375 fk_inner: fk_inner.cloned(),
11376 fk_pk_kind: kind,
11377 nullable,
11378 auto_now: attrs.auto_now,
11379 auto_now_add: attrs.auto_now_add,
11380 soft_delete: attrs.soft_delete,
11381 generated_as: attrs.generated_as.clone(),
11382 default_uuid_v7: attrs.default_uuid_v7,
11383 related_name: attrs.related_name.clone(),
11384 })
11385}
11386
11387fn check_bound_compatibility(
11388 field: &syn::Field,
11389 attrs: &FieldAttrs,
11390 kind: DetectedKind,
11391) -> syn::Result<()> {
11392 if attrs.max_length.is_some() && kind != DetectedKind::String {
11393 return Err(syn::Error::new_spanned(
11394 field,
11395 "`max_length` is only valid on `String` fields (or `Option<String>`)",
11396 ));
11397 }
11398 // These were silently dropped on the wrong type (#1937).
11399 if attrs.case_insensitive && kind != DetectedKind::String {
11400 return Err(syn::Error::new_spanned(
11401 field,
11402 "`citext` is only valid on `String` fields (or `Option<String>`)",
11403 ));
11404 }
11405 if attrs.vector_dims.is_some() && kind != DetectedKind::Vector {
11406 return Err(syn::Error::new_spanned(
11407 field,
11408 "`vector(dims = …)` is only valid on a pgvector `Vector` field",
11409 ));
11410 }
11411 if attrs.geometry_srid.is_some() && kind != DetectedKind::Geometry {
11412 return Err(syn::Error::new_spanned(
11413 field,
11414 "`geometry(srid = …)` is only valid on a PostGIS `Point` field",
11415 ));
11416 }
11417 if attrs.choices.is_some() && kind != DetectedKind::String {
11418 return Err(syn::Error::new_spanned(
11419 field,
11420 "`choices` is only valid on `String` fields (or `Option<String>`) — \
11421 integer-valued enumerations should be modeled with a Rust enum and \
11422 custom (de)serializer for now",
11423 ));
11424 }
11425 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11426 return Err(syn::Error::new_spanned(
11427 field,
11428 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11429 ));
11430 }
11431 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11432 if min > max {
11433 return Err(syn::Error::new_spanned(
11434 field,
11435 format!("`min` ({min}) is greater than `max` ({max})"),
11436 ));
11437 }
11438 }
11439 Ok(())
11440}
11441
11442/// A `&'static [ty]` of `const fn` results. The slice needs its own
11443/// `const`: a `&[..]` assigned in a statement is not extended to `'static`.
11444fn const_slice(ty: &TokenStream2, items: impl Iterator<Item = TokenStream2>) -> TokenStream2 {
11445 quote!({
11446 const ITEMS: &[#ty] = &[ #(#items),* ];
11447 ITEMS
11448 })
11449}
11450
11451fn optional_u32(value: Option<u32>) -> TokenStream2 {
11452 if let Some(v) = value {
11453 quote!(::core::option::Option::Some(#v))
11454 } else {
11455 quote!(::core::option::Option::None)
11456 }
11457}
11458
11459fn optional_i64(value: Option<i64>) -> TokenStream2 {
11460 if let Some(v) = value {
11461 quote!(::core::option::Option::Some(#v))
11462 } else {
11463 quote!(::core::option::Option::None)
11464 }
11465}
11466
11467fn optional_str(value: Option<&str>) -> TokenStream2 {
11468 if let Some(v) = value {
11469 quote!(::core::option::Option::Some(#v))
11470 } else {
11471 quote!(::core::option::Option::None)
11472 }
11473}
11474
11475fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11476 let Some(pairs) = pairs else {
11477 return quote!(::core::option::Option::None);
11478 };
11479 let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11480 quote!(::core::option::Option::Some(&[#(#entries),*]))
11481}
11482
11483fn relation_tokens(
11484 field: &syn::Field,
11485 attrs: &FieldAttrs,
11486 fk_inner: Option<&syn::Type>,
11487 table: &str,
11488) -> syn::Result<TokenStream2> {
11489 let root = rustango_root();
11490 if let Some(inner) = fk_inner {
11491 if attrs.fk.is_some() || attrs.o2o.is_some() {
11492 return Err(syn::Error::new_spanned(
11493 field,
11494 "`ForeignKey<T>` already declares the FK target via the type parameter — \
11495 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11496 ));
11497 }
11498 let on = attrs.on.as_deref().unwrap_or("id");
11499 return Ok(quote! {
11500 ::core::option::Option::Some(#root::core::Relation::fk(
11501 <#inner as #root::core::Model>::SCHEMA.table,
11502 #on,
11503 ))
11504 });
11505 }
11506 match (&attrs.fk, &attrs.o2o) {
11507 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11508 field,
11509 "`fk` and `o2o` are mutually exclusive",
11510 )),
11511 (Some(to), None) => {
11512 let on = attrs.on.as_deref().unwrap_or("id");
11513 // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11514 // the model's own table. Threaded as a literal string at
11515 // macro-expansion time to sidestep the const-eval cycle
11516 // that `Self::SCHEMA.table` would create when referenced
11517 // inside Self::SCHEMA's own initializer.
11518 let resolved = if to == "self" { table } else { to };
11519 Ok(quote! {
11520 ::core::option::Option::Some(#root::core::Relation::fk(#resolved, #on))
11521 })
11522 }
11523 (None, Some(to)) => {
11524 let on = attrs.on.as_deref().unwrap_or("id");
11525 let resolved = if to == "self" { table } else { to };
11526 Ok(quote! {
11527 ::core::option::Option::Some(#root::core::Relation::o2o(#resolved, #on))
11528 })
11529 }
11530 (None, None) => {
11531 if attrs.on.is_some() {
11532 return Err(syn::Error::new_spanned(
11533 field,
11534 "`on` requires `fk` or `o2o`",
11535 ));
11536 }
11537 Ok(quote!(::core::option::Option::None))
11538 }
11539 }
11540}
11541
11542/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11543/// about kinds without depending on `rustango-core` (which would require a
11544/// proc-macro/normal split it doesn't have today).
11545#[derive(Clone, Copy, PartialEq, Eq)]
11546enum DetectedKind {
11547 I16,
11548 I32,
11549 I64,
11550 F32,
11551 F64,
11552 Bool,
11553 String,
11554 DateTime,
11555 Date,
11556 Time,
11557 Uuid,
11558 Json,
11559 Decimal,
11560 Binary,
11561 /// `Array<String>` → PG `text[]` (#341).
11562 ArrayText,
11563 /// `Array<i32>` → PG `integer[]` (#341).
11564 ArrayInt,
11565 /// `Array<i64>` → PG `bigint[]` (#341).
11566 ArrayBigInt,
11567 /// `Range<i32>` → PG `int4range` (#343).
11568 RangeInt,
11569 /// `Range<i64>` → PG `int8range` (#343).
11570 RangeBigInt,
11571 /// `Range<Decimal>` → PG `numrange` (#343).
11572 RangeNumeric,
11573 /// `Range<NaiveDate>` → PG `daterange` (#343).
11574 RangeDate,
11575 /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11576 RangeDateTime,
11577 /// `HStore` → PG `hstore` (#342).
11578 HStore,
11579 /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11580 /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11581 /// in at the `FieldType` emission site (not carried on this enum).
11582 Vector,
11583 /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11584 /// from the `#[rustango(geometry(srid = N))]` field attribute,
11585 /// threaded in at the `FieldType` emission site.
11586 Geometry,
11587}
11588
11589impl DetectedKind {
11590 fn variant_tokens(self) -> TokenStream2 {
11591 let root = rustango_root();
11592 match self {
11593 Self::I16 => quote!(#root::core::FieldType::I16),
11594 Self::I32 => quote!(#root::core::FieldType::I32),
11595 Self::I64 => quote!(#root::core::FieldType::I64),
11596 Self::F32 => quote!(#root::core::FieldType::F32),
11597 Self::F64 => quote!(#root::core::FieldType::F64),
11598 Self::Bool => quote!(#root::core::FieldType::Bool),
11599 Self::String => quote!(#root::core::FieldType::String),
11600 Self::DateTime => quote!(#root::core::FieldType::DateTime),
11601 Self::Date => quote!(#root::core::FieldType::Date),
11602 Self::Time => quote!(#root::core::FieldType::Time),
11603 Self::Uuid => quote!(#root::core::FieldType::Uuid),
11604 Self::Json => quote!(#root::core::FieldType::Json),
11605 Self::Decimal => quote!(#root::core::FieldType::Decimal),
11606 Self::Binary => quote!(#root::core::FieldType::Binary),
11607 Self::ArrayText => {
11608 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11609 }
11610 Self::ArrayInt => {
11611 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11612 }
11613 Self::ArrayBigInt => {
11614 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11615 }
11616 Self::RangeInt => {
11617 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11618 }
11619 Self::RangeBigInt => {
11620 quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11621 }
11622 Self::RangeNumeric => {
11623 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11624 }
11625 Self::RangeDate => {
11626 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11627 }
11628 Self::RangeDateTime => {
11629 quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11630 }
11631 Self::HStore => quote!(#root::core::FieldType::HStore),
11632 // Dimension comes from the `vector(dims = N)` attribute,
11633 // applied at the emission site; `0` here is just a fallback.
11634 Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11635 // SRID comes from the `geometry(srid = N)` attribute, applied
11636 // at the emission site; `0` here is just a fallback.
11637 Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11638 }
11639 }
11640
11641 fn is_integer(self) -> bool {
11642 matches!(self, Self::I16 | Self::I32 | Self::I64)
11643 }
11644
11645 /// `(SqlValue::<Variant>, default expr)` for emitting the
11646 /// `match SqlValue { … }` arm in `LoadRelated::__rustango_load_related`
11647 /// for a `ForeignKey<T, K>` FK whose K maps to `self`. The default
11648 /// fires only when the parent's `__rustango_pk_value` returns a
11649 /// different variant than expected, which is a compile-time bug —
11650 /// but we still need a value-typed fallback to keep the match
11651 /// total.
11652 fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
11653 let root = rustango_root();
11654 match self {
11655 Self::I16 => (quote!(I16), quote!(0i16)),
11656 Self::I32 => (quote!(I32), quote!(0i32)),
11657 Self::I64 => (quote!(I64), quote!(0i64)),
11658 Self::F32 => (quote!(F32), quote!(0f32)),
11659 Self::F64 => (quote!(F64), quote!(0f64)),
11660 Self::Bool => (quote!(Bool), quote!(false)),
11661 Self::String => (quote!(String), quote!(::std::string::String::new())),
11662 Self::DateTime => (
11663 quote!(DateTime),
11664 quote!(<#root::__chrono::DateTime<#root::__chrono::Utc> as ::std::default::Default>::default()),
11665 ),
11666 Self::Date => (
11667 quote!(Date),
11668 quote!(<#root::__chrono::NaiveDate as ::std::default::Default>::default()),
11669 ),
11670 Self::Time => (
11671 quote!(Time),
11672 quote!(<#root::__chrono::NaiveTime as ::std::default::Default>::default()),
11673 ),
11674 Self::Uuid => (quote!(Uuid), quote!(#root::__uuid::Uuid::nil())),
11675 Self::Json => (quote!(Json), quote!(#root::__serde_json::Value::Null)),
11676 Self::Decimal => (
11677 quote!(Decimal),
11678 quote!(<#root::__rust_decimal::Decimal as ::std::default::Default>::default()),
11679 ),
11680 Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
11681 // Arrays (#341) can never be a foreign-key primary key, so
11682 // this arm is never reached at runtime — but the match must
11683 // stay total. A bare empty `SqlValue::Array` is the dummy.
11684 Self::ArrayText | Self::ArrayInt | Self::ArrayBigInt => {
11685 (quote!(Array), quote!(::std::vec::Vec::new()))
11686 }
11687 // Ranges (#343) likewise can't be a FK PK — never reached.
11688 Self::RangeInt
11689 | Self::RangeBigInt
11690 | Self::RangeNumeric
11691 | Self::RangeDate
11692 | Self::RangeDateTime => (quote!(RangeLiteral), quote!(::std::string::String::new())),
11693 // HStore (#342) can't be a FK PK — never reached.
11694 Self::HStore => (quote!(HStore), quote!(::std::vec::Vec::new())),
11695 // Vector (#824) can't be a FK PK — never reached.
11696 Self::Vector => (quote!(Vector), quote!(::std::vec::Vec::new())),
11697 // Geometry (#443) can't be a FK PK — never reached (arm is
11698 // exhaustiveness-only; never interpolated into emitted code).
11699 Self::Geometry => (quote!(Geometry), quote!(::std::vec::Vec::new())),
11700 }
11701 }
11702}
11703
11704/// Result of walking a field's Rust type. `kind` is the underlying
11705/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11706/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11707/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11708/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11709#[derive(Clone, Copy)]
11710struct DetectedType<'a> {
11711 kind: DetectedKind,
11712 nullable: bool,
11713 auto: bool,
11714 fk_inner: Option<&'a syn::Type>,
11715}
11716
11717/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11718/// non-`Auto` types — the caller should already have routed Auto-only
11719/// codegen through this helper, so a `None` indicates a macro-internal
11720/// invariant break.
11721fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11722 let Type::Path(TypePath { path, qself: None }) = ty else {
11723 return None;
11724 };
11725 let last = path.segments.last()?;
11726 if last.ident != "Auto" {
11727 return None;
11728 }
11729 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11730 return None;
11731 };
11732 args.args.iter().find_map(|a| match a {
11733 syn::GenericArgument::Type(t) => Some(t),
11734 _ => None,
11735 })
11736}
11737
11738fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11739 let Type::Path(TypePath { path, qself: None }) = ty else {
11740 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11741 };
11742 let last = path
11743 .segments
11744 .last()
11745 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11746
11747 if last.ident == "Option" {
11748 let inner = generic_inner(ty, &last.arguments, "Option")?;
11749 let inner_det = detect_type(inner)?;
11750 if inner_det.nullable {
11751 return Err(syn::Error::new_spanned(
11752 ty,
11753 "nested Option is not supported",
11754 ));
11755 }
11756 if inner_det.auto {
11757 return Err(syn::Error::new_spanned(
11758 ty,
11759 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11760 ));
11761 }
11762 return Ok(DetectedType {
11763 nullable: true,
11764 ..inner_det
11765 });
11766 }
11767
11768 if last.ident == "Auto" {
11769 let inner = generic_inner(ty, &last.arguments, "Auto")?;
11770 let inner_det = detect_type(inner)?;
11771 if inner_det.auto {
11772 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11773 }
11774 if inner_det.nullable {
11775 return Err(syn::Error::new_spanned(
11776 ty,
11777 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11778 ));
11779 }
11780 if inner_det.fk_inner.is_some() {
11781 return Err(syn::Error::new_spanned(
11782 ty,
11783 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11784 ));
11785 }
11786 if !matches!(
11787 inner_det.kind,
11788 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11789 ) {
11790 return Err(syn::Error::new_spanned(
11791 ty,
11792 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11793 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11794 (DEFAULT now())",
11795 ));
11796 }
11797 return Ok(DetectedType {
11798 auto: true,
11799 ..inner_det
11800 });
11801 }
11802
11803 if last.ident == "ForeignKey" {
11804 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11805 // Resolve the FK column's underlying SQL type from `K`. When the
11806 // user wrote `ForeignKey<T>` without a key parameter, the type
11807 // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11808 // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11809 // recurse into K so the column DDL emits the right SQL type
11810 // (VARCHAR for String, UUID for Uuid, …) and the load_related
11811 // emitter knows which `SqlValue` variant to match.
11812 let kind = match key_ty {
11813 Some(k) => detect_type(k)?.kind,
11814 None => DetectedKind::I64,
11815 };
11816 return Ok(DetectedType {
11817 kind,
11818 nullable: false,
11819 auto: false,
11820 fk_inner: Some(inner),
11821 });
11822 }
11823
11824 let kind = match last.ident.to_string().as_str() {
11825 "i16" => DetectedKind::I16,
11826 "i32" => DetectedKind::I32,
11827 "i64" => DetectedKind::I64,
11828 "f32" => DetectedKind::F32,
11829 "f64" => DetectedKind::F64,
11830 "bool" => DetectedKind::Bool,
11831 "String" => DetectedKind::String,
11832 "DateTime" => DetectedKind::DateTime,
11833 "NaiveDate" => DetectedKind::Date,
11834 "NaiveTime" => DetectedKind::Time,
11835 "Uuid" => DetectedKind::Uuid,
11836 "Value" => DetectedKind::Json,
11837 "Decimal" => DetectedKind::Decimal,
11838 // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11839 // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11840 // — that would emit Binary DDL and decode-fail at runtime.
11841 "Vec" => {
11842 let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11843 if let Type::Path(TypePath { path, qself: None }) = inner {
11844 if let Some(seg) = path.segments.last() {
11845 if seg.ident == "u8" && seg.arguments.is_empty() {
11846 return Ok(DetectedType {
11847 kind: DetectedKind::Binary,
11848 nullable: false,
11849 auto: false,
11850 fk_inner: None,
11851 });
11852 }
11853 }
11854 }
11855 return Err(syn::Error::new_spanned(
11856 ty,
11857 "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11858 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11859 ));
11860 }
11861 // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11862 // `integer[]` / `bigint[]` (#341).
11863 "Array" => {
11864 let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11865 let elem = match inner {
11866 Type::Path(TypePath { path, qself: None }) => {
11867 path.segments.last().map(|s| s.ident.to_string())
11868 }
11869 _ => None,
11870 };
11871 let kind = match elem.as_deref() {
11872 Some("String") => DetectedKind::ArrayText,
11873 Some("i32") => DetectedKind::ArrayInt,
11874 Some("i64") => DetectedKind::ArrayBigInt,
11875 _ => {
11876 return Err(syn::Error::new_spanned(
11877 ty,
11878 "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
11879 `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
11880 ));
11881 }
11882 };
11883 return Ok(DetectedType {
11884 kind,
11885 nullable: false,
11886 auto: false,
11887 fk_inner: None,
11888 });
11889 }
11890 // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
11891 // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
11892 // `int8range` / `numrange` / `daterange` / `tstzrange` (#343).
11893 "Range" => {
11894 let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
11895 let elem = match inner {
11896 Type::Path(TypePath { path, qself: None }) => {
11897 path.segments.last().map(|s| s.ident.to_string())
11898 }
11899 _ => None,
11900 };
11901 let kind = match elem.as_deref() {
11902 Some("i32") => DetectedKind::RangeInt,
11903 Some("i64") => DetectedKind::RangeBigInt,
11904 Some("Decimal") => DetectedKind::RangeNumeric,
11905 Some("NaiveDate") => DetectedKind::RangeDate,
11906 Some("DateTime") => DetectedKind::RangeDateTime,
11907 _ => {
11908 return Err(syn::Error::new_spanned(
11909 ty,
11910 "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
11911 `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
11912 `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
11913 (→ tstzrange) are supported (#343)",
11914 ));
11915 }
11916 };
11917 return Ok(DetectedType {
11918 kind,
11919 nullable: false,
11920 auto: false,
11921 fk_inner: None,
11922 });
11923 }
11924 // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
11925 // (the `CastValue` impl bridges logical↔stored); the field's own
11926 // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
11927 // schema just needs `FieldType::String`.
11928 "Cast" => {
11929 return Ok(DetectedType {
11930 kind: DetectedKind::String,
11931 nullable: false,
11932 auto: false,
11933 fk_inner: None,
11934 });
11935 }
11936 // `HStore` → PG `hstore` (#342). No generic
11937 // parameter — always a string→string map.
11938 "HStore" => {
11939 return Ok(DetectedType {
11940 kind: DetectedKind::HStore,
11941 nullable: false,
11942 auto: false,
11943 fk_inner: None,
11944 });
11945 }
11946 // `Vector` → pgvector `vector(N)` (#824). The dimension is
11947 // supplied by `#[rustango(vector(dims = N))]`, not the type.
11948 "Vector" => {
11949 return Ok(DetectedType {
11950 kind: DetectedKind::Vector,
11951 nullable: false,
11952 auto: false,
11953 fk_inner: None,
11954 });
11955 }
11956 // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
11957 // supplied by `#[rustango(geometry(srid = N))]`, not the type.
11958 "Point" => {
11959 return Ok(DetectedType {
11960 kind: DetectedKind::Geometry,
11961 nullable: false,
11962 auto: false,
11963 fk_inner: None,
11964 });
11965 }
11966 other => {
11967 return Err(syn::Error::new_spanned(
11968 ty,
11969 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)"),
11970 ));
11971 }
11972 };
11973 Ok(DetectedType {
11974 kind,
11975 nullable: false,
11976 auto: false,
11977 fk_inner: None,
11978 })
11979}
11980
11981fn generic_inner<'a>(
11982 ty: &'a Type,
11983 arguments: &'a PathArguments,
11984 wrapper: &str,
11985) -> syn::Result<&'a Type> {
11986 let PathArguments::AngleBracketed(args) = arguments else {
11987 return Err(syn::Error::new_spanned(
11988 ty,
11989 format!("{wrapper} requires a generic argument"),
11990 ));
11991 };
11992 args.args
11993 .iter()
11994 .find_map(|a| match a {
11995 GenericArgument::Type(t) => Some(t),
11996 _ => None,
11997 })
11998 .ok_or_else(|| {
11999 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12000 })
12001}
12002
12003/// Like [`generic_inner`] but pulls *two* type args — the first is
12004/// required, the second is optional. Used by the `ForeignKey<T, K>`
12005/// detection where K defaults to `i64` when omitted.
12006fn generic_pair<'a>(
12007 ty: &'a Type,
12008 arguments: &'a PathArguments,
12009 wrapper: &str,
12010) -> syn::Result<(&'a Type, Option<&'a Type>)> {
12011 let PathArguments::AngleBracketed(args) = arguments else {
12012 return Err(syn::Error::new_spanned(
12013 ty,
12014 format!("{wrapper} requires a generic argument"),
12015 ));
12016 };
12017 let mut types = args.args.iter().filter_map(|a| match a {
12018 GenericArgument::Type(t) => Some(t),
12019 _ => None,
12020 });
12021 let first = types.next().ok_or_else(|| {
12022 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12023 })?;
12024 let second = types.next();
12025 Ok((first, second))
12026}
12027
12028fn to_snake_case(s: &str) -> String {
12029 let mut out = String::with_capacity(s.len() + 4);
12030 for (i, ch) in s.chars().enumerate() {
12031 if ch.is_ascii_uppercase() {
12032 if i > 0 {
12033 out.push('_');
12034 }
12035 out.push(ch.to_ascii_lowercase());
12036 } else {
12037 out.push(ch);
12038 }
12039 }
12040 out
12041}
12042
12043// ============================================================
12044// #[derive(Form)] — slice 8.4B
12045// ============================================================
12046
12047/// Per-field `#[form(...)]` attributes recognised by the derive.
12048#[derive(Default)]
12049struct FormFieldAttrs {
12050 min: Option<i64>,
12051 max: Option<i64>,
12052 min_length: Option<u32>,
12053 max_length: Option<u32>,
12054 /// `#[form(clean = "fn_name")]` — per-field clean hook.
12055 /// The named static method on the form struct is called after the
12056 /// field's typed parse + length/range checks; it gets the parsed
12057 /// value by reference and returns `Result<<FieldType>, String>`.
12058 /// On Ok, the returned value replaces the parsed one; on Err, the
12059 /// message is attached to the field error list. Issue #372.
12060 clean: Option<syn::Ident>,
12061}
12062
12063/// Container-level `#[form(...)]` attributes. Currently only the
12064/// cross-field `validate` hook (issue #373).
12065#[derive(Default)]
12066struct FormContainerAttrs {
12067 /// `#[form(validate = "fn_name")]` — whole-form clean hook.
12068 /// After every per-field parse succeeds, the named method on the
12069 /// form struct is called with `&self` and may return
12070 /// `Result<(), FormErrors>`. Errors merge into the field error
12071 /// list. Issue #373.
12072 validate: Option<syn::Ident>,
12073}
12074
12075/// Detected shape of a form field's Rust type.
12076#[derive(Clone, Copy)]
12077enum FormFieldKind {
12078 String,
12079 I16,
12080 I32,
12081 I64,
12082 F32,
12083 F64,
12084 Bool,
12085}
12086
12087impl FormFieldKind {
12088 fn parse_method(self) -> &'static str {
12089 match self {
12090 Self::I16 => "i16",
12091 Self::I32 => "i32",
12092 Self::I64 => "i64",
12093 Self::F32 => "f32",
12094 Self::F64 => "f64",
12095 // String + Bool don't go through `str::parse`; the codegen
12096 // handles them inline.
12097 Self::String | Self::Bool => "",
12098 }
12099 }
12100}
12101
12102fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
12103 let root = rustango_root();
12104 let struct_name = &input.ident;
12105
12106 let Data::Struct(data) = &input.data else {
12107 return Err(syn::Error::new_spanned(
12108 struct_name,
12109 "Form can only be derived on structs",
12110 ));
12111 };
12112 let Fields::Named(named) = &data.fields else {
12113 return Err(syn::Error::new_spanned(
12114 struct_name,
12115 "Form requires a struct with named fields",
12116 ));
12117 };
12118
12119 // #373 — container-level `#[form(validate = "fn")]` hook.
12120 let container = parse_form_container_attrs(input)?;
12121 let post_field_clean: Vec<TokenStream2> = Vec::new();
12122 let _ = post_field_clean;
12123
12124 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12125 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12126
12127 for field in &named.named {
12128 let ident = field
12129 .ident
12130 .as_ref()
12131 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12132 let attrs = parse_form_field_attrs(field)?;
12133 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12134
12135 let name_lit = ident_name(ident);
12136 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12137 // #372 — append the per-field `clean_<field>` call right after
12138 // the parse block when the attribute is set. The clean fn
12139 // takes &T and returns Result<T, String>; on Err we attach
12140 // the message to the field error list without aborting, so
12141 // the caller sees every field error at once.
12142 let clean_block = if let Some(clean_fn) = &attrs.clean {
12143 quote! {
12144 if __errors.fields().get(#name_lit).is_none() {
12145 match Self::#clean_fn(&#ident) {
12146 ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
12147 ::core::result::Result::Err(__msg) => {
12148 __errors.add(#name_lit, __msg);
12149 }
12150 }
12151 }
12152 }
12153 } else {
12154 quote! {}
12155 };
12156 field_blocks.push(quote! {
12157 #parse_block
12158 #clean_block
12159 });
12160 field_idents.push(ident);
12161 }
12162
12163 // #373 — after every per-field parse + clean succeeds, call the
12164 // cross-field validator if declared. Errors merge into the
12165 // outgoing FormErrors via the existing `FormErrors::merge` helper
12166 // (same primitive the serializer cross-field hook uses).
12167 let cross_field_call = if let Some(validate_fn) = &container.validate {
12168 quote! {
12169 if __errors.is_empty() {
12170 let __candidate = Self { #( #field_idents ),* };
12171 if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12172 __errors.merge(__other);
12173 }
12174 if !__errors.is_empty() {
12175 return ::core::result::Result::Err(__errors);
12176 }
12177 return ::core::result::Result::Ok(__candidate);
12178 }
12179 }
12180 } else {
12181 quote! {}
12182 };
12183
12184 Ok(quote! {
12185 impl #root::forms::Form for #struct_name {
12186 fn parse(
12187 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12188 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12189 let mut __errors = #root::forms::FormErrors::default();
12190 #( #field_blocks )*
12191 #cross_field_call
12192 if !__errors.is_empty() {
12193 return ::core::result::Result::Err(__errors);
12194 }
12195 ::core::result::Result::Ok(Self {
12196 #( #field_idents ),*
12197 })
12198 }
12199 }
12200 })
12201}
12202
12203fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12204 let mut out = FormContainerAttrs::default();
12205 for attr in &input.attrs {
12206 if !attr.path().is_ident("form") {
12207 continue;
12208 }
12209 attr.parse_nested_meta(|meta| {
12210 if meta.path.is_ident("validate") {
12211 let s: LitStr = meta.value()?.parse()?;
12212 out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12213 return Ok(());
12214 }
12215 Err(meta.error("unknown form container attribute (supported: `validate`)"))
12216 })?;
12217 }
12218 Ok(out)
12219}
12220
12221fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12222 let mut out = FormFieldAttrs::default();
12223 for attr in &field.attrs {
12224 if !attr.path().is_ident("form") {
12225 continue;
12226 }
12227 attr.parse_nested_meta(|meta| {
12228 if meta.path.is_ident("min") {
12229 let lit: syn::LitInt = meta.value()?.parse()?;
12230 out.min = Some(lit.base10_parse::<i64>()?);
12231 return Ok(());
12232 }
12233 if meta.path.is_ident("max") {
12234 let lit: syn::LitInt = meta.value()?.parse()?;
12235 out.max = Some(lit.base10_parse::<i64>()?);
12236 return Ok(());
12237 }
12238 if meta.path.is_ident("min_length") {
12239 let lit: syn::LitInt = meta.value()?.parse()?;
12240 out.min_length = Some(lit.base10_parse::<u32>()?);
12241 return Ok(());
12242 }
12243 if meta.path.is_ident("max_length") {
12244 let lit: syn::LitInt = meta.value()?.parse()?;
12245 out.max_length = Some(lit.base10_parse::<u32>()?);
12246 return Ok(());
12247 }
12248 if meta.path.is_ident("clean") {
12249 let s: LitStr = meta.value()?.parse()?;
12250 out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12251 return Ok(());
12252 }
12253 Err(meta.error(
12254 "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12255 ))
12256 })?;
12257 }
12258 Ok(out)
12259}
12260
12261fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12262 let Type::Path(TypePath { path, qself: None }) = ty else {
12263 return Err(syn::Error::new(
12264 span,
12265 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12266 ));
12267 };
12268 let last = path
12269 .segments
12270 .last()
12271 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12272
12273 if last.ident == "Option" {
12274 let inner = generic_inner(ty, &last.arguments, "Option")?;
12275 let (kind, nested) = detect_form_field(inner, span)?;
12276 if nested {
12277 return Err(syn::Error::new(
12278 span,
12279 "nested Option in Form fields is not supported",
12280 ));
12281 }
12282 return Ok((kind, true));
12283 }
12284
12285 let kind = match last.ident.to_string().as_str() {
12286 "String" => FormFieldKind::String,
12287 "i16" => FormFieldKind::I16,
12288 "i32" => FormFieldKind::I32,
12289 "i64" => FormFieldKind::I64,
12290 "f32" => FormFieldKind::F32,
12291 "f64" => FormFieldKind::F64,
12292 "bool" => FormFieldKind::Bool,
12293 other => {
12294 return Err(syn::Error::new(
12295 span,
12296 format!(
12297 "Form field type `{other}` is not supported in v0.8 — use String / \
12298 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12299 ),
12300 ));
12301 }
12302 };
12303 Ok((kind, false))
12304}
12305
12306#[allow(clippy::too_many_lines)]
12307fn render_form_field_parse(
12308 ident: &syn::Ident,
12309 name_lit: &str,
12310 kind: FormFieldKind,
12311 nullable: bool,
12312 attrs: &FormFieldAttrs,
12313) -> TokenStream2 {
12314 // Pull the raw &str from the payload. Uses variable name `data` to
12315 // match the new `Form::parse(data: &HashMap<…>)` signature.
12316 let lookup = quote! {
12317 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12318 };
12319
12320 let parsed_value = match kind {
12321 FormFieldKind::Bool => quote! {
12322 let __v: bool = match __raw {
12323 ::core::option::Option::None => false,
12324 ::core::option::Option::Some(__s) => !matches!(
12325 __s.to_ascii_lowercase().as_str(),
12326 "" | "false" | "0" | "off" | "no"
12327 ),
12328 };
12329 },
12330 FormFieldKind::String => {
12331 if nullable {
12332 quote! {
12333 let __v: ::core::option::Option<::std::string::String> = match __raw {
12334 ::core::option::Option::None => ::core::option::Option::None,
12335 ::core::option::Option::Some(__s) if __s.is_empty() => {
12336 ::core::option::Option::None
12337 }
12338 ::core::option::Option::Some(__s) => {
12339 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12340 }
12341 };
12342 }
12343 } else {
12344 quote! {
12345 let __v: ::std::string::String = match __raw {
12346 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12347 ::core::clone::Clone::clone(__s)
12348 }
12349 _ => {
12350 __errors.add(#name_lit, "This field is required.");
12351 ::std::string::String::new()
12352 }
12353 };
12354 }
12355 }
12356 }
12357 FormFieldKind::I16
12358 | FormFieldKind::I32
12359 | FormFieldKind::I64
12360 | FormFieldKind::F32
12361 | FormFieldKind::F64 => {
12362 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12363 let ty_lit = kind.parse_method();
12364 let default_val = match kind {
12365 FormFieldKind::I16 => quote! { 0i16 },
12366 FormFieldKind::I32 => quote! { 0i32 },
12367 FormFieldKind::I64 => quote! { 0i64 },
12368 FormFieldKind::F32 => quote! { 0f32 },
12369 FormFieldKind::F64 => quote! { 0f64 },
12370 _ => quote! { Default::default() },
12371 };
12372 if nullable {
12373 quote! {
12374 let __v: ::core::option::Option<#parse_ty> = match __raw {
12375 ::core::option::Option::None => ::core::option::Option::None,
12376 ::core::option::Option::Some(__s) if __s.is_empty() => {
12377 ::core::option::Option::None
12378 }
12379 ::core::option::Option::Some(__s) => {
12380 match __s.parse::<#parse_ty>() {
12381 ::core::result::Result::Ok(__n) => {
12382 ::core::option::Option::Some(__n)
12383 }
12384 ::core::result::Result::Err(__e) => {
12385 __errors.add(
12386 #name_lit,
12387 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12388 );
12389 ::core::option::Option::None
12390 }
12391 }
12392 }
12393 };
12394 }
12395 } else {
12396 quote! {
12397 let __v: #parse_ty = match __raw {
12398 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12399 match __s.parse::<#parse_ty>() {
12400 ::core::result::Result::Ok(__n) => __n,
12401 ::core::result::Result::Err(__e) => {
12402 __errors.add(
12403 #name_lit,
12404 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12405 );
12406 #default_val
12407 }
12408 }
12409 }
12410 _ => {
12411 __errors.add(#name_lit, "This field is required.");
12412 #default_val
12413 }
12414 };
12415 }
12416 }
12417 }
12418 };
12419
12420 let validators = render_form_validators(name_lit, kind, nullable, attrs);
12421
12422 quote! {
12423 // `mut` so the per-field `clean` hook (#372) can rewrite the
12424 // parsed value in-place when it returns Ok with a normalized
12425 // form (e.g. trim / lowercase).
12426 let mut #ident = {
12427 #lookup
12428 #parsed_value
12429 #validators
12430 __v
12431 };
12432 }
12433}
12434
12435fn render_form_validators(
12436 name_lit: &str,
12437 kind: FormFieldKind,
12438 nullable: bool,
12439 attrs: &FormFieldAttrs,
12440) -> TokenStream2 {
12441 let mut checks: Vec<TokenStream2> = Vec::new();
12442
12443 let val_ref = if nullable {
12444 quote! { __v.as_ref() }
12445 } else {
12446 quote! { ::core::option::Option::Some(&__v) }
12447 };
12448
12449 let is_string = matches!(kind, FormFieldKind::String);
12450 let is_numeric = matches!(
12451 kind,
12452 FormFieldKind::I16
12453 | FormFieldKind::I32
12454 | FormFieldKind::I64
12455 | FormFieldKind::F32
12456 | FormFieldKind::F64
12457 );
12458
12459 if is_string {
12460 if let Some(min_len) = attrs.min_length {
12461 let min_len_usize = min_len as usize;
12462 checks.push(quote! {
12463 if let ::core::option::Option::Some(__s) = #val_ref {
12464 if __s.chars().count() < #min_len_usize {
12465 __errors.add(
12466 #name_lit,
12467 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12468 );
12469 }
12470 }
12471 });
12472 }
12473 if let Some(max_len) = attrs.max_length {
12474 let max_len_usize = max_len as usize;
12475 checks.push(quote! {
12476 if let ::core::option::Option::Some(__s) = #val_ref {
12477 if __s.chars().count() > #max_len_usize {
12478 __errors.add(
12479 #name_lit,
12480 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12481 );
12482 }
12483 }
12484 });
12485 }
12486 }
12487
12488 if is_numeric {
12489 if let Some(min) = attrs.min {
12490 checks.push(quote! {
12491 if let ::core::option::Option::Some(__n) = #val_ref {
12492 if (*__n as f64) < (#min as f64) {
12493 __errors.add(
12494 #name_lit,
12495 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12496 );
12497 }
12498 }
12499 });
12500 }
12501 if let Some(max) = attrs.max {
12502 checks.push(quote! {
12503 if let ::core::option::Option::Some(__n) = #val_ref {
12504 if (*__n as f64) > (#max as f64) {
12505 __errors.add(
12506 #name_lit,
12507 ::std::format!("Ensure this value is less than or equal to {}.", #max),
12508 );
12509 }
12510 }
12511 });
12512 }
12513 }
12514
12515 quote! { #( #checks )* }
12516}
12517
12518// ============================================================
12519// #[derive(ViewSet)]
12520// ============================================================
12521
12522struct ViewSetAttrs {
12523 model: syn::Path,
12524 fields: Option<Vec<String>>,
12525 filter_fields: Vec<String>,
12526 search_fields: Vec<String>,
12527 /// (field_name, desc)
12528 ordering: Vec<(String, bool)>,
12529 page_size: Option<usize>,
12530 read_only: bool,
12531 allow_anonymous: bool,
12532 perms: ViewSetPermsAttrs,
12533 /// `#[viewset(serializer = SomeSerializer)]` — render list /
12534 /// retrieve / create responses through this `#[derive(Serializer)]`
12535 /// type instead of the default field-level projection (requires the
12536 /// `serializer` feature). Tri-dialect.
12537 serializer: Option<syn::Path>,
12538}
12539
12540#[derive(Default)]
12541struct ViewSetPermsAttrs {
12542 list: Vec<String>,
12543 retrieve: Vec<String>,
12544 create: Vec<String>,
12545 update: Vec<String>,
12546 destroy: Vec<String>,
12547}
12548
12549fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12550 let root = rustango_root();
12551 let struct_name = &input.ident;
12552
12553 // Must be a unit struct or an empty named struct.
12554 match &input.data {
12555 Data::Struct(s) => match &s.fields {
12556 Fields::Unit | Fields::Named(_) => {}
12557 Fields::Unnamed(_) => {
12558 return Err(syn::Error::new_spanned(
12559 struct_name,
12560 "ViewSet can only be derived on a unit struct or an empty named struct",
12561 ));
12562 }
12563 },
12564 _ => {
12565 return Err(syn::Error::new_spanned(
12566 struct_name,
12567 "ViewSet can only be derived on a struct",
12568 ));
12569 }
12570 }
12571
12572 let attrs = parse_viewset_attrs(input)?;
12573 let model_path = &attrs.model;
12574
12575 // `.fields(&[...])` call — None means skip (use all scalar fields).
12576 let fields_call = if let Some(ref fields) = attrs.fields {
12577 let lits = fields.iter().map(|f| f.as_str());
12578 quote!(.fields(&[ #(#lits),* ]))
12579 } else {
12580 quote!()
12581 };
12582
12583 let filter_fields_call = if attrs.filter_fields.is_empty() {
12584 quote!()
12585 } else {
12586 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12587 quote!(.filter_fields(&[ #(#lits),* ]))
12588 };
12589
12590 let search_fields_call = if attrs.search_fields.is_empty() {
12591 quote!()
12592 } else {
12593 let lits = attrs.search_fields.iter().map(|f| f.as_str());
12594 quote!(.search_fields(&[ #(#lits),* ]))
12595 };
12596
12597 let ordering_call = if attrs.ordering.is_empty() {
12598 quote!()
12599 } else {
12600 let pairs = attrs.ordering.iter().map(|(f, desc)| {
12601 let f = f.as_str();
12602 quote!((#f, #desc))
12603 });
12604 quote!(.ordering(&[ #(#pairs),* ]))
12605 };
12606
12607 let page_size_call = if let Some(n) = attrs.page_size {
12608 quote!(.page_size(#n))
12609 } else {
12610 quote!()
12611 };
12612
12613 let read_only_call = if attrs.read_only {
12614 quote!(.read_only())
12615 } else {
12616 quote!()
12617 };
12618 let allow_anonymous_call = if attrs.allow_anonymous {
12619 quote!(.allow_anonymous())
12620 } else {
12621 quote!()
12622 };
12623
12624 // `.serializer::<S>()` — reshape responses through a derived
12625 // serializer. Requires the downstream crate to enable the
12626 // `serializer` feature (the method is gated on it).
12627 let serializer_call = if let Some(ref ser) = attrs.serializer {
12628 quote!(.serializer::<#ser>())
12629 } else {
12630 quote!()
12631 };
12632
12633 let perms = &attrs.perms;
12634 let perms_call = if perms.list.is_empty()
12635 && perms.retrieve.is_empty()
12636 && perms.create.is_empty()
12637 && perms.update.is_empty()
12638 && perms.destroy.is_empty()
12639 {
12640 quote!()
12641 } else {
12642 let list_lits = perms.list.iter().map(|s| s.as_str());
12643 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12644 let create_lits = perms.create.iter().map(|s| s.as_str());
12645 let update_lits = perms.update.iter().map(|s| s.as_str());
12646 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12647 quote! {
12648 .permissions(#root::viewset::ViewSetPerms {
12649 list: ::std::vec![ #(#list_lits.to_owned()),* ],
12650 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12651 create: ::std::vec![ #(#create_lits.to_owned()),* ],
12652 update: ::std::vec![ #(#update_lits.to_owned()),* ],
12653 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
12654 })
12655 }
12656 };
12657
12658 Ok(quote! {
12659 impl #struct_name {
12660 /// Build an `axum::Router` with the six standard REST endpoints
12661 /// for this ViewSet, mounted at `prefix`.
12662 ///
12663 /// Accepts any backend's pool — a `sqlx::PgPool`,
12664 /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
12665 /// enum — because they all `Into<Pool>` (#1273). This is why
12666 /// the derive no longer names `PgPool`, which would not
12667 /// exist on a project built without the `postgres` feature.
12668 pub fn router(
12669 prefix: &str,
12670 pool: impl ::core::convert::Into<#root::sql::Pool>,
12671 ) -> #root::__axum::Router {
12672 #root::viewset::ViewSet::for_model(
12673 <#model_path as #root::core::Model>::SCHEMA
12674 )
12675 #fields_call
12676 #filter_fields_call
12677 #search_fields_call
12678 #ordering_call
12679 #page_size_call
12680 #perms_call
12681 #read_only_call
12682 #allow_anonymous_call
12683 #serializer_call
12684 .router_pool(prefix, pool.into())
12685 }
12686 }
12687 })
12688}
12689
12690fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
12691 let mut model: Option<syn::Path> = None;
12692 let mut fields: Option<Vec<String>> = None;
12693 let mut filter_fields: Vec<String> = Vec::new();
12694 let mut search_fields: Vec<String> = Vec::new();
12695 let mut ordering: Vec<(String, bool)> = Vec::new();
12696 let mut page_size: Option<usize> = None;
12697 let mut read_only = false;
12698 let mut allow_anonymous = false;
12699 let mut perms = ViewSetPermsAttrs::default();
12700 let mut serializer: Option<syn::Path> = None;
12701
12702 for attr in &input.attrs {
12703 if !attr.path().is_ident("viewset") {
12704 continue;
12705 }
12706 attr.parse_nested_meta(|meta| {
12707 if meta.path.is_ident("model") {
12708 let path: syn::Path = meta.value()?.parse()?;
12709 model = Some(path);
12710 return Ok(());
12711 }
12712 if meta.path.is_ident("serializer") {
12713 let path: syn::Path = meta.value()?.parse()?;
12714 serializer = Some(path);
12715 return Ok(());
12716 }
12717 if meta.path.is_ident("fields") {
12718 let s: LitStr = meta.value()?.parse()?;
12719 fields = Some(split_field_list(&s.value()));
12720 return Ok(());
12721 }
12722 if meta.path.is_ident("filter_fields") {
12723 let s: LitStr = meta.value()?.parse()?;
12724 filter_fields = split_field_list(&s.value());
12725 return Ok(());
12726 }
12727 if meta.path.is_ident("search_fields") {
12728 let s: LitStr = meta.value()?.parse()?;
12729 search_fields = split_field_list(&s.value());
12730 return Ok(());
12731 }
12732 if meta.path.is_ident("ordering") {
12733 let s: LitStr = meta.value()?.parse()?;
12734 ordering = parse_ordering_list(&s.value());
12735 return Ok(());
12736 }
12737 if meta.path.is_ident("page_size") {
12738 let lit: syn::LitInt = meta.value()?.parse()?;
12739 page_size = Some(lit.base10_parse::<usize>()?);
12740 return Ok(());
12741 }
12742 if meta.path.is_ident("read_only") {
12743 read_only = true;
12744 return Ok(());
12745 }
12746 if meta.path.is_ident("allow_anonymous") {
12747 allow_anonymous = true;
12748 return Ok(());
12749 }
12750 if meta.path.is_ident("permissions") {
12751 meta.parse_nested_meta(|inner| {
12752 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
12753 let s: LitStr = inner.value()?.parse()?;
12754 Ok(split_field_list(&s.value()))
12755 };
12756 if inner.path.is_ident("list") {
12757 perms.list = parse_codenames(&inner)?;
12758 } else if inner.path.is_ident("retrieve") {
12759 perms.retrieve = parse_codenames(&inner)?;
12760 } else if inner.path.is_ident("create") {
12761 perms.create = parse_codenames(&inner)?;
12762 } else if inner.path.is_ident("update") {
12763 perms.update = parse_codenames(&inner)?;
12764 } else if inner.path.is_ident("destroy") {
12765 perms.destroy = parse_codenames(&inner)?;
12766 } else {
12767 return Err(inner.error(
12768 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
12769 ));
12770 }
12771 Ok(())
12772 })?;
12773 return Ok(());
12774 }
12775 Err(meta.error(
12776 "unknown viewset attribute (supported: model, fields, filter_fields, \
12777 search_fields, ordering, page_size, read_only, allow_anonymous, serializer, \
12778 permissions(...))",
12779 ))
12780 })?;
12781 }
12782
12783 let model = model.ok_or_else(|| {
12784 syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
12785 })?;
12786
12787 Ok(ViewSetAttrs {
12788 model,
12789 fields,
12790 filter_fields,
12791 search_fields,
12792 ordering,
12793 page_size,
12794 read_only,
12795 allow_anonymous,
12796 perms,
12797 serializer,
12798 })
12799}
12800
12801// ============================================================ #[derive(Serializer)]
12802
12803struct SerializerContainerAttrs {
12804 model: syn::Path,
12805 /// `#[serializer(validate = "fn_name")]` on the struct — the
12806 /// cross-field validation hook (#436). The named inherent method
12807 /// must take `&self` and return
12808 /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
12809 /// `validate()` runs every per-field validator first; then calls
12810 /// the cross-field method if declared; aggregates all errors into
12811 /// one `FormErrors`.
12812 cross_validate: Option<syn::Ident>,
12813}
12814
12815#[derive(Default)]
12816struct SerializerFieldAttrs {
12817 read_only: bool,
12818 write_only: bool,
12819 source: Option<String>,
12820 skip: bool,
12821 /// `#[serializer(method = "fn_name")]` — computed field. The macro
12822 /// emits a `from_model` initializer that calls
12823 /// `Self::fn_name(&model)` and stores the return value.
12824 method: Option<String>,
12825 /// `#[serializer(validate = "fn_name")]` — per-field validator
12826 /// callable run by `Self::validate(&self)`. Must return
12827 /// `Result<(), String>`. Errors land in `FormErrors` keyed by
12828 /// the field name.
12829 validate: Option<String>,
12830 /// `#[serializer(nested)]` on a field whose type is another
12831 /// `Serializer` — the macro emits `from_model` initializer that
12832 /// reads the parent via `model.<source>.value()` then calls the
12833 /// child serializer's `from_model(parent)`. When the FK is
12834 /// unloaded the field falls back to `Default::default()` (does
12835 /// NOT panic) so a missing prefetch in prod degrades gracefully.
12836 /// Source field on the model defaults to the field name; override
12837 /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
12838 /// panic-on-unloaded behavior for tests.
12839 nested: bool,
12840 /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
12841 /// strict behavior: panic when the FK isn't loaded. Useful in
12842 /// test code where forgetting select_related must trip a hard
12843 /// failure rather than render a blank nested object.
12844 nested_strict: bool,
12845 /// `#[serializer(many = TagSerializer)]` — declare the field as
12846 /// a list of nested serializers. Field type must be `Vec<S>`
12847 /// where `S` is the inner serializer. The macro initializes the
12848 /// field to `Vec::new()` in `from_model` and emits a typed
12849 /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
12850 /// maps each model row through `S::from_model`. Auto-load isn't
12851 /// possible (the M2M / one-to-many accessor is async); callers
12852 /// fetch the children + call the setter post-from_model.
12853 many: Option<syn::Type>,
12854 /// `#[serializer(slug = "name")]` — render a relation as one of
12855 /// its fields.
12856 /// Source field on the model must be a `ForeignKey<T>`; the
12857 /// macro emits `from_model` glue that walks
12858 /// `model.<source>.value()?.<slug>` and clones it. Field type on
12859 /// the serializer is typically `String` (whatever type the slug
12860 /// column has). When the FK is unloaded the field falls back to
12861 /// `Default::default()`, same graceful-degrade contract as
12862 /// `nested`. Source defaults to the field name; override with
12863 /// `source = "..."`. v0.44.
12864 slug: Option<String>,
12865 /// `#[serializer(max_length = N)]` — caps
12866 /// the character count of a string field on write. Overrides the
12867 /// model's `max_length`; when absent the model value is inherited.
12868 max_length: Option<u64>,
12869 /// `#[serializer(min_length = N)]` — minimum character count.
12870 /// Serializer-only (the model has no `min_length` column).
12871 min_length: Option<u64>,
12872 /// `#[serializer(min = N)]` — inclusive
12873 /// integer lower bound; overrides the model's `min` when given.
12874 min: Option<i64>,
12875 /// `#[serializer(max = N)]` — inclusive
12876 /// integer upper bound; overrides the model's `max` when given.
12877 max: Option<i64>,
12878}
12879
12880fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
12881 let mut model: Option<syn::Path> = None;
12882 let mut cross_validate: Option<syn::Ident> = None;
12883 for attr in &input.attrs {
12884 if !attr.path().is_ident("serializer") {
12885 continue;
12886 }
12887 attr.parse_nested_meta(|meta| {
12888 if meta.path.is_ident("model") {
12889 let _eq: syn::Token![=] = meta.input.parse()?;
12890 model = Some(meta.input.parse()?);
12891 return Ok(());
12892 }
12893 if meta.path.is_ident("validate") {
12894 // #436 — container-level `validate = "fn_name"` for
12895 // cross-field validation. Field-level
12896 // `#[serializer(validate = "...")]` on a field is
12897 // parsed separately in `parse_serializer_field_attrs`.
12898 let s: LitStr = meta.value()?.parse()?;
12899 cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
12900 return Ok(());
12901 }
12902 Err(meta.error(
12903 "unknown serializer container attribute \
12904 (supported: `model`, `validate`)",
12905 ))
12906 })?;
12907 }
12908 let model = model.ok_or_else(|| {
12909 syn::Error::new_spanned(
12910 &input.ident,
12911 "`#[serializer(model = SomeModel)]` is required",
12912 )
12913 })?;
12914 Ok(SerializerContainerAttrs {
12915 model,
12916 cross_validate,
12917 })
12918}
12919
12920fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
12921 let mut out = SerializerFieldAttrs::default();
12922 for attr in &field.attrs {
12923 if !attr.path().is_ident("serializer") {
12924 continue;
12925 }
12926 attr.parse_nested_meta(|meta| {
12927 if meta.path.is_ident("read_only") {
12928 out.read_only = true;
12929 return Ok(());
12930 }
12931 if meta.path.is_ident("write_only") {
12932 out.write_only = true;
12933 return Ok(());
12934 }
12935 if meta.path.is_ident("skip") {
12936 out.skip = true;
12937 return Ok(());
12938 }
12939 if meta.path.is_ident("source") {
12940 let s: LitStr = meta.value()?.parse()?;
12941 out.source = Some(s.value());
12942 return Ok(());
12943 }
12944 if meta.path.is_ident("method") {
12945 let s: LitStr = meta.value()?.parse()?;
12946 out.method = Some(s.value());
12947 return Ok(());
12948 }
12949 if meta.path.is_ident("validate") {
12950 let s: LitStr = meta.value()?.parse()?;
12951 out.validate = Some(s.value());
12952 return Ok(());
12953 }
12954 if meta.path.is_ident("many") {
12955 let _eq: syn::Token![=] = meta.input.parse()?;
12956 out.many = Some(meta.input.parse()?);
12957 return Ok(());
12958 }
12959 if meta.path.is_ident("nested") {
12960 out.nested = true;
12961 // Optional strict flag inside parentheses:
12962 // #[serializer(nested(strict))]
12963 if meta.input.peek(syn::token::Paren) {
12964 meta.parse_nested_meta(|inner| {
12965 if inner.path.is_ident("strict") {
12966 out.nested_strict = true;
12967 return Ok(());
12968 }
12969 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
12970 })?;
12971 }
12972 return Ok(());
12973 }
12974 if meta.path.is_ident("slug") {
12975 let s: LitStr = meta.value()?.parse()?;
12976 out.slug = Some(s.value());
12977 return Ok(());
12978 }
12979 if meta.path.is_ident("max_length") {
12980 let lit: syn::LitInt = meta.value()?.parse()?;
12981 out.max_length = Some(lit.base10_parse::<u64>()?);
12982 return Ok(());
12983 }
12984 if meta.path.is_ident("min_length") {
12985 let lit: syn::LitInt = meta.value()?.parse()?;
12986 out.min_length = Some(lit.base10_parse::<u64>()?);
12987 return Ok(());
12988 }
12989 if meta.path.is_ident("min") {
12990 let lit: syn::LitInt = meta.value()?.parse()?;
12991 out.min = Some(lit.base10_parse::<i64>()?);
12992 return Ok(());
12993 }
12994 if meta.path.is_ident("max") {
12995 let lit: syn::LitInt = meta.value()?.parse()?;
12996 out.max = Some(lit.base10_parse::<i64>()?);
12997 return Ok(());
12998 }
12999 Err(meta.error(
13000 "unknown serializer field attribute (supported: \
13001 `read_only`, `write_only`, `source`, `skip`, `method`, \
13002 `validate`, `nested`, `many`, `slug`, `max_length`, \
13003 `min_length`, `min`, `max`)",
13004 ))
13005 })?;
13006 }
13007 // Validate: read_only + write_only is nonsensical
13008 if out.read_only && out.write_only {
13009 return Err(syn::Error::new_spanned(
13010 field,
13011 "a field cannot be both `read_only` and `write_only`",
13012 ));
13013 }
13014 if out.method.is_some() && out.source.is_some() {
13015 return Err(syn::Error::new_spanned(
13016 field,
13017 "`method` and `source` are mutually exclusive — `method` computes \
13018 the value from a method, `source` reads it from a different model field",
13019 ));
13020 }
13021 if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
13022 return Err(syn::Error::new_spanned(
13023 field,
13024 "`slug` is mutually exclusive with `method`, `nested`, and `many` \
13025 — pick one strategy for populating the field",
13026 ));
13027 }
13028 Ok(out)
13029}
13030
13031fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
13032 let root = rustango_root();
13033 let struct_name = &input.ident;
13034 let struct_name_lit = struct_name.to_string();
13035
13036 let Data::Struct(data) = &input.data else {
13037 return Err(syn::Error::new_spanned(
13038 struct_name,
13039 "Serializer can only be derived on structs",
13040 ));
13041 };
13042 let Fields::Named(named) = &data.fields else {
13043 return Err(syn::Error::new_spanned(
13044 struct_name,
13045 "Serializer requires a struct with named fields",
13046 ));
13047 };
13048
13049 let container = parse_serializer_container_attrs(input)?;
13050 let model_path = &container.model;
13051
13052 // Classify each field. `ty` is only consumed by the
13053 // `#[cfg(feature = "openapi")]` block below, but we always
13054 // capture it to keep the field-info build a single pass.
13055 #[allow(dead_code)]
13056 struct FieldInfo {
13057 ident: syn::Ident,
13058 ty: syn::Type,
13059 attrs: SerializerFieldAttrs,
13060 }
13061 let mut fields_info: Vec<FieldInfo> = Vec::new();
13062 for field in &named.named {
13063 let ident = field.ident.clone().expect("named field has ident");
13064 let attrs = parse_serializer_field_attrs(field)?;
13065 fields_info.push(FieldInfo {
13066 ident,
13067 ty: field.ty.clone(),
13068 attrs,
13069 });
13070 }
13071
13072 // Generate from_model body: struct literal with each field assigned.
13073 let from_model_fields = fields_info.iter().map(|fi| {
13074 let ident = &fi.ident;
13075 let ty = &fi.ty;
13076 if let Some(_inner) = &fi.attrs.many {
13077 // Many — collection field. Initialize empty; caller
13078 // populates via the macro-emitted set_<field> helper
13079 // after fetching the M2M children.
13080 quote! { #ident: ::std::vec::Vec::new() }
13081 } else if let Some(method) = &fi.attrs.method {
13082 // SerializerMethodField: call Self::<method>(&model) to
13083 // compute the value. Method signature must be
13084 // `fn <method>(model: &T) -> <field type>`.
13085 let method_ident = syn::Ident::new(method, ident.span());
13086 quote! { #ident: Self::#method_ident(model) }
13087 } else if let Some(slug_field) = &fi.attrs.slug {
13088 // v0.44 — SlugRelatedField. Source defaults to the field
13089 // name on this struct; override via `source = "..."`. The
13090 // source field on the model is expected to be a
13091 // `ForeignKey<T>`; the slug field on the parent is named
13092 // by the attribute value. When the FK is unloaded the
13093 // field falls back to `Default::default()` — same
13094 // graceful-degrade contract as `nested`.
13095 let src_name = fi
13096 .attrs
13097 .source
13098 .as_deref()
13099 .unwrap_or(&ident_name(&fi.ident))
13100 .to_owned();
13101 let src_ident = ident_from_name(&src_name, ident.span());
13102 let slug_ident = syn::Ident::new(slug_field, ident.span());
13103 quote! {
13104 #ident: match model.#src_ident.value() {
13105 ::core::option::Option::Some(__loaded) =>
13106 ::core::clone::Clone::clone(&__loaded.#slug_ident),
13107 ::core::option::Option::None =>
13108 ::core::default::Default::default(),
13109 }
13110 }
13111 } else if fi.attrs.nested {
13112 // Nested serializer. Source defaults to the field name on
13113 // this struct; override via `source = "..."`. The source
13114 // field on the model is expected to be a `ForeignKey<T>`
13115 // whose `.value()` returns `Option<&T>` after lazy-load.
13116 //
13117 // Behavior matrix (tweakable per-field):
13118 // * FK loaded → nested object materializes via
13119 // ChildSerializer::from_model(parent).
13120 // * FK unloaded → fall back to ChildSerializer::default()
13121 // (so prod doesn't crash on a missing
13122 // prefetch — just renders a blank nested
13123 // object). Add `#[serializer(nested,
13124 // strict)]` to keep the v0.18.1
13125 // panic-on-unloaded behavior for tests
13126 // that want hard guardrails.
13127 let src_name = fi.attrs.source.as_deref().unwrap_or(&ident_name(&fi.ident)).to_owned();
13128 let src_ident = ident_from_name(&src_name, ident.span());
13129 if fi.attrs.nested_strict {
13130 let panic_msg = format!(
13131 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
13132 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
13133 );
13134 quote! {
13135 #ident: <#ty as #root::serializer::ModelSerializer>::from_model(
13136 model.#src_ident.value().expect(#panic_msg),
13137 )
13138 }
13139 } else {
13140 quote! {
13141 #ident: match model.#src_ident.value() {
13142 ::core::option::Option::Some(__loaded) =>
13143 <#ty as #root::serializer::ModelSerializer>::from_model(__loaded),
13144 ::core::option::Option::None =>
13145 ::core::default::Default::default(),
13146 }
13147 }
13148 }
13149 } else if fi.attrs.write_only || fi.attrs.skip {
13150 // Not read from model — use default
13151 quote! { #ident: ::core::default::Default::default() }
13152 } else if let Some(src) = &fi.attrs.source {
13153 let src_ident = ident_from_name(src, ident.span());
13154 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13155 } else {
13156 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13157 }
13158 });
13159
13160 // is_writable predicate (also used by writable_lits /
13161 // writable_source_lits below).
13162 let is_writable = |fi: &&FieldInfo| {
13163 !fi.attrs.read_only
13164 && !fi.attrs.skip
13165 && fi.attrs.method.is_none()
13166 && !fi.attrs.nested
13167 && fi.attrs.many.is_none()
13168 && fi.attrs.slug.is_none()
13169 };
13170
13171 // Declarative field constraints: one block
13172 // per writable field, run inside `validate()`. Each resolves its
13173 // bounds as the serializer attr when given (`#[serializer(max_length
13174 // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13175 // `max` / `choices`), then dispatches on the field's JSON value via
13176 // `forms::validators::check_value` (string → length/choices, integer
13177 // → min/max).
13178 let opt_usize = |v: Option<u64>| match v {
13179 Some(n) => {
13180 let n = n as usize;
13181 quote!(::core::option::Option::Some(#n))
13182 }
13183 None => quote!(::core::option::Option::None),
13184 };
13185 let opt_i64 = |v: Option<i64>| match v {
13186 Some(n) => quote!(::core::option::Option::Some(#n)),
13187 None => quote!(::core::option::Option::None),
13188 };
13189 let constraint_blocks: Vec<_> = fields_info
13190 .iter()
13191 .filter(is_writable)
13192 .map(|fi| {
13193 let ident = &fi.ident;
13194 let fname = ident_name(ident);
13195 let mname = fi
13196 .attrs
13197 .source
13198 .clone()
13199 .unwrap_or_else(|| ident_name(&fi.ident));
13200 let attr_max_len = opt_usize(fi.attrs.max_length);
13201 let attr_min_len = opt_usize(fi.attrs.min_length);
13202 let attr_min = opt_i64(fi.attrs.min);
13203 let attr_max = opt_i64(fi.attrs.max);
13204 quote! {
13205 if __sent(#fname) {
13206 let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13207 let __max_length: ::core::option::Option<usize> = #attr_max_len
13208 .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13209 let __min_length: ::core::option::Option<usize> = #attr_min_len;
13210 let __min: ::core::option::Option<i64> =
13211 #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13212 let __max: ::core::option::Option<i64> =
13213 #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13214 let __choices = __sf.and_then(|__f| __f.choices);
13215 let __v = #root::__serde_json::to_value(&self.#ident)
13216 .unwrap_or(#root::__serde_json::Value::Null);
13217 #root::forms::validators::check_value(
13218 #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13219 &mut __errors,
13220 );
13221 }
13222 }
13223 })
13224 .collect();
13225 let has_constraints = !constraint_blocks.is_empty();
13226
13227 // Per-field validators. Emit a
13228 // `validate(&self)` method that runs each user-defined validator
13229 // and aggregates errors into `FormErrors`.
13230 let validator_calls: Vec<_> = fields_info
13231 .iter()
13232 .filter_map(|fi| {
13233 let ident = &fi.ident;
13234 let name_lit = ident_name(ident);
13235 let method = fi.attrs.validate.as_ref()?;
13236 let method_ident = syn::Ident::new(method, ident.span());
13237 Some(quote! {
13238 if __sent(#name_lit) {
13239 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13240 __errors.add(#name_lit.to_owned(), __e);
13241 }
13242 }
13243 })
13244 })
13245 .collect();
13246 // #436 — cross-field `validate(&self)`. If the
13247 // container declared `#[serializer(validate = "fn_name")]`,
13248 // the macro-generated `validate(&self)` runs every per-field
13249 // validator first, then calls the user's cross-field method,
13250 // merging its `FormErrors` into the per-field errors. Either
13251 // alone is enough to emit the wrapper.
13252 let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13253 quote! {
13254 // Merge cross-field errors into the per-field bucket so
13255 // a single .validate() call surfaces both layers.
13256 if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13257 __errors.merge(__cross);
13258 }
13259 }
13260 });
13261 let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13262 // Anything to run? Declarative constraints (for any writable field) +
13263 // per-field validators + cross-field hook.
13264 let has_run_validations = has_validators || has_constraints;
13265 // Shared body: declarative field constraints first (length / range /
13266 // choices, serializer-attr-or-model), then per-field validators, then
13267 // the cross-field hook. Field rules run only where `__sent(name)` (#1995).
13268 let checks = quote! {
13269 let mut __errors = #root::forms::FormErrors::default();
13270 #( #constraint_blocks )*
13271 #( #validator_calls )*
13272 #cross_validate_call
13273 if __errors.is_empty() {
13274 ::core::result::Result::Ok(())
13275 } else {
13276 ::core::result::Result::Err(__errors)
13277 }
13278 };
13279 let validate_body = quote! {
13280 let __sent = |_: &str| true;
13281 #checks
13282 };
13283 // PATCH: only the keys the body carries.
13284 let validate_patch_body = quote! {
13285 let __sent = |__n: &str| __obj.is_some_and(|__o| __o.contains_key(__n));
13286 #checks
13287 };
13288 // Inherent `validate(&self)` — kept for back-compat with direct
13289 // `serializer.validate()` calls that don't import `ModelSerializer`.
13290 // Emitted only when the serializer declares per-field/cross-field
13291 // validators (unchanged rule) so it never collides with a
13292 // hand-written inherent `validate`.
13293 let validate_method = if has_validators {
13294 quote! {
13295 impl #struct_name {
13296 /// Run the declarative field constraints, every
13297 /// `#[serializer(validate = "...")]` per-field validator,
13298 /// and (when declared) the container-level cross-field
13299 /// validator. Aggregates errors into `FormErrors` keyed by
13300 /// the field name. Returns `Ok(())` when all pass.
13301 pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13302 #validate_body
13303 }
13304 }
13305 }
13306 } else {
13307 quote! {}
13308 };
13309 // Trait-method override so the type-erased ViewSet write path can
13310 // dispatch `ModelSerializer::validate` generically. Emitted whenever
13311 // there's anything to run (declarative constraints inherited from the
13312 // model count); otherwise the trait's default no-op is used.
13313 let trait_validate_override = if has_run_validations {
13314 quote! {
13315 fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13316 #validate_body
13317 }
13318 }
13319 } else {
13320 quote! {}
13321 };
13322
13323 // For every `#[serializer(many = S)]` field, emit a
13324 // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13325 // helper that maps the parents through `S::from_model`.
13326 let many_setters: Vec<_> = fields_info
13327 .iter()
13328 .filter_map(|fi| {
13329 let many_ty = fi.attrs.many.as_ref()?;
13330 let ident = &fi.ident;
13331 let setter = syn::Ident::new(&format!("set_{}", ident_name(ident)), ident.span());
13332 Some(quote! {
13333 /// Populate this `many` field by mapping each parent model
13334 /// through the inner serializer's `from_model`. Call after
13335 /// fetching the M2M / one-to-many children since
13336 /// `from_model` itself can't await an SQL query.
13337 pub fn #setter(
13338 &mut self,
13339 models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13340 ) -> &mut Self {
13341 self.#ident = models.iter()
13342 .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13343 .collect();
13344 self
13345 }
13346 })
13347 })
13348 .collect();
13349 let many_setters_impl = if many_setters.is_empty() {
13350 quote! {}
13351 } else {
13352 quote! {
13353 impl #struct_name {
13354 #( #many_setters )*
13355 }
13356 }
13357 };
13358
13359 // Generate custom Serialize: skip write_only fields
13360 let output_fields: Vec<_> = fields_info
13361 .iter()
13362 .filter(|fi| !fi.attrs.write_only)
13363 .collect();
13364 let output_field_count = output_fields.len();
13365 let serialize_fields = output_fields.iter().map(|fi| {
13366 let ident = &fi.ident;
13367 let name_lit = ident_name(ident);
13368 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13369 });
13370
13371 // writable_fields: normal + write_only.
13372 // Exclude:
13373 // - `read_only` — server-computed.
13374 // - `skip` — caller sets manually post-from_model.
13375 // - `method` — computed from a Self::fn(&model) call; accepting
13376 // it on write is meaningless.
13377 // - `nested` / `many` — populated from related-model data, not
13378 // from a field on the wire body.
13379 // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13380 // `many` in `writable_fields()`, which made the ViewSet write
13381 // path accept those fields from the JSON body and try to bind
13382 // them to the SQL UPDATE — a silent no-op at best, a type
13383 // mismatch at worst.
13384 // (`is_writable` is defined above, near the constraint codegen.)
13385 let writable_lits: Vec<_> = fields_info
13386 .iter()
13387 .filter(is_writable)
13388 .map(|fi| ident_name(&fi.ident))
13389 .collect();
13390
13391 // `writable_source_fields`: the MODEL field names of writable
13392 // serializer fields (source-resolved). The ViewSet write path skips
13393 // every model column NOT in this set, so `read_only` / `method` /
13394 // computed fields a client posts are ignored instead of written.
13395 let writable_source_lits: Vec<String> = fields_info
13396 .iter()
13397 .filter(is_writable)
13398 .map(|fi| {
13399 fi.attrs
13400 .source
13401 .clone()
13402 .unwrap_or_else(|| ident_name(&fi.ident))
13403 })
13404 .collect();
13405
13406 // `readable_source_fields`: model columns the JSON output copies
13407 // straight from the model, so `?ordering=` cannot sort on a column
13408 // the API never shows (#1845).
13409 let readable_source_lits: Vec<String> = fields_info
13410 .iter()
13411 .filter(|fi| {
13412 !fi.attrs.write_only
13413 && !fi.attrs.skip
13414 && fi.attrs.method.is_none()
13415 && !fi.attrs.nested
13416 && fi.attrs.many.is_none()
13417 && fi.attrs.slug.is_none()
13418 })
13419 .map(|fi| {
13420 fi.attrs
13421 .source
13422 .clone()
13423 .unwrap_or_else(|| ident_name(&fi.ident))
13424 })
13425 .collect();
13426
13427 // `from_writable_json`: build a partial instance for input
13428 // validation. Writable fields are parsed from the JSON body (keyed
13429 // by serializer field name); every other field defaults. Per-field
13430 // type errors collect into `FormErrors` keyed by the field name.
13431 // Construction is field-by-field (not `Self::default()`) so it works
13432 // for serializers regardless of a struct-level `Default`.
13433 let from_writable_json_inits: Vec<_> = fields_info
13434 .iter()
13435 .map(|fi| {
13436 let ident = &fi.ident;
13437 let fname = ident_name(ident);
13438 let ty = &fi.ty;
13439 if is_writable(&fi) {
13440 quote! {
13441 #ident: match __obj.and_then(|__o| __o.get(#fname)) {
13442 ::core::option::Option::Some(__v) => {
13443 match #root::__serde_json::from_value::<#ty>(
13444 ::core::clone::Clone::clone(__v),
13445 ) {
13446 ::core::result::Result::Ok(__x) => __x,
13447 ::core::result::Result::Err(__e) => {
13448 __errors.add(#fname.to_owned(), __e.to_string());
13449 ::core::default::Default::default()
13450 }
13451 }
13452 }
13453 ::core::option::Option::None => ::core::default::Default::default(),
13454 }
13455 }
13456 } else {
13457 quote! { #ident: ::core::default::Default::default() }
13458 }
13459 })
13460 .collect();
13461 // `validate_patch`: overlay each sent writable field on the stored row.
13462 let patch_overlays: Vec<_> = fields_info
13463 .iter()
13464 .filter(is_writable)
13465 .map(|fi| {
13466 let ident = &fi.ident;
13467 let fname = ident_name(ident);
13468 let ty = &fi.ty;
13469 quote! {
13470 if let ::core::option::Option::Some(__v) = __obj.and_then(|__o| __o.get(#fname)) {
13471 match #root::__serde_json::from_value::<#ty>(::core::clone::Clone::clone(__v)) {
13472 ::core::result::Result::Ok(__x) => __merged.#ident = __x,
13473 ::core::result::Result::Err(__e) => {
13474 __errors.add(#fname.to_owned(), __e.to_string());
13475 }
13476 }
13477 }
13478 }
13479 })
13480 .collect();
13481 let patch_validate_call = if has_run_validations {
13482 quote! { __merged.__rustango_validate_patch(__obj) }
13483 } else {
13484 quote! { let _ = &__merged; ::core::result::Result::Ok(()) }
13485 };
13486 let patch_validate_helper = if has_run_validations {
13487 quote! {
13488 impl #struct_name {
13489 #[doc(hidden)]
13490 fn __rustango_validate_patch(
13491 &self,
13492 __obj: ::core::option::Option<&#root::__serde_json::Map<::std::string::String, #root::__serde_json::Value>>,
13493 ) -> ::core::result::Result<(), #root::forms::FormErrors> {
13494 #validate_patch_body
13495 }
13496 }
13497 }
13498 } else {
13499 quote! {}
13500 };
13501
13502 // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
13503 // Only includes fields shown in JSON output (skips write_only). For each
13504 // `Option<T>` field, omit from `required` and add `.nullable()`.
13505 let openapi_impl = {
13506 #[cfg(feature = "openapi")]
13507 {
13508 let property_calls = output_fields.iter().map(|fi| {
13509 let ident = &fi.ident;
13510 let name_lit = ident_name(ident);
13511 let ty = &fi.ty;
13512 let nullable_call = if is_option(ty) {
13513 quote! { .nullable() }
13514 } else {
13515 quote! {}
13516 };
13517 quote! {
13518 .property(
13519 #name_lit,
13520 <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13521 #nullable_call,
13522 )
13523 }
13524 });
13525 let required_lits: Vec<_> = output_fields
13526 .iter()
13527 .filter(|fi| !is_option(&fi.ty))
13528 .map(|fi| ident_name(&fi.ident))
13529 .collect();
13530 quote! {
13531 impl #root::openapi::OpenApiSchema for #struct_name {
13532 fn openapi_schema() -> #root::openapi::Schema {
13533 #root::openapi::Schema::object()
13534 #( #property_calls )*
13535 .required([ #( #required_lits ),* ])
13536 }
13537 }
13538 }
13539 }
13540 #[cfg(not(feature = "openapi"))]
13541 {
13542 quote! {}
13543 }
13544 };
13545
13546 Ok(quote! {
13547 impl #root::serializer::ModelSerializer for #struct_name {
13548 type Model = #model_path;
13549
13550 fn from_model(model: &Self::Model) -> Self {
13551 Self {
13552 #( #from_model_fields ),*
13553 }
13554 }
13555
13556 fn writable_fields() -> &'static [&'static str] {
13557 &[ #( #writable_lits ),* ]
13558 }
13559
13560 fn writable_source_fields() -> &'static [&'static str] {
13561 &[ #( #writable_source_lits ),* ]
13562 }
13563
13564 fn readable_source_fields() -> &'static [&'static str] {
13565 &[ #( #readable_source_lits ),* ]
13566 }
13567
13568 fn from_writable_json(
13569 __body: &#root::__serde_json::Value,
13570 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
13571 let mut __errors = #root::forms::FormErrors::default();
13572 let __obj = __body.as_object();
13573 let __out = Self {
13574 #( #from_writable_json_inits ),*
13575 };
13576 if __errors.is_empty() {
13577 ::core::result::Result::Ok(__out)
13578 } else {
13579 ::core::result::Result::Err(__errors)
13580 }
13581 }
13582
13583 fn validate_patch(
13584 __stored: &Self::Model,
13585 __body: &#root::__serde_json::Value,
13586 ) -> ::core::result::Result<(), #root::forms::FormErrors> {
13587 let mut __errors = #root::forms::FormErrors::default();
13588 let __obj = __body.as_object();
13589 #[allow(unused_mut)]
13590 let mut __merged = <Self as #root::serializer::ModelSerializer>::from_model(__stored);
13591 #( #patch_overlays )*
13592 if !__errors.is_empty() {
13593 return ::core::result::Result::Err(__errors);
13594 }
13595 #patch_validate_call
13596 }
13597
13598 #trait_validate_override
13599 }
13600
13601 #patch_validate_helper
13602
13603 impl #root::__serde::Serialize for #struct_name {
13604 fn serialize<S>(&self, serializer: S)
13605 -> ::core::result::Result<S::Ok, S::Error>
13606 where
13607 S: #root::__serde::Serializer,
13608 {
13609 use #root::__serde::ser::SerializeStruct;
13610 let mut __state = serializer.serialize_struct(
13611 #struct_name_lit,
13612 #output_field_count,
13613 )?;
13614 #( #serialize_fields )*
13615 __state.end()
13616 }
13617 }
13618
13619 #openapi_impl
13620
13621 #validate_method
13622
13623 #many_setters_impl
13624 })
13625}
13626
13627/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
13628/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
13629/// when the feature is off.
13630#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
13631fn is_option(ty: &syn::Type) -> bool {
13632 if let syn::Type::Path(p) = ty {
13633 if let Some(last) = p.path.segments.last() {
13634 return last.ident == "Option";
13635 }
13636 }
13637 false
13638}
13639
13640#[cfg(test)]
13641mod main_attr_tests {
13642 use super::*;
13643
13644 fn expand(args: &str) -> String {
13645 let body = quote! { async fn main() { run().await } };
13646 expand_main(args.parse().unwrap(), body)
13647 .expect("expansion failed")
13648 .to_string()
13649 }
13650
13651 /// Asserts on the expansion rather than on `parse_logging`,
13652 /// because the defect was a subscriber reaching the output, not
13653 /// an argument being misread (#1465).
13654 #[test]
13655 fn logging_false_emits_no_subscriber() {
13656 let out = expand("logging = false");
13657 assert!(
13658 !out.contains("logging :: Setup"),
13659 "`logging = false` must not install a subscriber:\n{out}",
13660 );
13661 assert!(
13662 out.contains("block_on"),
13663 "the runtime must still be built:\n{out}",
13664 );
13665 }
13666
13667 #[test]
13668 fn default_still_installs_a_subscriber() {
13669 // The control. Without it, an `expand_main` that dropped the
13670 // prologue unconditionally would pass the test above while
13671 // leaving every default project with no logging at all.
13672 let out = expand("");
13673 assert!(
13674 out.contains("logging :: Setup"),
13675 "the default entrypoint must still install one:\n{out}",
13676 );
13677 }
13678
13679 #[test]
13680 fn logging_false_composes_with_flavor() {
13681 let out = expand("flavor = \"current_thread\", logging = false");
13682 assert!(!out.contains("logging :: Setup"), "{out}");
13683 assert!(out.contains("new_current_thread"), "{out}");
13684 }
13685
13686 #[test]
13687 fn logging_true_is_the_default_shape() {
13688 // `logging = true` is accepted and means the default, so the
13689 // match must not fire on the substring `logging`.
13690 assert!(parse_logging(&"logging = true".parse().unwrap()));
13691 assert!(!parse_logging(&"logging=false".parse().unwrap()));
13692 }
13693}
13694
13695#[cfg(test)]
13696mod field_attr_tests {
13697 use super::*;
13698
13699 fn error_for(field: syn::Field) -> Option<String> {
13700 let attrs = parse_field_attrs(&field).unwrap();
13701 let kind = detect_type(&field.ty).unwrap().kind;
13702 check_bound_compatibility(&field, &attrs, kind)
13703 .err()
13704 .map(|e| e.to_string())
13705 }
13706
13707 /// `citext`, `vector` and `geometry` on the wrong type are errors, not no-ops (#1937).
13708 #[test]
13709 fn type_specific_attrs_reject_other_types() {
13710 let f: syn::FieldsNamed = syn::parse_quote!({
13711 #[rustango(citext)] a: i64,
13712 #[rustango(vector(dims = 3))] b: String,
13713 #[rustango(geometry(srid = 4326))] c: String,
13714 #[rustango(citext)] d: String,
13715 });
13716 let errs: Vec<_> = f.named.into_iter().map(error_for).collect();
13717 assert!(errs[0].as_deref().unwrap().contains("citext"), "{errs:?}");
13718 assert!(errs[1].as_deref().unwrap().contains("vector"), "{errs:?}");
13719 assert!(errs[2].as_deref().unwrap().contains("geometry"), "{errs:?}");
13720 assert_eq!(errs[3], None);
13721 }
13722
13723 /// `manager(ext)` emitted a trait nobody could add methods to; it is refused (#2132).
13724 #[test]
13725 fn manager_ext_is_refused() {
13726 let input: DeriveInput = syn::parse_quote! {
13727 #[rustango(table = "p", manager(ext = "PostManagerExt"))]
13728 struct Post { id: i64 }
13729 };
13730 let err = parse_container_attrs(&input).err().expect("refused");
13731 assert!(err.to_string().contains("#2132"), "{err}");
13732 }
13733
13734 /// A raw ident derives names without `r#`; this used to panic (#1937).
13735 #[test]
13736 fn raw_idents_derive_plain_names() {
13737 let ident: syn::Ident = syn::parse_quote!(r#type);
13738 assert_eq!(column_type_ident(&ident).to_string(), "type_col");
13739 assert_eq!(ident_from_name("type", ident.span()).to_string(), "r#type");
13740 }
13741}