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"` — *required*. The only scalar fields list/retrieve
82/// JSON renders and create/update accept; other body keys are ignored.
83/// * `filter_fields = "a, b"` — fields filterable via `?a=v` query params.
84/// * `search_fields = "a, b"` — fields searched by `?search=...`.
85/// * `ordering = "a, -b"` — default list ordering; prefix `-` for DESC.
86/// * `page_size = N` — default page size (default: 20, max: 1000).
87/// * `read_only` — flag; wires only `list` + `retrieve` (no mutations).
88/// * `allow_anonymous` — flag; write actions without codenames are
89/// intended, so the mount warning is skipped.
90/// * `serializer = SomeSerializer` — render list / retrieve / create
91/// responses through a `#[derive(Serializer)]` type instead of the
92/// default field-level projection (`read_only` / `source` / `method`
93/// / `write_only` overrides apply). Tri-dialect. Requires the
94/// `serializer` feature.
95/// * `permissions(list = "...", retrieve = "...", create = "...",
96/// update = "...", destroy = "...")` — codenames required per action.
97#[proc_macro_derive(ViewSet, attributes(viewset))]
98pub fn derive_viewset(input: TokenStream) -> TokenStream {
99 let input = parse_macro_input!(input as DeriveInput);
100 expand_viewset(&input)
101 .unwrap_or_else(syn::Error::into_compile_error)
102 .into()
103}
104
105/// Derive `rustango::forms::Form` (slice 8.4B). Generates a
106/// `parse(&HashMap<String, String>) -> Result<Self, FormErrors>` impl
107/// that walks every named field and:
108///
109/// * Parses the string value into the field's Rust type (`String`,
110/// `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
111/// nullable case).
112/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
113/// `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
114/// validators in declaration order, returning `FormError::Parse`
115/// on the first failure.
116///
117/// Example:
118///
119/// ```ignore
120/// #[derive(Form)]
121/// pub struct CreateItemForm {
122/// #[form(min_length = 1, max_length = 64)]
123/// pub name: String,
124/// #[form(min = 0, max = 150)]
125/// pub age: i32,
126/// pub active: bool,
127/// pub email: Option<String>,
128/// }
129///
130/// let parsed = CreateItemForm::parse(&form_map)?;
131/// ```
132#[proc_macro_derive(Form, attributes(form))]
133pub fn derive_form(input: TokenStream) -> TokenStream {
134 let input = parse_macro_input!(input as DeriveInput);
135 expand_form(&input)
136 .unwrap_or_else(syn::Error::into_compile_error)
137 .into()
138}
139
140/// Derive `rustango::serializer::ModelSerializer` for a struct.
141/// (intra-doc link disabled — the macro crate doesn't depend on
142/// `rustango` itself, so rustdoc can't resolve the path.)
143///
144/// # Container attribute (required)
145/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
146///
147/// # Field attributes
148/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
149/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
150/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
151/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
152/// - `#[serializer(method = "fn_name")]` — computed field: calls `Self::fn_name(&model)` for the field value; excluded from `writable_fields()`
153/// - `#[serializer(nested)]` / `nested(strict)` — auto-resolves nested serializer from a loaded `ForeignKey`; excluded from `writable_fields()`
154/// - `#[serializer(many = ChildSerializer)]` — collection of nested serializers; populated via macro-emitted `set_<field>(&[Child::Model])`; excluded from `writable_fields()`
155/// - `#[serializer(slug = "name")]` — render a relation (`ForeignKey<T>` or `Option<ForeignKey<T>>`) as one of its fields: clones the loaded parent's `name`; excluded from `writable_fields()` (v0.44)
156/// - `#[serializer(validate = "fn_name")]` — per-field validator surfaced by `Self::validate(&self)`
157/// - `#[serializer(max_length = N)]` / `min_length` / `min` / `max` — declarative bounds checked on
158/// write; auto-inherit from the model's `FieldSchema` (`max_length`/`min`/`max`/`choices`) when
159/// no attr is given, override it when given (`min_length` is serializer-only)
160///
161/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
162#[proc_macro_derive(Serializer, attributes(serializer))]
163pub fn derive_serializer(input: TokenStream) -> TokenStream {
164 let input = parse_macro_input!(input as DeriveInput);
165 expand_serializer(&input)
166 .unwrap_or_else(syn::Error::into_compile_error)
167 .into()
168}
169
170/// Bake every `*.json` migration file in a directory into the binary
171/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
172/// of `(name, json_content)` pairs, lex-sorted by file stem.
173///
174/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
175/// behaviour as `migrate(pool, dir)` but with no filesystem access.
176/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
177/// (i.e. the crate that invokes the macro). Default is
178/// `"./migrations"` if no argument is supplied.
179///
180/// ```ignore
181/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
182/// // or:
183/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
184///
185/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
186/// ```
187///
188/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
189/// file's `name` field must equal its file stem, every `prev`
190/// reference must point to another migration in the same directory,
191/// and the JSON must parse. A broken chain — orphan `prev`, missing
192/// predecessor, malformed file — fails at macro-expansion time with
193/// a clear `compile_error!`. *No other Rust web framework
194/// validates migration chains at compile time*: Cot's migrations are
195/// imperative Rust code (no static chain), Loco's are SeaORM
196/// up/down (same), Rwf's are raw SQL (no chain at all).
197///
198/// Each migration is included via `include_str!` so cargo's rebuild
199/// detection picks up file *content* changes. Cargo does not watch
200/// directory listings, so a new migration file is missed until the
201/// crate rebuilds. Add a `build.rs` that tracks the directory:
202///
203/// ```ignore
204/// fn main() { println!("cargo:rerun-if-changed=migrations"); }
205/// ```
206///
207/// A missing directory embeds nothing and emits a deprecation warning.
208#[proc_macro]
209pub fn embed_migrations(input: TokenStream) -> TokenStream {
210 expand_embed_migrations(input.into())
211 .unwrap_or_else(syn::Error::into_compile_error)
212 .into()
213}
214
215/// `Q!()` — `Model.field__lookup = value` filter syntax, resolved at
216/// compile time against typed columns. Issue #269 / T1.7.
217///
218/// Each invocation lowers to the equivalent typed-column method call:
219///
220/// ```ignore
221/// // These expand identically:
222/// Q!(User.email__icontains = "alice")
223/// User::email.ilike("%alice%")
224/// ```
225///
226/// Field-name typos fail the build (the macro emits `User::no_such_field`
227/// which doesn't exist) — the headline ergonomic win of this slice over
228/// filters keyed by a plain string.
229///
230/// # Supported lookup suffixes
231///
232/// * bare `=` / `__exact` → `.eq(value)`
233/// * `__iexact` → `.ilike(value)` (case-insensitive equality, no wildcards)
234/// * `__ne` → `.ne(value)`
235/// * `__gt` / `__gte` / `__lt` / `__lte` → corresponding comparison
236/// * `__contains` / `__icontains` → `.like("%v%")` / `.ilike("%v%")`
237/// * `__startswith` / `__istartswith` → `.like("v%")` / `.ilike("v%")`
238/// * `__endswith` / `__iendswith` → `.like("%v")` / `.ilike("%v")`
239/// * `__in` → `.is_in(iterable)`
240/// * `__not_in` → `.not_in(iterable)`
241/// * `__isnull = true` → `.is_null()`; `__isnull = false` → `.is_not_null()`
242/// * `__between` accepts a tuple literal `(lo, hi)` → `.between(lo, hi)`
243/// * `__regex` / `__iregex` → `.regex(pattern)` / `.iregex(pattern)`
244///
245/// Unknown suffixes fail the build with a `compile_error!` pointing at
246/// the lookup token.
247///
248/// # Combine
249///
250/// Each `Q!()` returns a `TypedFilter<Model>` — chain via the existing
251/// `.and()` / `.or()` / `.not()` methods:
252///
253/// ```ignore
254/// User::objects()
255/// .where_(
256/// Q!(User.active = true)
257/// .and(Q!(User.email__icontains = "alice"))
258/// )
259/// .fetch(&pool).await?;
260/// ```
261///
262/// All emitted code routes through existing per-dialect writers — no new
263/// SQL emission machinery. Tri-dialect support is inherent.
264#[allow(non_snake_case)]
265#[proc_macro]
266pub fn Q(input: TokenStream) -> TokenStream {
267 expand_q(input.into())
268 .unwrap_or_else(syn::Error::into_compile_error)
269 .into()
270}
271
272/// `#[rustango::main]` — the runserver entrypoint. Wraps
273/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
274/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
275/// functions are zero-boilerplate. `RUST_LOG` and
276/// `RUSTANGO__LOGGING__FORMAT` (e.g. `json`) come from the real
277/// environment, else from `./.env` (only those keys are read):
278///
279/// ```ignore
280/// #[rustango::main]
281/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
282/// rustango::server::Builder::from_env().await?
283/// .migrate("migrations").await?
284/// .api(my_app::urls::api())
285/// .seed_with(my_app::seed::run).await?
286/// .serve("0.0.0.0:8080").await
287/// }
288/// ```
289///
290/// Optional `flavor = "current_thread"` passes through to
291/// `#[tokio::main]`; default is the multi-threaded runtime.
292///
293/// `logging = false` suppresses the default subscriber, for apps
294/// that install their own. Needed whenever the body reaches
295/// `Cli::with_logging()` or `logging::Setup`: the subscriber this
296/// macro installs gets there first, and `tracing` keeps the first
297/// one, so the `[logging]` section would otherwise be read and
298/// discarded (#1465).
299///
300/// ```ignore
301/// #[rustango::main(logging = false)]
302/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
303/// rustango::manage::Cli::new()
304/// .with_settings_from_env()
305/// .with_logging() // now the first installer
306/// .run().await
307/// }
308/// ```
309///
310/// Pulls `tracing-subscriber` into the rustango crate behind the
311/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
312/// out get plain `#[tokio::main]` ergonomics without the dependency.
313#[proc_macro_attribute]
314pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
315 expand_main(args.into(), item.into())
316 .unwrap_or_else(syn::Error::into_compile_error)
317 .into()
318}
319
320fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
321 let mut input: syn::ItemFn = syn::parse2(item)?;
322 if input.sig.asyncness.is_none() {
323 return Err(syn::Error::new(
324 input.sig.ident.span(),
325 "`#[rustango::main]` must wrap an `async fn`",
326 ));
327 }
328
329 // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
330 // to `#[tokio::main]`. Tokio's proc-macro internally emits
331 // `::tokio::*` paths that resolve against the user crate's deps,
332 // so calling it through the rustango re-export still requires the
333 // user to add tokio to their own Cargo.toml. Building the
334 // runtime ourselves keeps the dep transitive through the
335 // `runtime` feature on rustango.
336 //
337 // Parse optional `flavor = "current_thread"` / `flavor =
338 // "multi_thread"` from the attribute args. Unknown args still
339 // compile, with a warning that they are ignored.
340 let root = rustango_root();
341 let arg_warnings = main_arg_warnings(&args);
342 let flavor = parse_flavor(&args);
343 let builder_call = match flavor {
344 Flavor::CurrentThread => quote! {
345 #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
346 },
347 Flavor::MultiThread => quote! {
348 #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
349 },
350 };
351
352 // Detach the user body and rewrite `main` as a sync fn that
353 // builds the runtime and blocks on the async body.
354 let user_body = input.block.clone();
355 input.sig.asyncness = None;
356
357 // `logging = false` drops the block entirely, so a later
358 // `Cli::with_logging()` is the first installer and its
359 // `[logging]` settings actually take effect (#1465).
360 let logging_prologue = if parse_logging(&args) {
361 quote! {
362 // `Setup`'s defaults, not a copy of them: filter, format and
363 // the `Color::Auto` rule live in one place (#1493).
364 // `logging` and this macro share the `runtime` gate.
365 // The real `RUST_LOG` still wins over `./.env`'s (#2204).
366 let _ = #root::logging::Setup::new()
367 .with_default_env_filter(#root::__private_runtime::main_default_filter())
368 .with_format(#root::__private_runtime::main_format())
369 .install();
370 }
371 } else {
372 quote! {}
373 };
374
375 input.block = syn::parse2(quote! {{
376 #arg_warnings
377 { #logging_prologue }
378 let __rt = #builder_call
379 .enable_all()
380 .build()
381 .expect("failed to build tokio runtime");
382 __rt.block_on(async move #user_body)
383 }})?;
384
385 Ok(quote! {
386 #input
387 })
388}
389
390enum Flavor {
391 MultiThread,
392 CurrentThread,
393}
394
395/// `#[tokio::main]` argument names, accepted without a warning.
396const TOKIO_MAIN_ARGS: &[&str] = &[
397 "flavor",
398 "worker_threads",
399 "start_paused",
400 "crate",
401 "unhandled_panic",
402];
403
404/// A warning per `#[rustango::main]` argument the parsers below ignore.
405fn main_arg_warnings(args: &TokenStream2) -> TokenStream2 {
406 use syn::parse::Parser as _;
407 let parsed = syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated
408 .parse2(args.clone());
409 let metas = match parsed {
410 Ok(m) => m,
411 Err(e) => {
412 return deprecation_warning(
413 e.span(),
414 &format!("#[rustango::main]: cannot parse the arguments ({e}); they are ignored"),
415 )
416 }
417 };
418 let lit_bool = |m: &syn::MetaNameValue| match &m.value {
419 syn::Expr::Lit(syn::ExprLit {
420 lit: syn::Lit::Bool(_),
421 ..
422 }) => true,
423 _ => false,
424 };
425 metas
426 .iter()
427 .filter_map(|meta| {
428 // `#[tokio::main]`'s own names stay silent: a `deny(warnings)`
429 // crate passing one must keep compiling in a patch release.
430 let tokio_arg = TOKIO_MAIN_ARGS.iter().any(|n| meta.path().is_ident(n));
431 let ok = match meta {
432 syn::Meta::NameValue(m) if m.path.is_ident("logging") => lit_bool(m),
433 _ => tokio_arg,
434 };
435 (!ok).then(|| {
436 deprecation_warning(
437 meta.span(),
438 "#[rustango::main]: unknown argument, ignored (supported: \
439 `flavor = \"current_thread\" | \"multi_thread\"`, `logging = false`)",
440 )
441 })
442 })
443 .collect()
444}
445
446/// A compile warning at `span`: a proc macro's only stable way to warn
447/// is a `#[deprecated]` item that its own output then uses.
448fn deprecation_warning(span: proc_macro2::Span, note: &str) -> TokenStream2 {
449 quote::quote_spanned! {span=>
450 const _: () = {
451 #[deprecated(note = #note)]
452 const RUSTANGO_WARNING: () = ();
453 RUSTANGO_WARNING
454 };
455 }
456}
457
458fn parse_flavor(args: &TokenStream2) -> Flavor {
459 // Cheap parser: look for the literal token sequence
460 // `flavor = "current_thread"`. Everything else (including
461 // bare `multi_thread` or no args) defaults to multi-thread.
462 let s = args.to_string();
463 if s.contains("current_thread") {
464 Flavor::CurrentThread
465 } else {
466 Flavor::MultiThread
467 }
468}
469
470/// `false` when the attribute carries `logging = false` — the caller
471/// installs their own subscriber, usually via `Cli::with_logging`
472/// (#1465). Same cheap token match as `parse_flavor`.
473fn parse_logging(args: &TokenStream2) -> bool {
474 !args.to_string().replace(' ', "").contains("logging=false")
475}
476
477/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
478struct QInput {
479 base_path: syn::Path,
480 field: syn::Ident,
481 value: syn::Expr,
482}
483
484impl syn::parse::Parse for QInput {
485 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
486 let base_path: syn::Path = input.parse()?;
487 input.parse::<syn::Token![.]>()?;
488 let field: syn::Ident = input.parse()?;
489 input.parse::<syn::Token![=]>()?;
490 let value: syn::Expr = input.parse()?;
491 Ok(QInput {
492 base_path,
493 field,
494 value,
495 })
496 }
497}
498
499fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
500 let q: QInput = syn::parse2(input)?;
501 let root = rustango_root();
502 let field_str = ident_name(&q.field);
503 let field_span = q.field.span();
504 let (base, suffix) = match field_str.find("__") {
505 Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
506 None => (field_str.as_str(), ""),
507 };
508 if base.is_empty() {
509 return Err(syn::Error::new(
510 field_span,
511 "Q!(): field name is empty before `__` suffix",
512 ));
513 }
514 let base_ident = ident_from_name(base, field_span);
515 let value = &q.value;
516 let path = &q.base_path;
517
518 // Most suffixes map directly to a Column method with the value
519 // forwarded unchanged. Some need value-shape massaging (wildcards
520 // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
521 // ISNULL). Unknown suffixes fail the build.
522 let expanded = match suffix {
523 "" | "exact" => quote! {
524 #root::core::Column::eq(#path::#base_ident, #value)
525 },
526 "ne" => quote! {
527 #root::core::Column::ne(#path::#base_ident, #value)
528 },
529 "gt" => quote! {
530 #root::core::Column::gt(#path::#base_ident, #value)
531 },
532 "gte" => quote! {
533 #root::core::Column::gte(#path::#base_ident, #value)
534 },
535 "lt" => quote! {
536 #root::core::Column::lt(#path::#base_ident, #value)
537 },
538 "lte" => quote! {
539 #root::core::Column::lte(#path::#base_ident, #value)
540 },
541 // The escaped-LIKE lookups delegate to the typed Column
542 // methods, which escape the value and pair it with the
543 // *Escaped ops so `%` / `_` match literally on every dialect
544 // (#1257) — the macro no longer owns any pattern wrapping.
545 "iexact" => quote! {
546 #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
547 },
548 "contains" => quote! {
549 #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
550 },
551 "icontains" => quote! {
552 #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
553 },
554 "startswith" => quote! {
555 #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
556 },
557 "istartswith" => quote! {
558 #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
559 },
560 "endswith" => quote! {
561 #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
562 },
563 "iendswith" => quote! {
564 #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
565 },
566 "in" => quote! {
567 #root::core::Column::is_in(#path::#base_ident, #value)
568 },
569 "not_in" => quote! {
570 #root::core::Column::not_in(#path::#base_ident, #value)
571 },
572 "isnull" => {
573 // Must be a bool literal at macro time so we can route to
574 // is_null vs is_not_null without a runtime branch.
575 let b = match value {
576 syn::Expr::Lit(syn::ExprLit {
577 lit: syn::Lit::Bool(b),
578 ..
579 }) => b.value(),
580 _ => {
581 return Err(syn::Error::new_spanned(
582 value,
583 "Q!(): `__isnull` requires a `true` or `false` literal",
584 ));
585 }
586 };
587 if b {
588 quote! { #root::core::Column::is_null(#path::#base_ident) }
589 } else {
590 quote! { #root::core::Column::is_not_null(#path::#base_ident) }
591 }
592 }
593 "between" => {
594 // Accept a tuple literal `(lo, hi)`.
595 let tuple = match value {
596 syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
597 _ => {
598 return Err(syn::Error::new_spanned(
599 value,
600 "Q!(): `__between` requires a tuple literal `(lo, hi)`",
601 ));
602 }
603 };
604 let lo = &tuple.elems[0];
605 let hi = &tuple.elems[1];
606 quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
607 }
608 "regex" => quote! {
609 #root::core::Column::regex(#path::#base_ident, #value)
610 },
611 "iregex" => quote! {
612 #root::core::Column::iregex(#path::#base_ident, #value)
613 },
614 _ => {
615 return Err(syn::Error::new(
616 field_span,
617 format!(
618 "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",
619 suffix
620 ),
621 ));
622 }
623 };
624 Ok(expanded)
625}
626
627fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
628 // Default to "./migrations" if invoked without args.
629 let path_str = if input.is_empty() {
630 "./migrations".to_string()
631 } else {
632 let lit: LitStr = syn::parse2(input)?;
633 lit.value()
634 };
635
636 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
637 syn::Error::new(
638 proc_macro2::Span::call_site(),
639 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
640 )
641 })?;
642 let abs = std::path::Path::new(&manifest).join(&path_str);
643
644 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
645 if abs.is_dir() {
646 let read = std::fs::read_dir(&abs).map_err(|e| {
647 syn::Error::new(
648 proc_macro2::Span::call_site(),
649 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
650 )
651 })?;
652 for entry in read.flatten() {
653 let path = entry.path();
654 if !path.is_file() {
655 continue;
656 }
657 if path.extension().and_then(|s| s.to_str()) != Some("json") {
658 continue;
659 }
660 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
661 continue;
662 };
663 entries.push((stem.to_owned(), path));
664 }
665 }
666 entries.sort_by(|a, b| a.0.cmp(&b.0));
667
668 // Compile-time chain validation: read each migration's JSON,
669 // pull `name` and `prev` (file-stem-keyed for the chain check),
670 // and verify every `prev` points to another migration in the
671 // slice. Mismatches between the file stem and the embedded
672 // `name` field — and broken `prev` chains — fail at MACRO
673 // EXPANSION time so a misshapen migration set never compiles.
674 //
675 // This is the v0.4 Slice 5 distinguisher: rustango's JSON
676 // migrations + a Rust proc-macro that reads them is a combo no
677 // other Rust web framework matches (Cot's are imperative Rust
678 // code, Loco's are SeaORM up/down, Rwf's are raw SQL — none have
679 // a static chain to validate).
680 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
681 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
682 for (stem, path) in &entries {
683 let raw = std::fs::read_to_string(path).map_err(|e| {
684 syn::Error::new(
685 proc_macro2::Span::call_site(),
686 format!(
687 "embed_migrations!: cannot read {} for chain validation: {e}",
688 path.display()
689 ),
690 )
691 })?;
692 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
693 syn::Error::new(
694 proc_macro2::Span::call_site(),
695 format!(
696 "embed_migrations!: {} is not valid JSON: {e}",
697 path.display()
698 ),
699 )
700 })?;
701 let name = json
702 .get("name")
703 .and_then(|v| v.as_str())
704 .ok_or_else(|| {
705 syn::Error::new(
706 proc_macro2::Span::call_site(),
707 format!(
708 "embed_migrations!: {} is missing the `name` field",
709 path.display()
710 ),
711 )
712 })?
713 .to_owned();
714 if name != *stem {
715 return Err(syn::Error::new(
716 proc_macro2::Span::call_site(),
717 format!(
718 "embed_migrations!: file stem `{stem}` does not match the migration's \
719 `name` field `{name}` — rename the file or fix the JSON",
720 ),
721 ));
722 }
723 let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
724 chain_names.push(name.clone());
725 prev_refs.push((name, prev));
726 }
727
728 let name_set: std::collections::HashSet<&str> =
729 chain_names.iter().map(String::as_str).collect();
730 for (name, prev) in &prev_refs {
731 if let Some(p) = prev {
732 if !name_set.contains(p.as_str()) {
733 return Err(syn::Error::new(
734 proc_macro2::Span::call_site(),
735 format!(
736 "embed_migrations!: broken migration chain — `{name}` declares \
737 prev=`{p}` but no migration with that name exists in {}",
738 abs.display()
739 ),
740 ));
741 }
742 }
743 }
744
745 let pairs: Vec<TokenStream2> = entries
746 .iter()
747 .map(|(name, path)| {
748 let path_lit = path.display().to_string();
749 quote! { (#name, ::core::include_str!(#path_lit)) }
750 })
751 .collect();
752
753 // A wrong path used to embed nothing silently; a deprecation use is
754 // the only stable way for a proc macro to warn without breaking builds.
755 let missing_warning = if abs.is_dir() {
756 quote! {}
757 } else {
758 let note = format!(
759 "embed_migrations!: directory {} does not exist; no migrations are embedded",
760 abs.display()
761 );
762 quote! {
763 #[deprecated(note = #note)]
764 const __RUSTANGO_MIGRATIONS_DIR_MISSING: () = ();
765 #[allow(clippy::let_unit_value)]
766 let _ = __RUSTANGO_MIGRATIONS_DIR_MISSING;
767 }
768 };
769
770 Ok(quote! {
771 {
772 #missing_warning
773 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
774 __RUSTANGO_EMBEDDED
775 }
776 })
777}
778
779fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
780 let root = rustango_root();
781 let struct_name = &input.ident;
782
783 let Data::Struct(data) = &input.data else {
784 return Err(syn::Error::new_spanned(
785 struct_name,
786 "Model can only be derived on structs",
787 ));
788 };
789 let Fields::Named(named) = &data.fields else {
790 return Err(syn::Error::new_spanned(
791 struct_name,
792 "Model requires a struct with named fields",
793 ));
794 };
795
796 let mut container = parse_container_attrs(input)?;
797 let table = container
798 .table
799 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
800 let model_name = struct_name.to_string();
801
802 let collected = collect_fields(named, &table)?;
803 // `reverse_has` / `generic_has` join on the PK column unless they name another.
804 if let Some((_, pk_column)) = &collected.primary_key {
805 for rel in &mut container.reverse_has_relations {
806 rel.self_pk_column.get_or_insert_with(|| pk_column.clone());
807 }
808 for rel in &mut container.generic_has_relations {
809 rel.self_pk_column.get_or_insert_with(|| pk_column.clone());
810 }
811 }
812
813 // Validate that #[rustango(display = "…")] names a real field.
814 if let Some((ref display, span)) = container.display {
815 if !collected.field_names.iter().any(|n| n == display) {
816 return Err(syn::Error::new(
817 span,
818 format!("`display = \"{display}\"` does not match any field on this struct"),
819 ));
820 }
821 }
822 let display = container.display.map(|(name, _)| name);
823 let app_label = container.app.clone();
824
825 // Validate admin field-name lists against declared field names.
826 // Note: `list_display` is intentionally NOT validated here. As of
827 // v0.32 it may also reference inventory-registered computed
828 // fields (via `register_admin_computed!`) whose existence the
829 // macro can't see at compile time — they're submitted from any
830 // crate that depends on rustango. The runtime list-view resolves
831 // unknown names against the inventory + silently drops the
832 // truly-bogus ones, which is the cheaper trade-off versus
833 // forcing a per-Model attr to opt out.
834 if let Some(admin) = &container.admin {
835 for (label, list) in [
836 ("search_fields", &admin.search_fields),
837 ("readonly_fields", &admin.readonly_fields),
838 ("list_filter", &admin.list_filter),
839 ] {
840 if let Some((names, span)) = list {
841 for name in names {
842 if !collected.field_names.iter().any(|n| n == name) {
843 return Err(syn::Error::new(
844 *span,
845 format!(
846 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
847 ),
848 ));
849 }
850 }
851 }
852 }
853 if let Some((pairs, span)) = &admin.ordering {
854 for (name, _) in pairs {
855 if !collected.field_names.iter().any(|n| n == name) {
856 return Err(syn::Error::new(
857 *span,
858 format!(
859 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
860 ),
861 ));
862 }
863 }
864 }
865 if let Some((groups, span)) = &admin.fieldsets {
866 for (_, fields) in groups {
867 for name in fields {
868 if !collected.field_names.iter().any(|n| n == name) {
869 return Err(syn::Error::new(
870 *span,
871 format!(
872 "`fieldsets`: \"{name}\" is not a declared field on this struct"
873 ),
874 ));
875 }
876 }
877 }
878 }
879 }
880 if let Some(audit) = &container.audit {
881 if let Some((names, span)) = &audit.track {
882 for name in names {
883 if !collected.field_names.iter().any(|n| n == name) {
884 return Err(syn::Error::new(
885 *span,
886 format!(
887 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
888 ),
889 ));
890 }
891 }
892 }
893 }
894
895 // Issue #291 / T2.5 — validate each `default_order` column name
896 // against the model's collected fields. Typos fail at macro-expand
897 // time, not at the database.
898 for (col, _desc, span) in &container.default_order {
899 if !collected.field_names.iter().any(|n| n == col) {
900 return Err(syn::Error::new(
901 *span,
902 format!(
903 "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
904 ),
905 ));
906 }
907 }
908
909 let column_warnings =
910 unknown_column_warnings(&container.indexes, &container.get_latest_by, &collected);
911
912 // Build the audit_track list for ModelSchema: None when no audit attr,
913 // Some(empty) when audit present without track, Some(names) when explicit.
914 let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
915 audit
916 .track
917 .as_ref()
918 .map(|(names, _)| names.clone())
919 .unwrap_or_default()
920 });
921
922 // The upsert conflict target: a container-level composite unique,
923 // never a field `index(unique)` or a partial index PG can't target.
924 let upsert_unique: Option<Vec<String>> = container
925 .indexes
926 .iter()
927 .find(|i| i.unique && i.where_clause.is_none() && !i.columns.is_empty())
928 .map(|i| i.columns.clone());
929
930 // Merge field-level indexes into the container's index list.
931 let mut all_indexes: Vec<IndexAttr> = container.indexes;
932 for field in &named.named {
933 let ident = field.ident.as_ref().expect("named");
934 // The same default column as `process_field` (#1937).
935 let col = ident_name(ident);
936 // Re-parse field attrs to check for index flag
937 if let Ok(fa) = parse_field_attrs(field) {
938 if fa.index {
939 let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
940 let auto_name = capped_index_name(if fa.index_unique {
941 format!("{table}_{col_name}_uq_idx")
942 } else {
943 format!("{table}_{col_name}_idx")
944 });
945 all_indexes.push(IndexAttr {
946 span: field.span(),
947 name: fa.index_name.or(Some(auto_name)),
948 columns: vec![col_name],
949 unique: fa.index_unique,
950 method: fa.index_method,
951 where_clause: None,
952 include: Vec::new(),
953 });
954 }
955 }
956 }
957 // An unbounded String is LONGTEXT on MySQL, which no key can hold (1170, #2575).
958 for field in &named.named {
959 let (Ok(fa), Ok(det)) = (parse_field_attrs(field), detect_type(&field.ty)) else {
960 continue;
961 };
962 if det.kind != DetectedKind::String || fa.max_length.is_some() {
963 continue;
964 }
965 let col = fa
966 .column
967 .clone()
968 .unwrap_or_else(|| ident_name(field.ident.as_ref().expect("named")));
969 // MySQL indexes every foreign-key column too.
970 let relation = det.fk_inner.is_some() || fa.fk.is_some() || fa.o2o.is_some();
971 if fa.primary_key
972 || fa.unique
973 || relation
974 || all_indexes.iter().any(|i| i.columns.contains(&col))
975 {
976 return Err(syn::Error::new_spanned(
977 field,
978 "a primary-key, unique, indexed or foreign-key `String` needs \
979 `max_length`: MySQL cannot index an unbounded (LONGTEXT) column",
980 ));
981 }
982 }
983
984 let model_impl = model_impl_tokens(
985 struct_name,
986 &model_name,
987 &table,
988 display.as_deref(),
989 app_label.as_deref(),
990 container.admin.as_ref(),
991 &container.default_order,
992 &collected.field_schemas,
993 collected.soft_delete_column.as_deref(),
994 container.permissions,
995 audit_track_names.as_deref(),
996 &container.m2m,
997 &all_indexes,
998 &container.checks,
999 &container.excludes,
1000 &container.composite_fks,
1001 &container.generic_fks,
1002 container.scope.as_deref(),
1003 container.is_view,
1004 container.verbose_name.as_deref(),
1005 container.verbose_name_plural.as_deref(),
1006 container.managed,
1007 container.base_manager_name.as_deref(),
1008 container.order_with_respect_to.as_deref(),
1009 container.proxy,
1010 &container.required_db_features,
1011 container.required_db_vendor.as_deref(),
1012 container.default_related_name.as_deref(),
1013 container.db_table_comment.as_deref(),
1014 container
1015 .get_latest_by
1016 .as_ref()
1017 .map(|(c, d, _)| (c.as_str(), *d)),
1018 &container.extra_permissions,
1019 &container.default_permissions,
1020 &container.global_scopes,
1021 &container.reverse_has_relations,
1022 &container.generic_has_relations,
1023 );
1024 let module_ident = column_module_ident(struct_name);
1025 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
1026 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
1027 let track_set: Option<std::collections::HashSet<&str>> = audit
1028 .track
1029 .as_ref()
1030 .map(|(names, _)| names.iter().map(String::as_str).collect());
1031 collected
1032 .column_entries
1033 .iter()
1034 .filter(|c| {
1035 track_set
1036 .as_ref()
1037 .map_or(true, |s| s.contains(c.name.as_str()))
1038 })
1039 .collect()
1040 });
1041 let inherent_impl = inherent_impl_tokens(
1042 struct_name,
1043 &collected,
1044 collected.primary_key.as_ref(),
1045 &column_consts,
1046 audited_fields.as_deref(),
1047 upsert_unique.as_deref(),
1048 &container.manager_fns,
1049 );
1050 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
1051 let from_row_impl = from_row_impl_tokens(
1052 struct_name,
1053 &collected.from_row_inits,
1054 &collected.from_row_inits_my,
1055 );
1056 let reverse_helpers = reverse_helper_tokens(
1057 struct_name,
1058 &collected.fk_relations,
1059 container.default_related_name.as_deref(),
1060 )?;
1061 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
1062 let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
1063 // Issue #817 — `#[rustango(through(...))]` accessors.
1064 let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
1065 // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
1066 let reverse_has_accessors = reverse_has_accessor_tokens(
1067 struct_name,
1068 &container.reverse_has_relations,
1069 &collected.column_entries,
1070 );
1071 let generic_fk_accessors = generic_fk_accessor_tokens(
1072 struct_name,
1073 &container.generic_fks,
1074 &collected.column_entries,
1075 );
1076
1077 Ok(quote! {
1078 #column_warnings
1079 #model_impl
1080 #inherent_impl
1081 #from_row_impl
1082 #column_module
1083 #reverse_helpers
1084 #m2m_accessors
1085 #generic_m2m_accessors
1086 #through_accessors
1087 #reverse_has_accessors
1088 #generic_fk_accessors
1089
1090 #root::core::inventory::submit! {
1091 // `module_path!()` evaluates at the registration site,
1092 // so a Model declared in `crate::blog::models` records
1093 // `"<crate>::blog::models"` and `resolved_app_label()`
1094 // can infer "blog" without an explicit attribute.
1095 #root::core::ModelEntry::new(
1096 <#struct_name as #root::core::Model>::SCHEMA,
1097 ::core::module_path!(),
1098 )
1099 .with_audited(
1100 <#struct_name as #root::core::Model>::__rustango_audited_update,
1101 <#struct_name as #root::core::Model>::__rustango_audited_delete,
1102 )
1103 .with_audited_create(<#struct_name as #root::core::Model>::__rustango_audited_create)
1104 .with_audited_update_record(
1105 <#struct_name as #root::core::Model>::__rustango_audited_update_record,
1106 )
1107 .with_audited_cascade(<#struct_name as #root::core::Model>::__rustango_audited_cascade)
1108 }
1109 })
1110}
1111
1112/// A deprecation-style warning per index or `get_latest_by` name that is
1113/// no column of the model: an error would break code that compiles today.
1114fn unknown_column_warnings(
1115 indexes: &[IndexAttr],
1116 get_latest_by: &Option<(String, bool, proc_macro2::Span)>,
1117 collected: &CollectedFields,
1118) -> TokenStream2 {
1119 let is_column = |c: &str| collected.column_entries.iter().any(|e| e.column == c);
1120 // Only plain names: an expression index such as `lower(email)` is legal.
1121 let plain = |c: &str| c.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_');
1122 let mut found: Vec<(proc_macro2::Span, String)> = Vec::new();
1123 for idx in indexes {
1124 for col in idx.columns.iter().filter(|c| plain(c) && !is_column(c)) {
1125 found.push((
1126 idx.span,
1127 format!("index column `{col}` is not a column of this model; its DDL will fail"),
1128 ));
1129 }
1130 }
1131 if let Some((col, _, span)) = get_latest_by {
1132 if !is_column(col) && !collected.field_names.iter().any(|n| n == col) {
1133 found.push((
1134 *span,
1135 format!("`get_latest_by`: `{col}` is not a field of this model"),
1136 ));
1137 }
1138 }
1139 found
1140 .into_iter()
1141 .map(|(span, note)| deprecation_warning(span, ¬e))
1142 .collect()
1143}
1144
1145/// Guard before decoding a `select_related` target (#2293): a NULL nullable
1146/// FK leaves the relation unloaded; a set FK whose row is missing is an error.
1147fn join_guard_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1148 let root = rustango_root();
1149 let parent_ty = &rel.parent_type;
1150 let field_ident = &rel.field_ident;
1151 let null_fk = if rel.nullable {
1152 quote! {
1153 if #receiver.#field_ident.is_none() {
1154 return ::core::result::Result::Ok(false);
1155 }
1156 }
1157 } else {
1158 quote! {}
1159 };
1160 quote! {
1161 #null_fk
1162 #root::sql::__rustango_require_join(
1163 row, <#parent_ty as #root::core::Model>::SCHEMA, alias,
1164 )?;
1165 }
1166}
1167
1168/// A target an earlier chain already loaded (`a__b` before `a__c`) is
1169/// reused, and the deeper chain stitched into it; re-decoding it would
1170/// drop what that chain loaded.
1171fn loaded_reuse_tokens(
1172 rel: &FkRelation,
1173 receiver: &TokenStream2,
1174 load: &TokenStream2,
1175) -> TokenStream2 {
1176 let root = rustango_root();
1177 let field_ident = &rel.field_ident;
1178 let pattern = if rel.nullable {
1179 quote!(::core::option::Option::Some(#root::sql::ForeignKey::Loaded { value: __loaded, .. }))
1180 } else {
1181 quote!(#root::sql::ForeignKey::Loaded { value: __loaded, .. })
1182 };
1183 quote! {
1184 if let #pattern = &mut #receiver.#field_ident {
1185 if let ::core::option::Option::Some(__r) = __rest {
1186 let _ = #load(&mut **__loaded, row, __r, &__next_alias)?;
1187 }
1188 return ::core::result::Result::Ok(true);
1189 }
1190 }
1191}
1192
1193/// Store the decoded `_parent` on the FK field, keeping the key the row
1194/// holds: with `on = <unique column>` it is not the parent's PK.
1195fn loaded_assign_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1196 let root = rustango_root();
1197 let field_ident = &rel.field_ident;
1198 if rel.nullable {
1199 quote! {
1200 if let ::core::option::Option::Some(__fk) = &mut #receiver.#field_ident {
1201 let _pk = ::core::clone::Clone::clone(__fk.pk_ref());
1202 *__fk = #root::sql::ForeignKey::loaded(_pk, _parent);
1203 }
1204 }
1205 } else {
1206 quote! {
1207 let _pk = ::core::clone::Clone::clone(#receiver.#field_ident.pk_ref());
1208 #receiver.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1209 }
1210 }
1211}
1212
1213/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
1214/// matches `field_name` against the model's FK fields and, for a
1215/// match, decodes the FK target via the parent's macro-generated
1216/// `__rustango_from_aliased_row`, and stores `ForeignKey::Loaded` on
1217/// `self` under the key the row already holds.
1218///
1219/// Always emitted (with empty arms for FK-less models, which
1220/// return `Ok(false)` for any field name) so the `T: LoadRelated`
1221/// trait bound on `fetch_on` is universally satisfied — users
1222/// never have to think about implementing it.
1223fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1224 let root = rustango_root();
1225 let arms = fk_relations.iter().map(|rel| {
1226 let parent_ty = &rel.parent_type;
1227 let field_ident = &rel.field_ident;
1228 let fk_name = ident_name(field_ident);
1229 let fk_col = fk_name.as_str();
1230 let guard = join_guard_tokens(rel, "e!(self));
1231 let reuse = loaded_reuse_tokens(
1232 rel,
1233 "e!(self),
1234 "e!(#root::sql::LoadRelated::__rustango_load_related),
1235 );
1236 let assign = loaded_assign_tokens(rel, "e!(self));
1237 quote! {
1238 #fk_col => {
1239 #guard
1240 #reuse
1241 let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
1242 // Audit #451 — multi-hop `select_related("a__b__c")`:
1243 // stitch the deeper relation onto this parent first,
1244 // decoding it at the accumulated `__next_alias`. The
1245 // parent type also impls `LoadRelated`, so this recurses
1246 // the FK chain to arbitrary depth.
1247 if let ::core::option::Option::Some(__r) = __rest {
1248 let _ = #root::sql::LoadRelated::__rustango_load_related(
1249 &mut _parent, row, __r, &__next_alias,
1250 )?;
1251 }
1252 #assign
1253 ::core::result::Result::Ok(true)
1254 }
1255 }
1256 });
1257 quote! {
1258 #[cfg(feature = "postgres")]
1259 impl #root::sql::LoadRelated for #struct_name {
1260 #[allow(unused_variables)]
1261 fn __rustango_load_related(
1262 &mut self,
1263 row: &#root::sql::sqlx::postgres::PgRow,
1264 field_name: &str,
1265 alias: &str,
1266 ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1267 // Audit #451 — split the multi-hop path: `base` is the FK
1268 // on THIS model, `__rest` (if any) is the remaining chain
1269 // to stitch onto the loaded parent. `__next_alias` is the
1270 // accumulated join alias the parent's columns live under
1271 // (`{alias}__{next-hop}`), matching `lower_select_related`.
1272 let (__base, __rest): (&str, ::core::option::Option<&str>) =
1273 match field_name.split_once("__") {
1274 ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1275 ::core::option::Option::None => (field_name, ::core::option::Option::None),
1276 };
1277 let __next_alias: ::std::string::String = match __rest {
1278 ::core::option::Option::Some(__r) => {
1279 let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1280 ::std::format!("{}__{}", alias, __rb)
1281 }
1282 ::core::option::Option::None => ::std::string::String::new(),
1283 };
1284 match __base {
1285 #( #arms )*
1286 _ => ::core::result::Result::Ok(false),
1287 }
1288 }
1289 }
1290 }
1291}
1292
1293/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1294/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1295/// which expands to a `LoadRelatedMy` impl when rustango is built with
1296/// the `mysql` feature, and to nothing otherwise. The decoded parent
1297/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1298/// decoder, also batch8) so the dual emission is symmetric across
1299/// backends.
1300fn load_related_impl_my_tokens(
1301 struct_name: &syn::Ident,
1302 fk_relations: &[FkRelation],
1303) -> TokenStream2 {
1304 let root = rustango_root();
1305 let arms = fk_relations.iter().map(|rel| {
1306 let parent_ty = &rel.parent_type;
1307 let field_ident = &rel.field_ident;
1308 let fk_name = ident_name(field_ident);
1309 let fk_col = fk_name.as_str();
1310 let guard = join_guard_tokens(rel, "e!(__self));
1311 let reuse = loaded_reuse_tokens(
1312 rel,
1313 "e!(__self),
1314 "e!(#root::sql::LoadRelatedMy::__rustango_load_related_my),
1315 );
1316 let assign = loaded_assign_tokens(rel, "e!(__self));
1317 // `self` IS hygiene-tracked through macro_rules — emitted from
1318 // a different context than the `&mut self` parameter inside
1319 // the macro_rules-expanded fn. Pass it through as `__self`
1320 // and let the macro_rules rebind it to the receiver.
1321 quote! {
1322 #fk_col => {
1323 #guard
1324 #reuse
1325 let mut _parent: #parent_ty =
1326 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1327 // Audit #451 — multi-hop: stitch the deeper relation onto
1328 // the parent at the accumulated alias (see PG twin).
1329 if let ::core::option::Option::Some(__r) = __rest {
1330 let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1331 &mut _parent, row, __r, &__next_alias,
1332 )?;
1333 }
1334 #assign
1335 ::core::result::Result::Ok(true)
1336 }
1337 }
1338 });
1339 quote! {
1340 #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1341 #( #arms )*
1342 });
1343 }
1344}
1345
1346/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1347/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1348/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1349fn load_related_impl_sqlite_tokens(
1350 struct_name: &syn::Ident,
1351 fk_relations: &[FkRelation],
1352) -> TokenStream2 {
1353 let root = rustango_root();
1354 let arms = fk_relations.iter().map(|rel| {
1355 let parent_ty = &rel.parent_type;
1356 let field_ident = &rel.field_ident;
1357 let fk_name = ident_name(field_ident);
1358 let fk_col = fk_name.as_str();
1359 let guard = join_guard_tokens(rel, "e!(__self));
1360 let reuse = loaded_reuse_tokens(
1361 rel,
1362 "e!(__self),
1363 "e!(#root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite),
1364 );
1365 let assign = loaded_assign_tokens(rel, "e!(__self));
1366 quote! {
1367 #fk_col => {
1368 #guard
1369 #reuse
1370 let mut _parent: #parent_ty =
1371 <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1372 // Audit #451 — multi-hop: stitch the deeper relation onto
1373 // the parent at the accumulated alias (see PG twin).
1374 if let ::core::option::Option::Some(__r) = __rest {
1375 let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1376 &mut _parent, row, __r, &__next_alias,
1377 )?;
1378 }
1379 #assign
1380 ::core::result::Result::Ok(true)
1381 }
1382 }
1383 });
1384 quote! {
1385 #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1386 #( #arms )*
1387 });
1388 }
1389}
1390
1391/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1392/// matches `field_name` against the model's FK fields and returns
1393/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1394/// group children by parent PK.
1395///
1396/// Always emitted (with `_ => None` for FK-less models) so the
1397/// trait bound on `fetch_with_prefetch` is universally satisfied.
1398fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1399 let root = rustango_root();
1400 let arms = fk_relations.iter().map(|rel| {
1401 let field_ident = &rel.field_ident;
1402 let fk_name = ident_name(field_ident);
1403 let fk_col = fk_name.as_str();
1404 if rel.pk_kind == DetectedKind::I64 {
1405 // i64 FK — return the stored PK so prefetch_related can
1406 // group children by it. Nullable variant unwraps via
1407 // `as_ref().map(...)`: an unset (NULL) FK column yields
1408 // `None` and that child sits out of the grouping (correct
1409 // semantics — it has no parent to attach to).
1410 if rel.nullable {
1411 quote! {
1412 #fk_col => self.#field_ident
1413 .as_ref()
1414 .map(|fk| #root::sql::ForeignKey::pk(fk)),
1415 }
1416 } else {
1417 quote! {
1418 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1419 }
1420 }
1421 } else {
1422 // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1423 // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1424 // i64-keyed grouping path — the trait signature is
1425 // `Option<i64>` and a non-i64 PK can't lower into it.
1426 // The FK still works for everything else (CRUD, lazy
1427 // load via `.get()`, select_related JOINs); only the
1428 // bulk prefetch grouper needs the integer key.
1429 quote! {
1430 #fk_col => ::core::option::Option::None,
1431 }
1432 }
1433 });
1434 // PK-type-agnostic version: every FK arm emits an
1435 // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1436 // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1437 // opt OUT of the legacy i64 method (it returns None) but opt IN
1438 // here.
1439 let value_arms = fk_relations.iter().map(|rel| {
1440 let field_ident = &rel.field_ident;
1441 let fk_name = ident_name(field_ident);
1442 let fk_col = fk_name.as_str();
1443 if rel.nullable {
1444 quote! {
1445 #fk_col => self.#field_ident
1446 .as_ref()
1447 .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1448 #root::sql::ForeignKey::pk(fk)
1449 )),
1450 }
1451 } else {
1452 quote! {
1453 #fk_col => ::core::option::Option::Some(
1454 ::core::convert::Into::<#root::core::SqlValue>::into(
1455 self.#field_ident.pk()
1456 )
1457 ),
1458 }
1459 }
1460 });
1461 quote! {
1462 impl #root::sql::FkPkAccess for #struct_name {
1463 #[allow(unused_variables)]
1464 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1465 match field_name {
1466 #( #arms )*
1467 _ => ::core::option::Option::None,
1468 }
1469 }
1470 #[allow(unused_variables)]
1471 fn __rustango_fk_pk_value(
1472 &self,
1473 field_name: &str,
1474 ) -> ::core::option::Option<#root::core::SqlValue> {
1475 match field_name {
1476 #( #value_arms )*
1477 _ => ::core::option::Option::None,
1478 }
1479 }
1480 }
1481 }
1482}
1483
1484/// `related_name = "+"`: the FK gets no reverse accessor on its parent.
1485const NO_REVERSE_ACCESSOR: &str = "+";
1486
1487/// For every `ForeignKey<Parent>` field on `Child`, emit
1488/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1489/// Reads the column the FK points at (`on`, else the PK) and runs one
1490/// `SELECT … WHERE <fk_column> = $1`: one round trip, no N+1.
1491///
1492/// **PG-only emission**: the accessor is bounded on
1493/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1494/// both of which are gated behind the `postgres` cargo feature. The
1495/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1496/// model derive itself compiles on tri-dialect / sqlite-only
1497/// downstream builds — the accessor just isn't materialised. A tri-
1498/// dialect `_set_pool` variant is a separate follow-up.
1499fn reverse_helper_tokens(
1500 child_ident: &syn::Ident,
1501 fk_relations: &[FkRelation],
1502 default_related_name: Option<&str>,
1503) -> syn::Result<TokenStream2> {
1504 let root = rustango_root();
1505 // `related_name = "+"`: no accessor, so a parent from another crate works.
1506 let fk_relations: Vec<&FkRelation> = fk_relations
1507 .iter()
1508 .filter(|rel| rel.related_name.as_deref() != Some(NO_REVERSE_ACCESSOR))
1509 .collect();
1510 if fk_relations.is_empty() {
1511 return Ok(TokenStream2::new());
1512 }
1513 // Method-name resolution per FK (issue #816 + follow-up):
1514 // 1. Field-level `#[rustango(related_name = "...")]` on the FK
1515 // itself — wins over everything else.
1516 // 2. Container-level `default_related_name = "..."` on the
1517 // child. Applies to every FK on this model that didn't
1518 // override.
1519 // 3. Fallback: `<child_snake>_set`. `Post` → `post_set`,
1520 // `BlogComment` → `blog_comment_set`. Avoids English-plural
1521 // edge cases.
1522 //
1523 // The PG-on-executor variant keeps the resolved name; the
1524 // tri-dialect `_pool` variant appends `_pool` to it (matches the
1525 // framework's convention for the `&Pool` flavor of every helper).
1526 let default_pg_suffix = default_related_name
1527 .map(str::to_owned)
1528 .unwrap_or_else(|| format!("{}_set", to_snake_case(&ident_name(child_ident))));
1529 let name_of = |rel: &FkRelation| {
1530 rel.related_name
1531 .clone()
1532 .unwrap_or_else(|| default_pg_suffix.clone())
1533 };
1534 // Two FKs to one parent would define the same method twice (E0592).
1535 for (i, rel) in fk_relations.iter().enumerate() {
1536 let parent_ty = &rel.parent_type;
1537 let parent = quote!(#parent_ty).to_string();
1538 let name = name_of(rel);
1539 if let Some(first) = fk_relations[..i].iter().find(|r| {
1540 let other = &r.parent_type;
1541 quote!(#other).to_string() == parent && name_of(r) == name
1542 }) {
1543 return Err(syn::Error::new_spanned(
1544 &rel.field_ident,
1545 format!(
1546 "`{}` and `{}` both point at `{parent}` and would both add `{parent}::{name}`; \
1547 give one `#[rustango(related_name = \"...\")]`, or `related_name = \"+\"` for none",
1548 first.field_ident, rel.field_ident
1549 ),
1550 ));
1551 }
1552 }
1553 let impls = fk_relations.iter().map(|rel| {
1554 let pg_suffix = name_of(rel);
1555 let pool_suffix = format!("{}_pool", pg_suffix);
1556 let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1557 let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1558 let parent_ty = &rel.parent_type;
1559 let fk_name = ident_name(&rel.field_ident);
1560 let fk_col = fk_name.as_str();
1561 let doc = format!(
1562 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1563 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1564 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1565 further `{child_ident}::objects()` filters via direct queryset use."
1566 );
1567 let pool_doc = format!(
1568 "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1569 [`#root::sql::Pool`] and dispatches per backend so the \
1570 reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1571 Use this from framework code that holds a `&Pool` (admin, \
1572 tenancy resolver, viewset handlers); reach for the executor- \
1573 bound variant when you already have a typed `sqlx::Executor`."
1574 );
1575 quote! {
1576 #[cfg(feature = "postgres")]
1577 impl #parent_ty {
1578 #[doc = #doc]
1579 ///
1580 /// # Errors
1581 /// Returns [`#root::sql::ExecError`] for SQL-writing
1582 /// or driver failures.
1583 pub async fn #pg_method_ident<'_c, _E>(
1584 &self,
1585 _executor: _E,
1586 ) -> ::core::result::Result<
1587 ::std::vec::Vec<#child_ident>,
1588 #root::sql::ExecError,
1589 >
1590 where
1591 _E: #root::sql::sqlx::Executor<
1592 '_c,
1593 Database = #root::sql::sqlx::Postgres,
1594 >,
1595 {
1596 let _key = #root::sql::__reverse_fk_key::<Self, #child_ident>(self, #fk_col)?;
1597 #root::query::QuerySet::<#child_ident>::new()
1598 .filter_op(#fk_col, #root::core::Op::Eq, _key)
1599 .fetch_on(_executor)
1600 .await
1601 }
1602 }
1603
1604 impl #parent_ty {
1605 #[doc = #pool_doc]
1606 ///
1607 /// # Errors
1608 /// Returns [`#root::sql::ExecError`] for SQL-writing
1609 /// or driver failures.
1610 pub async fn #pool_method_ident(
1611 &self,
1612 pool: &#root::sql::Pool,
1613 ) -> ::core::result::Result<
1614 ::std::vec::Vec<#child_ident>,
1615 #root::sql::ExecError,
1616 > {
1617 use #root::sql::FetcherPool as _;
1618 let _key = #root::sql::__reverse_fk_key::<Self, #child_ident>(self, #fk_col)?;
1619 #root::query::QuerySet::<#child_ident>::new()
1620 .filter_op(#fk_col, #root::core::Op::Eq, _key)
1621 .fetch(pool)
1622 .await
1623 }
1624 }
1625 }
1626 });
1627 Ok(quote! { #( #impls )* })
1628}
1629
1630/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1631/// relation declared on the model.
1632/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1633/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1634///
1635/// Closes #239 + #240 — `comment.content_object` /
1636/// `comment.content_object = post` ergonomics on top of the existing
1637/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1638///
1639/// `column_entries` is passed so we can resolve each `ct_column` /
1640/// `pk_column` SQL name back to its Rust field ident — the macro
1641/// only sees the column-side strings in the attribute, but the
1642/// emitted accessor needs to read the actual struct field.
1643fn generic_fk_accessor_tokens(
1644 struct_name: &syn::Ident,
1645 generic_fks: &[GenericFkAttr],
1646 column_entries: &[ColumnEntry],
1647) -> TokenStream2 {
1648 let root = rustango_root();
1649 if generic_fks.is_empty() {
1650 return TokenStream2::new();
1651 }
1652 let methods = generic_fks.iter().filter_map(|gfk| {
1653 // Resolve `ct_column` + `pk_column` to the struct's Rust
1654 // field idents. A typo (column name doesn't match any field)
1655 // emits no method for that registration — the user will see
1656 // the compiler reject the SCHEMA literal anyway, so there's
1657 // a clear error path without us double-reporting.
1658 let ct_ident = column_entries
1659 .iter()
1660 .find(|c| c.column == gfk.ct_column)
1661 .map(|c| c.ident.clone())?;
1662 let pk_ident = column_entries
1663 .iter()
1664 .find(|c| c.column == gfk.pk_column)
1665 .map(|c| c.ident.clone())?;
1666
1667 let accessor_ident =
1668 syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1669 let setter_ident =
1670 syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1671 let name_literal = gfk.name.as_str();
1672
1673 Some(quote! {
1674 #[doc = concat!(
1675 "Resolve the polymorphic `",
1676 #name_literal,
1677 "` relation. Reads `self.",
1678 stringify!(#ct_ident),
1679 "` + `self.",
1680 stringify!(#pk_ident),
1681 "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1682 "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1683 #name_literal,
1684 "\", ...))]`."
1685 )]
1686 pub async fn #accessor_ident(
1687 &self,
1688 pool: &#root::sql::Pool,
1689 ) -> ::core::result::Result<
1690 ::core::option::Option<#root::__serde_json::Value>,
1691 #root::sql::ExecError,
1692 > {
1693 let gfk = #root::contenttypes::GenericForeignKey::new(
1694 self.#ct_ident as i64,
1695 self.#pk_ident as i64,
1696 );
1697 gfk.get_object(pool).await
1698 }
1699
1700 #[doc = concat!(
1701 "Set the polymorphic `",
1702 #name_literal,
1703 "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1704 stringify!(#ct_ident),
1705 "` and `self.",
1706 stringify!(#pk_ident),
1707 "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1708 #name_literal,
1709 "\", ...))]`."
1710 )]
1711 pub async fn #setter_ident<T: #root::core::Model>(
1712 &mut self,
1713 pool: &#root::sql::Pool,
1714 target_pk: i64,
1715 ) -> ::core::result::Result<(), #root::sql::ExecError> {
1716 let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1717 pool,
1718 target_pk,
1719 ).await?;
1720 self.#ct_ident = gfk.content_type_id as _;
1721 self.#pk_ident = gfk.object_pk as _;
1722 ::core::result::Result::Ok(())
1723 }
1724 })
1725 });
1726 quote! {
1727 impl #struct_name {
1728 #( #methods )*
1729 }
1730 }
1731}
1732
1733fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1734 let root = rustango_root();
1735 if m2m_relations.is_empty() {
1736 return TokenStream2::new();
1737 }
1738 let methods = m2m_relations.iter().map(|rel| {
1739 let method_name = format!("{}_m2m", rel.name);
1740 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1741 let through = rel.through.as_str();
1742 let src_col = rel.src.as_str();
1743 let dst_col = rel.dst.as_str();
1744 quote! {
1745 pub fn #method_ident(&self) -> #root::sql::M2MManager {
1746 #root::sql::M2MManager {
1747 src_pk: self.__rustango_pk_value(),
1748 through: #through,
1749 src_col: #src_col,
1750 dst_col: #dst_col,
1751 }
1752 }
1753 }
1754 });
1755 quote! {
1756 impl #struct_name {
1757 #( #methods )*
1758 }
1759 }
1760}
1761
1762/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1763/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1764fn generic_m2m_accessor_tokens(
1765 struct_name: &syn::Ident,
1766 relations: &[GenericM2MAttr],
1767) -> TokenStream2 {
1768 let root = rustango_root();
1769 if relations.is_empty() {
1770 return TokenStream2::new();
1771 }
1772 let methods = relations.iter().map(|rel| {
1773 let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1774 let through = rel.through.as_str();
1775 let pk_col = rel.pk_column.as_str();
1776 let ct_col = rel.ct_column.as_str();
1777 let related_col = rel.related_column.as_str();
1778 quote! {
1779 pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1780 #root::sql::GenericM2MManager {
1781 src_pk: self.__rustango_pk_value(),
1782 src_schema: <Self as #root::core::Model>::SCHEMA,
1783 through: #through,
1784 pk_col: #pk_col,
1785 ct_col: #ct_col,
1786 dst_col: #related_col,
1787 }
1788 }
1789 }
1790 });
1791 // Lets an owner delete find and clear its junction rows.
1792 let entries = relations.iter().map(|rel| {
1793 let through = rel.through.as_str();
1794 let pk_col = rel.pk_column.as_str();
1795 let ct_col = rel.ct_column.as_str();
1796 quote! {
1797 #root::core::inventory::submit! {
1798 #root::core::GenericM2MEntry::new(
1799 <#struct_name as #root::core::Model>::SCHEMA,
1800 #through,
1801 #pk_col,
1802 #ct_col,
1803 )
1804 }
1805 }
1806 });
1807 quote! {
1808 impl #struct_name {
1809 #( #methods )*
1810 }
1811 #( #entries )*
1812 }
1813}
1814
1815/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1816/// associated functions for each `#[rustango(reverse_has(...))]`
1817/// attribute. Issue #830.
1818///
1819/// The two emitted functions return ready-to-use `WhereExpr` nodes
1820/// that downstream callers drop into
1821/// `QuerySet::<Self>::where_raw(...)`:
1822///
1823/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1824/// <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1825/// `whereHas` parity (without the closure-style sub-predicate
1826/// refinement — that's a follow-up; users can layer additional
1827/// predicates by constructing the SelectQuery themselves and
1828/// calling `where_raw(exists(query))` from `crate::core::subquery`).
1829/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1830/// `whereDoesntHave` parity.
1831///
1832/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1833/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1834/// machinery threads the outer-table reference through automatically
1835/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1836fn reverse_has_accessor_tokens(
1837 struct_name: &syn::Ident,
1838 reverse_has_relations: &[ReverseHasAttr],
1839 columns: &[ColumnEntry],
1840) -> TokenStream2 {
1841 let root = rustango_root();
1842 if reverse_has_relations.is_empty() {
1843 return TokenStream2::new();
1844 }
1845 let methods = reverse_has_relations.iter().map(|rel| {
1846 let exists_name = format!("{}_exists_expr", rel.name);
1847 let not_exists_name = format!("{}_not_exists_expr", rel.name);
1848 let count_name = format!("{}_count", rel.name);
1849 let fetch_name = format!("{}_fetch", rel.name);
1850 let first_name = format!("{}_first", rel.name);
1851 let pluck_name = format!("{}_pluck", rel.name);
1852 let accessor_name = rel.name.as_str();
1853 let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1854 let not_exists_ident = syn::Ident::new(¬_exists_name, struct_name.span());
1855 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1856 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1857 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1858 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1859 let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1860 let child = &rel.child;
1861 let child_fk_column = rel.child_fk_column.as_str();
1862 let self_pk_column = rel.self_pk_column();
1863 // The instance helpers filter on the value the EXISTS form joins on.
1864 let self_value = columns
1865 .iter()
1866 .find(|c| c.column == self_pk_column)
1867 .map_or_else(
1868 || quote! { self.__rustango_pk_value() },
1869 |c| {
1870 let ident = &c.ident;
1871 quote! {
1872 ::core::convert::Into::<#root::core::SqlValue>::into(
1873 ::core::clone::Clone::clone(&self.#ident)
1874 )
1875 }
1876 },
1877 );
1878 let exists_doc = format!(
1879 "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1880 FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1881 Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1882 filter to {struct_name}s with at least one matching child.",
1883 );
1884 let not_exists_doc = format!(
1885 "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1886 (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1887 <outer>.{self_pk_column})`. Drop into \
1888 `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1889 {struct_name}s with **no** matching child.",
1890 );
1891 let count_doc = format!(
1892 "Eloquent `$model->{name}->count()` analog — returns \
1893 the number of `{child}` rows whose `{child_fk_column}` \
1894 matches this `{struct_name}` instance's `{self_pk_column}`. \
1895 Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1896 {child_fk_column} = <self.{self_pk_column}>`.",
1897 name = rel.name,
1898 );
1899 let accessor_doc = format!(
1900 "Eloquent `$model->{name}` accessor — returns a \
1901 `QuerySet<{child}>` filtered to rows whose \
1902 `{child_fk_column}` matches this `{struct_name}` \
1903 instance's `{self_pk_column}`. **Chainable**: compose `.filter()` \
1904 / `.order_by()` / `.limit()` etc. on top, then call \
1905 `.fetch(&pool)` (the QuerySet trait method) when \
1906 done. For the simple \"fetch all\" hot path with no \
1907 further composition, prefer the bare-name \
1908 `{name}_fetch(&pool)` companion.",
1909 name = rel.name,
1910 );
1911 let fetch_doc = format!(
1912 "Eloquent `$model->{name}->get()` — bare-name hot-path \
1913 over `{name}(&self).fetch(&pool)`. Use this when \
1914 you don't need further `.filter()` / `.order_by()` \
1915 composition; falls back to the chainable accessor when \
1916 you do. Avoids the `_pool` suffix on the most common \
1917 call-site shape.",
1918 name = rel.name,
1919 );
1920 quote! {
1921 #[doc = #accessor_doc]
1922 pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1923 #root::query::QuerySet::<#child>::new()
1924 .filter(#child_fk_column, #self_value)
1925 }
1926
1927 #[doc = #fetch_doc]
1928 pub async fn #fetch_ident(
1929 &self,
1930 pool: &#root::sql::Pool,
1931 ) -> ::core::result::Result<
1932 ::std::vec::Vec<#child>,
1933 #root::sql::ExecError,
1934 > {
1935 use #root::sql::FetcherPool as _;
1936 self.#accessor_ident().fetch(pool).await
1937 }
1938
1939 /// Eloquent `$model->relation->first()` / `hasOne`
1940 /// semantics — bare-name shortcut over
1941 /// `self.<name>().first(&pool)`. Returns `None` when no
1942 /// child rows match. Useful when the relation is
1943 /// nominally many-to-one in shape but at most one row is
1944 /// expected (latest comment, primary tag, etc.).
1945 pub async fn #first_ident(
1946 &self,
1947 pool: &#root::sql::Pool,
1948 ) -> ::core::result::Result<
1949 ::core::option::Option<#child>,
1950 #root::sql::ExecError,
1951 > {
1952 self.#accessor_ident().first(pool).await
1953 }
1954
1955 /// Eloquent `$model->relation->pluck($col)` — project a
1956 /// single column from the child rows into a `Vec<U>`.
1957 /// Skips the typed `Child` decode, which is cheaper when
1958 /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1959 pub async fn #pluck_ident<U>(
1960 &self,
1961 col: &'static str,
1962 pool: &#root::sql::Pool,
1963 ) -> ::core::result::Result<
1964 ::std::vec::Vec<U>,
1965 #root::sql::ExecError,
1966 >
1967 where
1968 U: #root::sql::FlatScalar,
1969 {
1970 self.#accessor_ident()
1971 .values_list_flat(col)
1972 .fetch::<U>(pool)
1973 .await
1974 }
1975
1976 #[doc = #exists_doc]
1977 pub fn #exists_ident() -> #root::core::WhereExpr {
1978 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1979 let child_schema =
1980 <#child as #root::core::Model>::SCHEMA;
1981 let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1982 lhs: Expr::Column(#child_fk_column),
1983 op: Op::Eq,
1984 rhs: Expr::OuterRef(#self_pk_column),
1985 });
1986 WhereExpr::Exists(::std::boxed::Box::new(inner))
1987 }
1988
1989 #[doc = #not_exists_doc]
1990 pub fn #not_exists_ident() -> #root::core::WhereExpr {
1991 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1992 let child_schema =
1993 <#child as #root::core::Model>::SCHEMA;
1994 let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1995 lhs: Expr::Column(#child_fk_column),
1996 op: Op::Eq,
1997 rhs: Expr::OuterRef(#self_pk_column),
1998 });
1999 WhereExpr::NotExists(::std::boxed::Box::new(inner))
2000 }
2001
2002 #[doc = #count_doc]
2003 pub async fn #count_ident(
2004 &self,
2005 pool: &#root::sql::Pool,
2006 ) -> ::core::result::Result<
2007 i64,
2008 #root::sql::ExecError,
2009 > {
2010 use #root::sql::CounterPool as _;
2011 self.#accessor_ident().count(pool).await
2012 }
2013 }
2014 });
2015 quote! {
2016 impl #struct_name {
2017 #( #methods )*
2018 }
2019 }
2020}
2021
2022/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
2023/// `#[rustango(through(...))]` attribute. Issue #817.
2024///
2025/// Each accessor builds a correlated subquery via
2026/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
2027/// intermediate table, filters on the FK column pointing at this
2028/// model, and projects the intermediate PK. The outer queryset
2029/// filters the far model by its FK-to-intermediate column being in
2030/// that set.
2031///
2032/// The returned `QuerySet<Far>` is **chainable** — the subquery
2033/// lives inside a `where_raw` clause so the user's later
2034/// `.filter()` / `.order_by()` / `.limit()` compositions don't
2035/// disturb it.
2036///
2037/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
2038/// SQLite — no LATERAL or backend-specific syntax involved.
2039fn through_accessor_tokens(
2040 struct_name: &syn::Ident,
2041 through_relations: &[ThroughAttr],
2042) -> TokenStream2 {
2043 let root = rustango_root();
2044 if through_relations.is_empty() {
2045 return TokenStream2::new();
2046 }
2047 let methods = through_relations.iter().map(|rel| {
2048 let method_name = format!("{}_through", rel.name);
2049 let count_name = format!("{}_through_count", rel.name);
2050 let fetch_name = format!("{}_through_fetch", rel.name);
2051 let first_name = format!("{}_through_first", rel.name);
2052 let pluck_name = format!("{}_through_pluck", rel.name);
2053 let method_ident = syn::Ident::new(&method_name, struct_name.span());
2054 let count_ident = syn::Ident::new(&count_name, struct_name.span());
2055 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
2056 let first_ident = syn::Ident::new(&first_name, struct_name.span());
2057 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
2058 let far = &rel.far;
2059 let intermediate = &rel.intermediate;
2060 let far_fk_column = rel.far_fk_column.as_str();
2061 let intermediate_fk_column = rel.intermediate_fk_column.as_str();
2062 let intermediate_pk_column = rel.intermediate_pk_column.as_str();
2063 let doc = format!(
2064 "Eloquent `hasManyThrough` accessor — returns a \
2065 `QuerySet<{far}>` whose rows reach this `{struct_name}` \
2066 instance through the intermediate `{intermediate}` table. \
2067 Generated SQL shape: \
2068 `… WHERE {far_fk_column} IN (SELECT \
2069 {intermediate_pk_column} FROM <{intermediate}> WHERE \
2070 {intermediate_fk_column} = self.pk)`. Chainable like any \
2071 other QuerySet — compose `.filter()` / `.order_by()` / \
2072 `.limit()` etc. on top.",
2073 );
2074 let count_doc = format!(
2075 "Eloquent `$model->{name}->count()` analog for the \
2076 through-relation — returns the number of `{far}` rows \
2077 reachable through `{intermediate}`. Equivalent to \
2078 `self.{name}_through().count(pool)` but spelled \
2079 as a bare instance method for parity with the \
2080 `reverse_has` `<name>_count` shape.",
2081 name = rel.name,
2082 );
2083 quote! {
2084 #[doc = #doc]
2085 pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
2086 use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
2087 let intermediate_schema =
2088 <#intermediate as #root::core::Model>::SCHEMA;
2089 let sub = SelectQuery::new(intermediate_schema)
2090 .where_clause(WhereExpr::Predicate(Filter::new(
2091 #intermediate_fk_column,
2092 Op::Eq,
2093 self.__rustango_pk_value(),
2094 )))
2095 .projection(::std::vec![#intermediate_pk_column]);
2096 #root::query::QuerySet::<#far>::new().where_raw(
2097 WhereExpr::InSubquery {
2098 column: #far_fk_column,
2099 negated: false,
2100 subquery: ::std::boxed::Box::new(sub),
2101 },
2102 )
2103 }
2104
2105 #[doc = #count_doc]
2106 pub async fn #count_ident(
2107 &self,
2108 pool: &#root::sql::Pool,
2109 ) -> ::core::result::Result<
2110 i64,
2111 #root::sql::ExecError,
2112 > {
2113 use #root::sql::CounterPool as _;
2114 self.#method_ident().count(pool).await
2115 }
2116
2117 /// Eloquent `$model->relation->get()` for the
2118 /// through-relation — bare-name hot-path over
2119 /// `self.<name>_through().fetch(&pool)`. Use this
2120 /// when you don't need further `.filter()` /
2121 /// `.order_by()` composition; falls back to the
2122 /// chainable accessor when you do. Avoids the `_pool`
2123 /// suffix on the most common call-site shape.
2124 pub async fn #fetch_ident(
2125 &self,
2126 pool: &#root::sql::Pool,
2127 ) -> ::core::result::Result<
2128 ::std::vec::Vec<#far>,
2129 #root::sql::ExecError,
2130 > {
2131 use #root::sql::FetcherPool as _;
2132 self.#method_ident().fetch(pool).await
2133 }
2134
2135 /// Eloquent `hasOneThrough` analog — bare-name shortcut
2136 /// over `self.<name>_through().first(&pool)`. Returns
2137 /// `None` when no far rows are reachable through the
2138 /// intermediate. Useful when at most one row is
2139 /// expected (latest comment by country, primary tag,
2140 /// etc.).
2141 pub async fn #first_ident(
2142 &self,
2143 pool: &#root::sql::Pool,
2144 ) -> ::core::result::Result<
2145 ::core::option::Option<#far>,
2146 #root::sql::ExecError,
2147 > {
2148 self.#method_ident().first(pool).await
2149 }
2150
2151 /// Pluck a single column from the far rows into `Vec<U>`
2152 /// — cheaper than the typed `<Far>` decode when only one
2153 /// scalar column is needed.
2154 pub async fn #pluck_ident<U>(
2155 &self,
2156 col: &'static str,
2157 pool: &#root::sql::Pool,
2158 ) -> ::core::result::Result<
2159 ::std::vec::Vec<U>,
2160 #root::sql::ExecError,
2161 >
2162 where
2163 U: #root::sql::FlatScalar,
2164 {
2165 self.#method_ident()
2166 .values_list_flat(col)
2167 .fetch::<U>(pool)
2168 .await
2169 }
2170 }
2171 });
2172 quote! {
2173 impl #struct_name {
2174 #( #methods )*
2175 }
2176 }
2177}
2178
2179struct ColumnEntry {
2180 /// The struct field ident, used both for the inherent const name on
2181 /// the model and for the inner column type's name.
2182 ident: syn::Ident,
2183 /// The struct's field type, used as `Column::Value`.
2184 value_ty: Type,
2185 /// Rust-side field name (e.g. `"id"`).
2186 name: String,
2187 /// SQL-side column name (e.g. `"user_id"`).
2188 column: String,
2189 /// `#root::core::FieldType::I64` etc.
2190 field_type_tokens: TokenStream2,
2191 /// A bare or optional `Uuid` field; MySQL decodes it from text.
2192 uuid_column: bool,
2193}
2194
2195struct CollectedFields {
2196 field_schemas: Vec<TokenStream2>,
2197 from_row_inits: Vec<TokenStream2>,
2198 /// Aliased counterparts of `from_row_inits` — read columns via
2199 /// `format!("{prefix}__{col}")` aliases so a Model can be
2200 /// decoded from a JOINed row's projected target columns.
2201 from_aliased_row_inits: Vec<TokenStream2>,
2202 /// MySQL twins of `from_row_inits` / `from_aliased_row_inits`.
2203 from_row_inits_my: Vec<TokenStream2>,
2204 from_aliased_row_inits_my: Vec<TokenStream2>,
2205 /// Static column-name list — used by the simple insert path
2206 /// (no `Auto<T>` fields). Aligned with `insert_values`.
2207 insert_columns: Vec<TokenStream2>,
2208 /// Static `Into<SqlValue>` expressions, one per field. Aligned
2209 /// with `insert_columns`. Used by the simple insert path only.
2210 insert_values: Vec<TokenStream2>,
2211 /// Per-field push expressions for the dynamic (Auto-aware)
2212 /// insert path. Each statement either unconditionally pushes
2213 /// `(column, value)` or, for an `Auto<T>` field, conditionally
2214 /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
2215 insert_pushes: Vec<TokenStream2>,
2216 /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
2217 /// when `has_auto == false`.
2218 returning_cols: Vec<TokenStream2>,
2219 /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
2220 /// field. Run after `insert_returning` to populate the model.
2221 auto_assigns: Vec<TokenStream2>,
2222 /// `(ident, column_literal, is_uuid)` for every Auto field. Used by
2223 /// the bulk_insert codegen to rebuild assigns against `_row_mut`
2224 /// instead of `self`.
2225 auto_field_idents: Vec<(syn::Ident, String, bool)>,
2226 /// #1028 — `(ident, column)` for each `generated_as` field. Drives
2227 /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
2228 /// back into the struct after insert (MySQL has no RETURNING → the
2229 /// field stays at its placeholder until the next read).
2230 generated_field_idents: Vec<(syn::Ident, String)>,
2231 /// First `Auto<T>` field the database fills, with its inner `T`: the
2232 /// MySQL `LAST_INSERT_ID()` target. Rust-filled ones never are (#1934).
2233 first_db_auto: Option<(syn::Ident, Type)>,
2234 /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2235 /// by the all-Auto-Unset bulk path (Auto cols dropped from
2236 /// `columns`).
2237 bulk_pushes_no_auto: Vec<TokenStream2>,
2238 /// Bulk-insert per-row pushes for **all fields including Auto**.
2239 /// Used by the all-Auto-Set bulk path (Auto col included with the
2240 /// caller-supplied value).
2241 bulk_pushes_all: Vec<TokenStream2>,
2242 /// Column-name literals for non-Auto fields only (paired with
2243 /// `bulk_pushes_no_auto`).
2244 bulk_columns_no_auto: Vec<TokenStream2>,
2245 /// Column-name literals for every field including Auto (paired
2246 /// with `bulk_pushes_all`).
2247 bulk_columns_all: Vec<TokenStream2>,
2248 /// Fills each `default_uuid_v7` field of `_row` in place, so a
2249 /// mutable bulk insert hands the PKs back (#1934).
2250 bulk_fill_in_place: Vec<TokenStream2>,
2251 /// Rust-filled `Auto` fields (`default_uuid_v7`, `auto_now(_add)`), reset when `insert_or_ignore` skips (#1937).
2252 rust_filled_idents: Vec<syn::Ident>,
2253 /// `auto_now` fields: `upsert` stamps them before binding.
2254 auto_now_idents: Vec<syn::Ident>,
2255 /// `default_uuid_v7` fields: an `Unset` one gets a fresh id, so the
2256 /// upsert marks it `generated` for the MySQL key guard.
2257 uuid_v7_fields: Vec<(syn::Ident, String)>,
2258 /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2259 /// + the loop that asserts every row matches. One pair per Auto
2260 /// field. Empty when `has_auto == false`.
2261 bulk_auto_uniformity: Vec<TokenStream2>,
2262 /// Identifier of the first Auto field, used as the witness for
2263 /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2264 first_auto_ident: Option<syn::Ident>,
2265 /// `true` if any field on the struct is `Auto<T>`.
2266 has_auto: bool,
2267 /// `true` when the primary-key field's Rust type is `Auto<T>`.
2268 /// Gates `save()` codegen — only Auto PKs let us infer
2269 /// insert-vs-update from the in-memory value.
2270 pk_is_auto: bool,
2271 /// `Assignment` constructors for every non-PK column. Drives the
2272 /// UPDATE branch of `save()`.
2273 update_assignments: Vec<TokenStream2>,
2274 /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2275 /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2276 upsert_update_columns: Vec<TokenStream2>,
2277 primary_key: Option<(syn::Ident, String)>,
2278 column_entries: Vec<ColumnEntry>,
2279 /// Rust-side field names, in declaration order. Used to validate
2280 /// container attributes like `display = "…"`.
2281 field_names: Vec<String>,
2282 /// FK fields on this child model. Drives the reverse-relation
2283 /// helper emit — for each FK, the macro adds an inherent
2284 /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2285 /// method on the parent type.
2286 fk_relations: Vec<FkRelation>,
2287 /// SQL column name of the `#[rustango(soft_delete)]` field, if
2288 /// the model has one. Drives emission of the `soft_delete_on` /
2289 /// `restore_on` inherent methods. At most one such column per
2290 /// model is allowed; collect_fields rejects duplicates.
2291 soft_delete_column: Option<String>,
2292 /// Rust field ident of the `#[rustango(soft_delete)]` field —
2293 /// companion to `soft_delete_column` for emitting predicates
2294 /// that need to read the field off `&self` (e.g. `trashed()`).
2295 soft_delete_field_ident: Option<syn::Ident>,
2296}
2297
2298#[derive(Clone)]
2299struct FkRelation {
2300 /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2301 /// helper is emitted as `impl <ParentType> { … }`.
2302 parent_type: Type,
2303 /// Rust field ident of the FK. Its name is the key for select_related,
2304 /// prefetch and filters; the SQL column may differ (#1936).
2305 field_ident: syn::Ident,
2306 /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2307 /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2308 /// default `ForeignKey<T>` (no explicit K); other kinds when the
2309 /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2310 pk_kind: DetectedKind,
2311 /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2312 /// FK column). Drives the `Some(...)` wrapping in load_related
2313 /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2314 /// the codegen matches the field's declared shape.
2315 nullable: bool,
2316 /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2317 /// override. When set, the reverse helper picks this name instead
2318 /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2319 /// #816 (issue's "Related" note re: per-FK override).
2320 related_name: Option<String>,
2321}
2322
2323fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2324 let root = rustango_root();
2325 let cap = named.named.len();
2326 let mut out = CollectedFields {
2327 field_schemas: Vec::with_capacity(cap),
2328 from_row_inits: Vec::with_capacity(cap),
2329 from_aliased_row_inits: Vec::with_capacity(cap),
2330 from_row_inits_my: Vec::with_capacity(cap),
2331 from_aliased_row_inits_my: Vec::with_capacity(cap),
2332 insert_columns: Vec::with_capacity(cap),
2333 insert_values: Vec::with_capacity(cap),
2334 insert_pushes: Vec::with_capacity(cap),
2335 returning_cols: Vec::new(),
2336 auto_assigns: Vec::new(),
2337 auto_field_idents: Vec::new(),
2338 generated_field_idents: Vec::new(),
2339 first_db_auto: None,
2340 bulk_pushes_no_auto: Vec::with_capacity(cap),
2341 bulk_pushes_all: Vec::with_capacity(cap),
2342 bulk_columns_no_auto: Vec::with_capacity(cap),
2343 bulk_columns_all: Vec::with_capacity(cap),
2344 bulk_fill_in_place: Vec::new(),
2345 rust_filled_idents: Vec::new(),
2346 auto_now_idents: Vec::new(),
2347 uuid_v7_fields: Vec::new(),
2348 bulk_auto_uniformity: Vec::new(),
2349 first_auto_ident: None,
2350 has_auto: false,
2351 pk_is_auto: false,
2352 update_assignments: Vec::with_capacity(cap),
2353 upsert_update_columns: Vec::with_capacity(cap),
2354 primary_key: None,
2355 column_entries: Vec::with_capacity(cap),
2356 field_names: Vec::with_capacity(cap),
2357 fk_relations: Vec::new(),
2358 soft_delete_column: None,
2359 soft_delete_field_ident: None,
2360 };
2361
2362 for field in &named.named {
2363 let info = process_field(field, table)?;
2364 out.field_names.push(ident_name(info.ident));
2365 out.field_schemas.push(info.schema);
2366 out.from_row_inits.push(info.from_row_init);
2367 out.from_aliased_row_inits.push(info.from_aliased_row_init);
2368 out.from_row_inits_my.push(info.from_row_init_my);
2369 out.from_aliased_row_inits_my
2370 .push(info.from_aliased_row_init_my);
2371 if let Some(parent_ty) = info.fk_inner.clone() {
2372 out.fk_relations.push(FkRelation {
2373 parent_type: parent_ty,
2374 field_ident: info.ident.clone(),
2375 pk_kind: info.fk_pk_kind,
2376 nullable: info.nullable,
2377 related_name: info.related_name.clone(),
2378 });
2379 }
2380 if info.soft_delete {
2381 if out.soft_delete_column.is_some() {
2382 return Err(syn::Error::new_spanned(
2383 field,
2384 "only one field may be marked `#[rustango(soft_delete)]`",
2385 ));
2386 }
2387 out.soft_delete_column = Some(info.column.clone());
2388 out.soft_delete_field_ident = Some(info.ident.clone());
2389 }
2390 let column = info.column.as_str();
2391 let ident = info.ident;
2392 // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2393 // skip every write path — Postgres recomputes the value
2394 // from EXPR. Push only the column-entry record (so typed
2395 // column constants still exist for filtering / projection)
2396 // and the schema literal (already pushed above) and move
2397 // on. No insert_columns/values, no insert_pushes, no
2398 // bulk_*, no update_assignments, no upsert_update_columns,
2399 // no returning_cols.
2400 if info.generated_as.is_some() {
2401 out.column_entries.push(ColumnEntry {
2402 ident: ident.clone(),
2403 value_ty: info.value_ty.clone(),
2404 name: ident_name(ident),
2405 column: info.column.clone(),
2406 field_type_tokens: info.field_type_tokens,
2407 uuid_column: info.uuid_column,
2408 });
2409 // #1028 — refresh the DB-computed value after insert on
2410 // RETURNING-capable backends. The column joins `returning_cols`
2411 // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2412 // ident is recorded so the `AssignAutoPkPool` impl decodes it
2413 // back into the struct. MySQL has no `INSERT … RETURNING`, so
2414 // it keeps the placeholder until the next read.
2415 out.returning_cols.push(quote!(#column));
2416 out.generated_field_idents
2417 .push((ident.clone(), info.column.clone()));
2418 continue;
2419 }
2420 out.insert_columns.push(quote!(#column));
2421 out.insert_values.push(quote! {
2422 ::core::convert::Into::<#root::core::SqlValue>::into(
2423 ::core::clone::Clone::clone(&self.#ident)
2424 )
2425 });
2426 if info.auto {
2427 out.has_auto = true;
2428 if out.first_auto_ident.is_none() {
2429 out.first_auto_ident = Some(ident.clone());
2430 }
2431 // `default_uuid_v7` (issue #823) generates the PK Rust-side
2432 // before binding, so the value is already in
2433 // `self.#ident` after the insert_push — RETURNING is
2434 // redundant. Skip adding this column to returning_cols /
2435 // auto_assigns / auto_field_idents to avoid (a) an
2436 // unnecessary RETURNING column on every dialect, and (b)
2437 // the MySQL `LAST_INSERT_ID()` path that can only fill an
2438 // integer PK.
2439 // `auto_now_add` / `auto_now` join `default_uuid_v7` here
2440 // for the same reason: they are filled Rust-side below and
2441 // bound, so the value is already in `self` and RETURNING
2442 // would be a redundant column on every dialect.
2443 if !info.default_uuid_v7 && !info.auto_now_add && !info.auto_now {
2444 if out.first_db_auto.is_none() {
2445 out.first_db_auto = auto_inner_type(info.value_ty)
2446 .cloned()
2447 .map(|ty| (ident.clone(), ty));
2448 }
2449 out.returning_cols.push(quote!(#column));
2450 out.auto_field_idents.push((
2451 ident.clone(),
2452 info.column.clone(),
2453 info.fk_pk_kind == DetectedKind::Uuid,
2454 ));
2455 out.auto_assigns.push(quote! {
2456 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2457 });
2458 }
2459 if info.default_uuid_v7 {
2460 // Rust-side UUIDv7 generation (issue #823, Eloquent
2461 // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2462 // and bind; Auto::Set → bind the user's value. The
2463 // column is ALWAYS present in the INSERT statement —
2464 // no RETURNING / no DB DEFAULT needed.
2465 out.insert_pushes.push(quote! {
2466 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2467 self.#ident = #root::sql::Auto::Set(
2468 #root::__uuid::Uuid::now_v7(),
2469 );
2470 }
2471 if let #root::sql::Auto::Set(_v) = &self.#ident {
2472 _columns.push(#column);
2473 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2474 ::core::clone::Clone::clone(_v)
2475 ));
2476 }
2477 });
2478 } else if info.auto_now_add || info.auto_now {
2479 // Rust-side write timestamp (#1464), mirroring
2480 // `default_uuid_v7` above. `auto_now` has always bound
2481 // `Utc::now()` on UPDATE but not on INSERT, so the
2482 // first write of an `updated_at` took the DB default
2483 // while every later one took the clock — the same
2484 // column, filled two different ways.
2485 //
2486 // The default is the problem. `CREATE TABLE IF NOT
2487 // EXISTS` leaves an upgraded table's
2488 // `DEFAULT CURRENT_TIMESTAMP` in place, and SQLite's
2489 // ALTER TABLE grammar (RENAME/ADD/DROP) has no
2490 // statement that changes it — so every insert relying
2491 // on that default wrote the legacy shape forever, while
2492 // the migrate sweep converted the rows around it. The
2493 // column went permanently mixed and `ORDER BY` inverted
2494 // the admin audit log (#1616 rework review,
2495 // correctness-001 / dialects-005 / security-002).
2496 //
2497 // Binding it here makes the stale default unreachable
2498 // rather than trying to migrate it: the timestamp comes
2499 // from the application, not the database.
2500 out.insert_pushes.push(quote! {
2501 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2502 self.#ident = #root::sql::Auto::Set(
2503 #root::__chrono::Utc::now(),
2504 );
2505 }
2506 if let #root::sql::Auto::Set(_v) = &self.#ident {
2507 _columns.push(#column);
2508 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2509 ::core::clone::Clone::clone(_v)
2510 ));
2511 }
2512 });
2513 } else {
2514 out.insert_pushes.push(quote! {
2515 if let #root::sql::Auto::Set(_v) = &self.#ident {
2516 _columns.push(#column);
2517 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2518 ::core::clone::Clone::clone(_v)
2519 ));
2520 }
2521 });
2522 }
2523 // …except the timestamp columns, which must appear in BOTH
2524 // paths (#1464). The path is chosen by the *first* Auto
2525 // field — in practice the PK — so a bulk insert of rows
2526 // with an unset PK took the no-auto branch and dropped the
2527 // timestamp column, falling back to the DB default that
2528 // this change exists to stop depending on.
2529 //
2530 // `rows` is borrowed immutably here, so this fills per row
2531 // at push time rather than writing back into the struct. An
2532 // explicitly-Set value is still honoured; only `Unset`
2533 // takes the clock.
2534 // `default_uuid_v7` likewise: its column has no DB default (#1934).
2535 if info.default_uuid_v7 || info.auto_now_add || info.auto_now {
2536 out.rust_filled_idents.push(ident.clone());
2537 }
2538 if info.auto_now {
2539 out.auto_now_idents.push(ident.clone());
2540 }
2541 if info.default_uuid_v7 {
2542 out.uuid_v7_fields
2543 .push((ident.clone(), info.column.clone()));
2544 }
2545 let rust_fill = if info.default_uuid_v7 {
2546 Some(quote!(#root::__uuid::Uuid::now_v7()))
2547 } else if info.auto_now_add || info.auto_now {
2548 Some(quote!(#root::__chrono::Utc::now()))
2549 } else {
2550 None
2551 };
2552 // Bulk: Auto fields appear in the all-Set path. An unset filled
2553 // field takes its fill there too, not NULL (#1950).
2554 out.bulk_columns_all.push(quote!(#column));
2555 if let Some(fill) = &rust_fill {
2556 let push = quote! {
2557 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2558 match &_row.#ident {
2559 #root::sql::Auto::Set(_v) => ::core::clone::Clone::clone(_v),
2560 #root::sql::Auto::Unset => #fill,
2561 }
2562 ));
2563 };
2564 out.bulk_columns_no_auto.push(quote!(#column));
2565 out.bulk_pushes_no_auto.push(push.clone());
2566 out.bulk_pushes_all.push(push);
2567 } else {
2568 out.bulk_pushes_all.push(quote! {
2569 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2570 ::core::clone::Clone::clone(&_row.#ident)
2571 ));
2572 });
2573 }
2574 if info.default_uuid_v7 {
2575 out.bulk_fill_in_place.push(quote! {
2576 if matches!(&_row.#ident, #root::sql::Auto::Unset) {
2577 _row.#ident = #root::sql::Auto::Set(#root::__uuid::Uuid::now_v7());
2578 }
2579 });
2580 }
2581 // Uniformity check: every row's Auto state must match the
2582 // first row's. Mixed Set/Unset within one bulk_insert is
2583 // rejected here so the column list stays consistent.
2584 // A filled field may be unset on any row, so it is exempt.
2585 let ident_clone = ident.clone();
2586 if rust_fill.is_none() {
2587 out.bulk_auto_uniformity.push(quote! {
2588 for _r in rows.iter().skip(1) {
2589 if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2590 return ::core::result::Result::Err(
2591 #root::sql::ExecError::Sql(
2592 #root::sql::SqlError::BulkAutoMixed
2593 )
2594 );
2595 }
2596 }
2597 });
2598 }
2599 } else {
2600 out.insert_pushes.push(quote! {
2601 _columns.push(#column);
2602 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2603 ::core::clone::Clone::clone(&self.#ident)
2604 ));
2605 });
2606 // Bulk: non-Auto fields appear in BOTH paths.
2607 out.bulk_columns_no_auto.push(quote!(#column));
2608 out.bulk_columns_all.push(quote!(#column));
2609 let push_expr = quote! {
2610 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2611 ::core::clone::Clone::clone(&_row.#ident)
2612 ));
2613 };
2614 out.bulk_pushes_no_auto.push(push_expr.clone());
2615 out.bulk_pushes_all.push(push_expr);
2616 }
2617 if info.primary_key {
2618 if out.primary_key.is_some() {
2619 return Err(syn::Error::new_spanned(
2620 field,
2621 "only one field may be marked `#[rustango(primary_key)]`",
2622 ));
2623 }
2624 out.primary_key = Some((ident.clone(), info.column.clone()));
2625 if info.auto {
2626 out.pk_is_auto = true;
2627 }
2628 } else if info.auto_now_add {
2629 // Immutable post-insert: skip from UPDATE entirely.
2630 } else if info.auto_now {
2631 // `auto_now` columns: bind `chrono::Utc::now()` on every
2632 // UPDATE, and write it back so `self` matches the row.
2633 out.update_assignments.push(quote! {
2634 #root::core::Assignment::new(
2635 #column,
2636 {
2637 let _now = #root::__chrono::Utc::now();
2638 self.#ident = #root::sql::Auto::Set(_now);
2639 ::core::convert::Into::<#root::core::SqlValue>::into(_now)
2640 },
2641 )
2642 });
2643 out.upsert_update_columns.push(quote!(#column));
2644 } else {
2645 out.update_assignments.push(quote! {
2646 #root::core::Assignment::new(
2647 #column,
2648 ::core::convert::Into::<#root::core::SqlValue>::into(
2649 ::core::clone::Clone::clone(&self.#ident)
2650 ),
2651 )
2652 });
2653 out.upsert_update_columns.push(quote!(#column));
2654 }
2655 out.column_entries.push(ColumnEntry {
2656 ident: ident.clone(),
2657 value_ty: info.value_ty.clone(),
2658 name: ident_name(ident),
2659 column: info.column.clone(),
2660 field_type_tokens: info.field_type_tokens,
2661 uuid_column: info.uuid_column,
2662 });
2663 }
2664 Ok(out)
2665}
2666
2667/// A derived index name within PG's 63 bytes (MySQL takes 64); a longer one
2668/// keeps a prefix plus a hash of the whole, so two long names stay distinct.
2669fn capped_index_name(name: String) -> String {
2670 const MAX: usize = 63;
2671 if name.len() <= MAX {
2672 return name;
2673 }
2674 // FNV-1a: stable across builds, unlike `DefaultHasher`.
2675 let hash = name.bytes().fold(0x811c_9dc5_u32, |h, b| {
2676 (h ^ u32::from(b)).wrapping_mul(0x0100_0193)
2677 });
2678 let mut cut = MAX - 9;
2679 while !name.is_char_boundary(cut) {
2680 cut -= 1;
2681 }
2682 format!("{}_{hash:08x}", &name[..cut])
2683}
2684
2685fn model_impl_tokens(
2686 struct_name: &syn::Ident,
2687 model_name: &str,
2688 table: &str,
2689 display: Option<&str>,
2690 app_label: Option<&str>,
2691 admin: Option<&AdminAttrs>,
2692 default_order: &[(String, bool, proc_macro2::Span)],
2693 field_schemas: &[TokenStream2],
2694 soft_delete_column: Option<&str>,
2695 permissions: bool,
2696 audit_track: Option<&[String]>,
2697 m2m_relations: &[M2MAttr],
2698 indexes: &[IndexAttr],
2699 checks: &[CheckAttr],
2700 excludes: &[ExcludeAttr],
2701 composite_fks: &[CompositeFkAttr],
2702 generic_fks: &[GenericFkAttr],
2703 scope: Option<&str>,
2704 is_view: bool,
2705 verbose_name: Option<&str>,
2706 verbose_name_plural: Option<&str>,
2707 managed: bool,
2708 base_manager_name: Option<&str>,
2709 order_with_respect_to: Option<&str>,
2710 proxy: bool,
2711 required_db_features: &[String],
2712 required_db_vendor: Option<&str>,
2713 default_related_name: Option<&str>,
2714 db_table_comment: Option<&str>,
2715 get_latest_by: Option<(&str, bool)>,
2716 extra_permissions: &[(String, String)],
2717 default_permissions: &[String],
2718 global_scopes: &[GlobalScopeAttr],
2719 reverse_has_relations: &[ReverseHasAttr],
2720 generic_has_relations: &[GenericHasAttr],
2721) -> TokenStream2 {
2722 let root = rustango_root();
2723 let display_tokens = if let Some(name) = display {
2724 quote!(::core::option::Option::Some(#name))
2725 } else {
2726 quote!(::core::option::Option::None)
2727 };
2728 let app_label_tokens = if let Some(name) = app_label {
2729 quote!(::core::option::Option::Some(#name))
2730 } else {
2731 quote!(::core::option::Option::None)
2732 };
2733 let soft_delete_tokens = if let Some(col) = soft_delete_column {
2734 quote!(::core::option::Option::Some(#col))
2735 } else {
2736 quote!(::core::option::Option::None)
2737 };
2738 let audit_track_tokens = match audit_track {
2739 None => quote!(::core::option::Option::None),
2740 Some(names) => {
2741 let lits = names.iter().map(|n| n.as_str());
2742 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2743 }
2744 };
2745 let admin_tokens = admin_config_tokens(admin);
2746 // Default `tenant` so single-tenant projects (no `scope` attr
2747 // anywhere) keep the v0.24.x behavior. Container-attr parser
2748 // already validated the value is "registry" or "tenant".
2749 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2750 Some("registry") => quote!(#root::core::ModelScope::Registry),
2751 _ => quote!(#root::core::ModelScope::Tenant),
2752 };
2753 let verbose_name_tokens = optional_str(verbose_name);
2754 let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2755 let base_manager_name_tokens = optional_str(base_manager_name);
2756 let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2757 let required_db_features_lits: Vec<&str> =
2758 required_db_features.iter().map(String::as_str).collect();
2759 let required_db_vendor_tokens = optional_str(required_db_vendor);
2760 let default_related_name_tokens = optional_str(default_related_name);
2761 let db_table_comment_tokens = optional_str(db_table_comment);
2762 let get_latest_by_tokens = match get_latest_by {
2763 Some((col, desc)) => {
2764 quote!(::core::option::Option::Some((#col, #desc)))
2765 }
2766 None => quote!(::core::option::Option::None),
2767 };
2768 let extra_permission_tokens: Vec<_> = extra_permissions
2769 .iter()
2770 .map(|(c, l)| quote!((#c, #l)))
2771 .collect();
2772 let default_permission_tokens: Vec<_> = default_permissions
2773 .iter()
2774 .map(|action| quote!(#action))
2775 .collect();
2776 let indexes_tokens = indexes.iter().map(|idx| {
2777 // When no explicit `name = "..."` was given, derive a stable,
2778 // collision-free name from the table + columns
2779 // (`<table>_<col>_<col>_idx`) instead of a shared literal that
2780 // would clash the moment a model declares two unnamed indexes.
2781 let derived_name = idx
2782 .name
2783 .clone()
2784 .unwrap_or_else(|| capped_index_name(format!("{table}_{}_idx", idx.columns.join("_"))));
2785 let name = derived_name.as_str();
2786 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2787 let unique = idx.unique;
2788 // Map the parsed method string onto the IndexMethod enum
2789 // variant — kept at the codegen layer so the IR doesn't
2790 // carry the string form.
2791 let method_variant = match idx.method.as_str() {
2792 "gin" => quote!(#root::core::IndexMethod::Gin),
2793 "gist" => quote!(#root::core::IndexMethod::Gist),
2794 "brin" => quote!(#root::core::IndexMethod::Brin),
2795 "spgist" => quote!(#root::core::IndexMethod::SpGist),
2796 "hash" => quote!(#root::core::IndexMethod::Hash),
2797 "bloom" => quote!(#root::core::IndexMethod::Bloom),
2798 _ => quote!(#root::core::IndexMethod::BTree),
2799 };
2800 let where_clause = match &idx.where_clause {
2801 Some(s) => quote!(::core::option::Option::Some(#s)),
2802 None => quote!(::core::option::Option::None),
2803 };
2804 let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2805 quote! {{
2806 let mut i = #root::core::IndexSchema::new(#name, &[ #(#cols),* ]);
2807 i.unique = #unique;
2808 i.method = #method_variant;
2809 i.where_clause = #where_clause;
2810 i.include = &[ #(#include_lits),* ];
2811 i
2812 }}
2813 });
2814 let checks_tokens = checks.iter().map(|c| {
2815 let name = c.name.as_str();
2816 let expr = c.expr.as_str();
2817 quote!(#root::core::CheckConstraint::new(#name, #expr))
2818 });
2819 let excludes_tokens = excludes.iter().map(|e| {
2820 let name = e.name.as_str();
2821 let using = e.using.as_str();
2822 let element_tokens = e.elements.iter().map(|(col, op)| {
2823 let col_s = col.as_str();
2824 let op_s = op.as_str();
2825 quote!((#col_s, #op_s))
2826 });
2827 let where_tokens = match e.where_clause.as_deref() {
2828 Some(w) => quote!(::core::option::Option::Some(#w)),
2829 None => quote!(::core::option::Option::None),
2830 };
2831 quote! {{
2832 let mut e = #root::core::ExclusionConstraint::new(
2833 #name,
2834 #using,
2835 &[ #(#element_tokens),* ],
2836 );
2837 e.where_clause = #where_tokens;
2838 e
2839 }}
2840 });
2841 let composite_fk_tokens = composite_fks.iter().map(|rel| {
2842 let name = rel.name.as_str();
2843 let to = rel.to.as_str();
2844 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2845 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2846 quote! {
2847 #root::core::CompositeFkRelation::new(
2848 #name,
2849 #to,
2850 &[ #(#from_cols),* ],
2851 &[ #(#on_cols),* ],
2852 )
2853 }
2854 });
2855 let generic_fk_tokens = generic_fks.iter().map(|rel| {
2856 let name = rel.name.as_str();
2857 let ct_col = rel.ct_column.as_str();
2858 let pk_col = rel.pk_column.as_str();
2859 quote!(#root::core::GenericRelation::new(#name, #ct_col, #pk_col))
2860 });
2861 // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2862 // no `#[rustango(default_order = "...")]` attribute was supplied.
2863 let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2864 let col_lit = col.as_str();
2865 quote! { (#col_lit, #desc) }
2866 });
2867
2868 // Issue #820 — `global_scopes` slice literal. Empty when no
2869 // `#[rustango(global_scope(...))]` attribute was supplied. The
2870 // `apply` path is re-emitted verbatim so it resolves in the
2871 // consumer's scope; the name is stored as a string literal.
2872 let global_scope_tokens = global_scopes.iter().map(|s| {
2873 let name = s.name.as_str();
2874 // The slice sits in a nested `const`, where `Self` does not resolve.
2875 let mut apply = s.apply.clone();
2876 if let Some(first) = apply.segments.first_mut() {
2877 if first.ident == "Self" {
2878 first.ident = struct_name.clone();
2879 }
2880 }
2881 quote!(#root::core::GlobalScope::new(#name, #apply))
2882 });
2883
2884 let m2m_tokens = m2m_relations.iter().map(|rel| {
2885 let name = rel.name.as_str();
2886 let to = rel.to.as_str();
2887 let through = rel.through.as_str();
2888 let src = rel.src.as_str();
2889 let dst = rel.dst.as_str();
2890 let auto_create = rel.auto_create;
2891 quote! {{
2892 let mut m = #root::core::M2MRelation::new(#name, #to, #through, #src, #dst);
2893 m.auto_create = #auto_create;
2894 m
2895 }}
2896 });
2897 // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2898 // when the model declares `#[rustango(reverse_has(...))]`. Each
2899 // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2900 // a const expression. Models without reverse_has fall through to
2901 // the trait's empty default — no override emitted.
2902 let reverse_relations_override = if reverse_has_relations.is_empty() {
2903 quote!()
2904 } else {
2905 let entries = reverse_has_relations.iter().map(|rel| {
2906 let name = rel.name.as_str();
2907 let child = &rel.child;
2908 let child_fk_column = rel.child_fk_column.as_str();
2909 let self_pk_column = rel.self_pk_column();
2910 quote! {
2911 #root::core::ReverseRelation::new(
2912 #name,
2913 <#child as #root::core::Model>::SCHEMA,
2914 #child_fk_column,
2915 #self_pk_column,
2916 )
2917 }
2918 });
2919 quote! {
2920 fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2921 const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2922 RELS
2923 }
2924 }
2925 };
2926 // Issue #830 — `Model::generic_reverse_relations()` override for
2927 // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2928 let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2929 quote!()
2930 } else {
2931 let entries = generic_has_relations.iter().map(|rel| {
2932 let name = rel.name.as_str();
2933 let child = &rel.child;
2934 let ct_column = rel.ct_column.as_str();
2935 let pk_column = rel.pk_column.as_str();
2936 let self_pk_column = rel.self_pk_column();
2937 quote! {
2938 #root::core::GenericReverseRelation::new(
2939 #name,
2940 <#child as #root::core::Model>::SCHEMA,
2941 #ct_column,
2942 #pk_column,
2943 #self_pk_column,
2944 )
2945 }
2946 });
2947 quote! {
2948 fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2949 const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2950 RELS
2951 }
2952 }
2953 };
2954 let fields_slice = const_slice(
2955 "e!(#root::core::FieldSchema),
2956 field_schemas.iter().cloned(),
2957 );
2958 let m2m_slice = const_slice("e!(#root::core::M2MRelation), m2m_tokens);
2959 let indexes_slice = const_slice("e!(#root::core::IndexSchema), indexes_tokens);
2960 let checks_slice = const_slice("e!(#root::core::CheckConstraint), checks_tokens);
2961 let excludes_slice = const_slice("e!(#root::core::ExclusionConstraint), excludes_tokens);
2962 let composite_fk_slice = const_slice(
2963 "e!(#root::core::CompositeFkRelation),
2964 composite_fk_tokens,
2965 );
2966 let generic_fk_slice = const_slice("e!(#root::core::GenericRelation), generic_fk_tokens);
2967 let audited_update_override = if audit_track.is_some() {
2968 quote! {
2969 fn __rustango_audited_update() -> ::core::option::Option<#root::audit::AuditedUpdate> {
2970 ::core::option::Option::Some(Self::__rustango_update_audited)
2971 }
2972 fn __rustango_audited_delete() -> ::core::option::Option<#root::audit::AuditedDelete> {
2973 ::core::option::Option::Some(Self::__rustango_delete_audited)
2974 }
2975 fn __rustango_audited_create() -> ::core::option::Option<#root::audit::AuditedCreate> {
2976 ::core::option::Option::Some(Self::__rustango_create_audited)
2977 }
2978 fn __rustango_audited_update_record(
2979 ) -> ::core::option::Option<#root::audit::AuditedUpdateRecord> {
2980 ::core::option::Option::Some(Self::__rustango_update_record_audited)
2981 }
2982 fn __rustango_audited_cascade() -> ::core::option::Option<#root::audit::AuditedCascade> {
2983 ::core::option::Option::Some(Self::__rustango_cascade_audited)
2984 }
2985 }
2986 } else {
2987 quote!()
2988 };
2989 let global_scope_slice = const_slice("e!(#root::core::GlobalScope), global_scope_tokens);
2990 quote! {
2991 impl #root::core::Model for #struct_name {
2992 const SCHEMA: &'static #root::core::ModelSchema = &{
2993 #root::core::ModelSchema::from_parts(#root::core::ModelSchemaParts {
2994 name: #model_name,
2995 table: #table,
2996 fields: #fields_slice,
2997 display: #display_tokens,
2998 app_label: #app_label_tokens,
2999 admin: #admin_tokens,
3000 soft_delete_column: #soft_delete_tokens,
3001 permissions: #permissions,
3002 audit_track: #audit_track_tokens,
3003 m2m: #m2m_slice,
3004 indexes: #indexes_slice,
3005 check_constraints: #checks_slice,
3006 exclusion_constraints: #excludes_slice,
3007 composite_relations: #composite_fk_slice,
3008 generic_relations: #generic_fk_slice,
3009 scope: #scope_tokens,
3010 default_order: &[ #(#default_order_tokens),* ],
3011 is_view: #is_view,
3012 verbose_name: #verbose_name_tokens,
3013 verbose_name_plural: #verbose_name_plural_tokens,
3014 managed: #managed,
3015 base_manager_name: #base_manager_name_tokens,
3016 order_with_respect_to: #order_with_respect_to_tokens,
3017 proxy: #proxy,
3018 required_db_features: &[ #(#required_db_features_lits),* ],
3019 required_db_vendor: #required_db_vendor_tokens,
3020 default_related_name: #default_related_name_tokens,
3021 db_table_comment: #db_table_comment_tokens,
3022 get_latest_by: #get_latest_by_tokens,
3023 extra_permissions: &[ #(#extra_permission_tokens),* ],
3024 default_permissions: &[ #(#default_permission_tokens),* ],
3025 global_scopes: #global_scope_slice,
3026 })
3027 };
3028
3029 #reverse_relations_override
3030 #generic_reverse_relations_override
3031 #audited_update_override
3032 }
3033 }
3034}
3035
3036/// Emit the `admin: Option<&'static AdminConfig>` field for the model
3037/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
3038/// otherwise a static reference to a populated `AdminConfig`.
3039fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
3040 let root = rustango_root();
3041 let Some(admin) = admin else {
3042 return quote!(::core::option::Option::None);
3043 };
3044
3045 let list_display = admin
3046 .list_display
3047 .as_ref()
3048 .map(|(v, _)| v.as_slice())
3049 .unwrap_or(&[]);
3050 let list_display_lits = list_display.iter().map(|s| s.as_str());
3051
3052 let search_fields = admin
3053 .search_fields
3054 .as_ref()
3055 .map(|(v, _)| v.as_slice())
3056 .unwrap_or(&[]);
3057 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
3058
3059 let readonly_fields = admin
3060 .readonly_fields
3061 .as_ref()
3062 .map(|(v, _)| v.as_slice())
3063 .unwrap_or(&[]);
3064 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
3065
3066 let list_filter = admin
3067 .list_filter
3068 .as_ref()
3069 .map(|(v, _)| v.as_slice())
3070 .unwrap_or(&[]);
3071 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
3072
3073 let actions = admin
3074 .actions
3075 .as_ref()
3076 .map(|(v, _)| v.as_slice())
3077 .unwrap_or(&[]);
3078 let actions_lits = actions.iter().map(|s| s.as_str());
3079
3080 let fieldsets = admin
3081 .fieldsets
3082 .as_ref()
3083 .map(|(v, _)| v.as_slice())
3084 .unwrap_or(&[]);
3085 let fieldset_tokens = const_slice(
3086 "e!(#root::core::Fieldset),
3087 fieldsets.iter().map(|(title, fields)| {
3088 let title = title.as_str();
3089 let field_lits = fields.iter().map(String::as_str);
3090 quote!(#root::core::Fieldset::new(#title, &[ #( #field_lits ),* ]))
3091 }),
3092 );
3093
3094 let list_display_links = admin
3095 .list_display_links
3096 .as_ref()
3097 .map(|(v, _)| v.as_slice())
3098 .unwrap_or(&[]);
3099 let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
3100
3101 let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
3102 let actions_on_top = admin.actions_on_top.unwrap_or(true);
3103 let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
3104 let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
3105
3106 let prepopulated = admin
3107 .prepopulated_fields
3108 .as_ref()
3109 .map(|(v, _)| v.as_slice())
3110 .unwrap_or(&[]);
3111 let prepopulated_tokens = const_slice(
3112 "e!(#root::core::PrepopulatedField),
3113 prepopulated.iter().map(|(target, sources)| {
3114 let target = target.as_str();
3115 let source_lits = sources.iter().map(String::as_str);
3116 quote!(#root::core::PrepopulatedField::new(#target, &[ #( #source_lits ),* ]))
3117 }),
3118 );
3119
3120 let raw_id_fields = admin
3121 .raw_id_fields
3122 .as_ref()
3123 .map(|(v, _)| v.as_slice())
3124 .unwrap_or(&[]);
3125 let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
3126
3127 let autocomplete_fields = admin
3128 .autocomplete_fields
3129 .as_ref()
3130 .map(|(v, _)| v.as_slice())
3131 .unwrap_or(&[]);
3132 let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
3133
3134 // #352 — list_select_related accepts "all" | "none" | "field, field, …".
3135 let list_select_related_tokens = match admin.list_select_related.as_deref() {
3136 None | Some("all") => quote!(#root::core::ListSelectRelated::All),
3137 Some("none") => quote!(#root::core::ListSelectRelated::None),
3138 Some(raw) => {
3139 let names: Vec<&str> = raw
3140 .split(',')
3141 .map(str::trim)
3142 .filter(|s| !s.is_empty())
3143 .collect();
3144 quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
3145 }
3146 };
3147
3148 // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
3149 // a Vec<(String, String)>. Empty / unset → no overrides.
3150 let formfield_pairs: Vec<(&str, &str)> = admin
3151 .formfield_overrides
3152 .as_ref()
3153 .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
3154 .unwrap_or_default();
3155 let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
3156 let field = *field;
3157 let widget = *widget;
3158 quote!((#field, #widget))
3159 });
3160
3161 let list_per_page = admin.list_per_page.unwrap_or(0);
3162
3163 let ordering_pairs = admin
3164 .ordering
3165 .as_ref()
3166 .map(|(v, _)| v.as_slice())
3167 .unwrap_or(&[]);
3168 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
3169 let name = name.as_str();
3170 let desc = *desc;
3171 quote!((#name, #desc))
3172 });
3173
3174 quote! {
3175 ::core::option::Option::Some({
3176 const ADMIN: &#root::core::AdminConfig = &{
3177 #root::core::AdminConfig::from_parts(#root::core::AdminConfigParts {
3178 list_display: &[ #( #list_display_lits ),* ],
3179 search_fields: &[ #( #search_fields_lits ),* ],
3180 list_per_page: #list_per_page,
3181 ordering: &[ #( #ordering_tokens ),* ],
3182 readonly_fields: &[ #( #readonly_fields_lits ),* ],
3183 list_filter: &[ #( #list_filter_lits ),* ],
3184 actions: &[ #( #actions_lits ),* ],
3185 fieldsets: #fieldset_tokens,
3186 list_display_links: &[ #( #list_display_links_lits ),* ],
3187 search_help_text: #search_help_text,
3188 actions_on_top: #actions_on_top,
3189 actions_on_bottom: #actions_on_bottom,
3190 date_hierarchy: #date_hierarchy,
3191 prepopulated_fields: #prepopulated_tokens,
3192 raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
3193 autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
3194 list_select_related: #list_select_related_tokens,
3195 formfield_overrides: &[ #( #formfield_tokens ),* ],
3196 })
3197 };
3198 ADMIN
3199 })
3200 }
3201}
3202
3203fn inherent_impl_tokens(
3204 struct_name: &syn::Ident,
3205 fields: &CollectedFields,
3206 primary_key: Option<&(syn::Ident, String)>,
3207 column_consts: &TokenStream2,
3208 audited_fields: Option<&[&ColumnEntry]>,
3209 upsert_unique: Option<&[String]>,
3210 manager_fns: &[syn::Ident],
3211) -> TokenStream2 {
3212 let root = rustango_root();
3213 // Audit-emit fragments threaded into write paths. Non-empty only
3214 // when the model carries `#[rustango(audit(...))]`. They reborrow
3215 // `_executor` (a `&mut PgConnection` for audited models — the
3216 // macro switches the signature below) so the data write and the
3217 // audit INSERT both run on the same caller-supplied connection.
3218 let executor_passes_to_data_write = if audited_fields.is_some() {
3219 quote!(&mut *_executor)
3220 } else {
3221 quote!(_executor)
3222 };
3223 let executor_param = if audited_fields.is_some() {
3224 quote!(_executor: &mut #root::sql::sqlx::PgConnection)
3225 } else {
3226 quote!(_executor: _E)
3227 };
3228 let executor_generics = if audited_fields.is_some() {
3229 quote!()
3230 } else {
3231 quote!(<'_c, _E>)
3232 };
3233 let executor_where = if audited_fields.is_some() {
3234 quote!()
3235 } else {
3236 quote! {
3237 where
3238 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
3239 }
3240 };
3241 // Audited `_on` methods take `&mut PgConnection`; `&PgPool` is an Executor for the rest.
3242 // One transaction: the before-image's FOR UPDATE must hold through
3243 // the UPDATE and its audit row.
3244 let pool_to_save_on = if audited_fields.is_some() {
3245 // Any failure, COMMIT included, rolls the INSERT back: put the pk and
3246 // stamps back too, or a retry takes the UPDATE path and writes nothing.
3247 let restored: Vec<&syn::Ident> = primary_key
3248 .map(|(pk, _)| pk)
3249 .into_iter()
3250 .chain(&fields.auto_now_idents)
3251 .collect();
3252 let prev: Vec<syn::Ident> = (0..restored.len())
3253 .map(|i| quote::format_ident!("__rustango_save_prev_{}", i))
3254 .collect();
3255 quote! {
3256 #( let #prev = ::core::clone::Clone::clone(&self.#restored); )*
3257 let _saved: ::core::result::Result<u64, #root::sql::ExecError> = async {
3258 let mut _tx = pool.begin().await?;
3259 let _affected = self.save_on(&mut *_tx).await?;
3260 _tx.commit().await?;
3261 ::core::result::Result::Ok(_affected)
3262 }
3263 .await;
3264 if _saved.is_err() {
3265 #( self.#restored = #prev; )*
3266 }
3267 _saved
3268 }
3269 } else {
3270 quote!(self.save_on(pool).await)
3271 };
3272 // One transaction so the write and its audit row commit together.
3273 let pool_to_insert_on = if audited_fields.is_some() {
3274 // A failed COMMIT leaves the RETURNING values on `self`; put them back.
3275 let assigned: Vec<syn::Ident> = fields
3276 .auto_field_idents
3277 .iter()
3278 .map(|(ident, _, _)| ident.clone())
3279 .collect();
3280 restore_auto_now_unless(
3281 &assigned,
3282 "e!(::core::result::Result<(), #root::sql::ExecError>),
3283 "e!(_saved.is_ok()),
3284 "e! {
3285 let mut _tx = pool.begin().await?;
3286 self.insert_on(&mut *_tx).await?;
3287 _tx.commit().await?;
3288 ::core::result::Result::Ok(())
3289 },
3290 )
3291 } else {
3292 quote!(self.insert_on(pool).await)
3293 };
3294 let pool_to_delete_on = if audited_fields.is_some() {
3295 quote! {
3296 let mut _tx = pool.begin().await?;
3297 let _affected = self.delete_on(&mut *_tx).await?;
3298 _tx.commit().await?;
3299 ::core::result::Result::Ok(_affected)
3300 }
3301 } else {
3302 quote!(self.delete_on(pool).await)
3303 };
3304 let pool_to_bulk_insert_on = if audited_fields.is_some() {
3305 quote! {
3306 let mut _tx = pool.begin().await?;
3307 Self::bulk_insert_on(rows, &mut *_tx).await?;
3308 _tx.commit().await?;
3309 ::core::result::Result::Ok(())
3310 }
3311 } else {
3312 quote!(Self::bulk_insert_on(rows, pool).await)
3313 };
3314 // Pre-existing bug surfaced by batch 22's first audited Auto<T>
3315 // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
3316 // directly, but `upsert_on` for audited models takes
3317 // `&mut PgConnection` (the audit emit needs a real connection).
3318 // Add the missing acquire shim to keep audited Auto-PK upsert
3319 // compiling.
3320 let pool_to_upsert_on = if audited_fields.is_some() {
3321 quote! {
3322 let mut _tx = pool.begin().await?;
3323 self.upsert_on(&mut *_tx).await?;
3324 _tx.commit().await?;
3325 ::core::result::Result::Ok(())
3326 }
3327 } else {
3328 quote!(self.upsert_on(pool).await)
3329 };
3330
3331 // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
3332 // (audit-on-connection over &Pool needs a bi-dialect transaction
3333 // helper, deferred). Two body shapes:
3334 // - has_auto: build InsertQuery skipping Auto::Unset columns,
3335 // request Auto cols in `returning`, dispatch via
3336 // `insert_returning_pool`, then on the returned `PgRow` /
3337 // `MySqlAutoId(id)` enum — pull each Auto field from the PG
3338 // row OR drop the single i64 into the first Auto field on MySQL
3339 // (multi-Auto models on MySQL error at runtime since
3340 // `LAST_INSERT_ID()` only reports one)
3341 // - non-Auto: build InsertQuery with explicit columns/values and
3342 // call `insert_pool` (no returning needed)
3343 // pool_insert_method body for the audited Auto-PK case is moved
3344 // to after audit_pair_tokens / audit_pk_to_string (they live
3345 // ~150 lines below). This block keeps the non-audited and
3346 // non-Auto branches in place — the audited Auto-PK arm is
3347 // computed below and merged via the dispatch helper variable.
3348 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
3349 // Audited models with explicit (non-Auto) PKs go through
3350 // the non-Auto insert path below — the audit emit is one
3351 // round-trip after the INSERT inside the same tx via
3352 // audit::save_one_with_audit? No, INSERT semantics
3353 // differ. For non-Auto PK + audited, route through a
3354 // dedicated insert + audit emit on the same tx, but defer
3355 // the macro emission to the audit-bundle-aware block below
3356 // — this `quote!()` placeholder gets overwritten there.
3357 quote!()
3358 } else if audited_fields.is_some() && fields.has_auto {
3359 // Audited Auto-PK insert_pool — assembled after the audit
3360 // bundles. Placeholder; real emission below.
3361 quote!()
3362 } else if fields.has_auto {
3363 let pushes = &fields.insert_pushes;
3364 let returning_cols = &fields.returning_cols;
3365 // A row not inserted was never saved, so a Rust-filled value must not stay (#1937).
3366 let filled = &fields.rust_filled_idents;
3367 let was_unset: Vec<syn::Ident> = filled
3368 .iter()
3369 .map(|i| quote::format_ident!("__{}_was_unset", ident_name(i)))
3370 .collect();
3371 let insert_or_ignore_call = quote! {
3372 #( let #was_unset = matches!(&self.#filled, #root::sql::Auto::Unset); )*
3373 let mut _columns: ::std::vec::Vec<&'static str> =
3374 ::std::vec::Vec::new();
3375 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3376 ::std::vec::Vec::new();
3377 #( #pushes )*
3378 let _query = #root::core::InsertQuery::new(
3379 <Self as #root::core::Model>::SCHEMA,
3380 _columns,
3381 _values,
3382 );
3383 let _result = #root::sql::insert_or_ignore(pool, &_query).await;
3384 if !matches!(_result, ::core::result::Result::Ok(true)) {
3385 #( if #was_unset { self.#filled = #root::sql::Auto::Unset; } )*
3386 }
3387 _result
3388 };
3389 // When every `Auto<T>` field is filled Rust-side
3390 // (`default_uuid_v7`, issue #823), there is no column to read
3391 // back from the database — `returning_cols` is empty. Route
3392 // through plain `insert_pool` instead of
3393 // `insert_returning_pool` to skip the redundant RETURNING /
3394 // LAST_INSERT_ID round-trip.
3395 if fields.returning_cols.is_empty() {
3396 quote! {
3397 /// Insert this row against either backend. Every
3398 /// `Auto<T>` PK on this model is filled Rust-side
3399 /// (e.g. `default_uuid_v7`) before binding, so no
3400 /// RETURNING round-trip is needed.
3401 ///
3402 /// # Errors
3403 /// As [`Self::insert`].
3404 pub async fn insert_pool(
3405 &mut self,
3406 pool: &#root::sql::Pool,
3407 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3408 let mut _columns: ::std::vec::Vec<&'static str> =
3409 ::std::vec::Vec::new();
3410 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3411 ::std::vec::Vec::new();
3412 #( #pushes )*
3413 let _query = #root::core::InsertQuery::new(
3414 <Self as #root::core::Model>::SCHEMA,
3415 _columns,
3416 _values,
3417 );
3418 #root::sql::insert_pool(pool, &_query).await
3419 }
3420
3421 /// Eloquent `Model::insertOrIgnore()` — same shape
3422 /// as the auto-PK branch above. Returns `Ok(true)`
3423 /// when inserted, `Ok(false)` when a conflict caused
3424 /// the INSERT to silently skip.
3425 ///
3426 /// # Errors
3427 /// As [`Self::insert`].
3428 pub async fn insert_or_ignore(
3429 &mut self,
3430 pool: &#root::sql::Pool,
3431 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3432 #insert_or_ignore_call
3433 }
3434 }
3435 } else {
3436 quote! {
3437 /// Insert this row against either backend, populating any
3438 /// `Auto<T>` PK from the auto-assigned value.
3439 ///
3440 /// # Errors
3441 /// As [`Self::insert`].
3442 pub async fn insert_pool(
3443 &mut self,
3444 pool: &#root::sql::Pool,
3445 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3446 let mut _columns: ::std::vec::Vec<&'static str> =
3447 ::std::vec::Vec::new();
3448 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3449 ::std::vec::Vec::new();
3450 #( #pushes )*
3451 let _query = #root::core::InsertQuery::new(
3452 <Self as #root::core::Model>::SCHEMA,
3453 _columns,
3454 _values,
3455 )
3456 .returning(::std::vec![ #( #returning_cols ),* ]);
3457 let _result = #root::sql::insert_returning_pool(
3458 pool, &_query,
3459 ).await?;
3460 #root::sql::apply_auto_pk(_result, self)
3461 }
3462
3463 /// Eloquent `Model::insertOrIgnore()` — INSERT this
3464 /// row or silently skip on unique-constraint
3465 /// violation. Returns `Ok(true)` when a row was
3466 /// inserted, `Ok(false)` when a conflict caused the
3467 /// INSERT to silently skip.
3468 ///
3469 /// **Caveat on auto-PK models**: when the row is
3470 /// skipped (conflict), this instance's `Auto<T>`
3471 /// fields stay `Unset` — no PK is back-populated
3472 /// because the server didn't auto-assign one. For
3473 /// "insert then read back the PK or the existing
3474 /// row's PK", use the `upsert` family or
3475 /// `get_or_create`.
3476 ///
3477 /// # Errors
3478 /// As [`Self::insert`].
3479 pub async fn insert_or_ignore(
3480 &mut self,
3481 pool: &#root::sql::Pool,
3482 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3483 #insert_or_ignore_call
3484 }
3485 }
3486 }
3487 } else {
3488 let insert_columns = &fields.insert_columns;
3489 let insert_values = &fields.insert_values;
3490 quote! {
3491 /// Insert this row into its table against either backend.
3492 /// Equivalent to [`Self::insert`] but takes
3493 /// [`#root::sql::Pool`].
3494 ///
3495 /// # Errors
3496 /// As [`Self::insert`].
3497 pub async fn insert_pool(
3498 &self,
3499 pool: &#root::sql::Pool,
3500 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3501 let _query = #root::core::InsertQuery::new(
3502 <Self as #root::core::Model>::SCHEMA,
3503 ::std::vec![ #( #insert_columns ),* ],
3504 ::std::vec![ #( #insert_values ),* ],
3505 );
3506 #root::sql::insert_pool(pool, &_query).await
3507 }
3508
3509 /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3510 /// or silently skip on unique-constraint violation. Maps
3511 /// to per-dialect "INSERT ... DO NOTHING on conflict":
3512 /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3513 /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL an
3514 /// `ON DUPLICATE KEY UPDATE` that leaves the row as is.
3515 ///
3516 /// Returns `Ok(true)` when a row was inserted,
3517 /// `Ok(false)` when a conflict caused the INSERT to
3518 /// silently skip.
3519 ///
3520 /// Use for "create-if-absent" patterns where you don't
3521 /// need the row back. For "find-or-create with the row
3522 /// returned", use the queryset-level
3523 /// `crate::sql::get_or_create` free function.
3524 ///
3525 /// # Errors
3526 /// As [`Self::insert`], plus any dialect-specific
3527 /// translation error from the ConflictClause writer.
3528 pub async fn insert_or_ignore(
3529 &self,
3530 pool: &#root::sql::Pool,
3531 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3532 let _query = #root::core::InsertQuery::new(
3533 <Self as #root::core::Model>::SCHEMA,
3534 ::std::vec![ #( #insert_columns ),* ],
3535 ::std::vec![ #( #insert_values ),* ],
3536 );
3537 #root::sql::insert_or_ignore(pool, &_query).await
3538 }
3539 }
3540 };
3541
3542 // The tracked (column, JSON value) pairs, spliced only into
3543 // `__rustango_audit_pairs`. Empty when the model isn't audited.
3544 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3545 .map(|tracked| {
3546 tracked
3547 .iter()
3548 .map(|c| {
3549 let column_lit = c.column.as_str();
3550 let ident = &c.ident;
3551 quote! {
3552 (
3553 #column_lit,
3554 #root::core::__json_value(&self.#ident),
3555 )
3556 }
3557 })
3558 .collect()
3559 })
3560 .unwrap_or_default();
3561 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3562 if fields.pk_is_auto {
3563 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3564 } else {
3565 quote!(::std::format!("{}", &self.#pk_ident))
3566 }
3567 } else {
3568 quote!(::std::string::String::new())
3569 };
3570 // `__rustango_audit_entry`: the one place a snapshot `PendingEntry`
3571 // is built, so every audited write path records the same shape.
3572 let delete_audited_body = if primary_key.is_some() {
3573 quote! {
3574 ::std::boxed::Box::pin(#root::audit::delete_many_with_audit::<Self>(
3575 pool,
3576 query,
3577 |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3578 ))
3579 }
3580 } else {
3581 quote! {
3582 let _ = (pool, query);
3583 ::std::boxed::Box::pin(async {
3584 ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3585 table: <Self as #root::core::Model>::SCHEMA.table,
3586 })
3587 })
3588 }
3589 };
3590 let cascade_audited_body = if primary_key.is_some() {
3591 quote! {
3592 ::std::boxed::Box::pin(#root::audit::cascade_rows::<Self>(
3593 tx,
3594 <Self as #root::core::Model>::SCHEMA,
3595 rows,
3596 |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3597 ))
3598 }
3599 } else {
3600 quote! {
3601 let _ = (tx, rows);
3602 ::std::boxed::Box::pin(async {
3603 ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3604 table: <Self as #root::core::Model>::SCHEMA.table,
3605 })
3606 })
3607 }
3608 };
3609 let update_audited_body = if primary_key.is_some() {
3610 quote! {
3611 ::std::boxed::Box::pin(#root::audit::update_many_with_audit::<Self>(
3612 pool,
3613 query,
3614 move |_r: &Self| _r.__rustango_audit_entry(op),
3615 ))
3616 }
3617 } else {
3618 quote! {
3619 let _ = (pool, query, op);
3620 ::std::boxed::Box::pin(async {
3621 ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3622 table: <Self as #root::core::Model>::SCHEMA.table,
3623 })
3624 })
3625 }
3626 };
3627 let audit_entry_method = if audited_fields.is_some() {
3628 let pairs = audit_pair_tokens.iter();
3629 let pk_str = audit_pk_to_string.clone();
3630 quote! {
3631 /// This row's tracked `(column, value)` pairs.
3632 #[doc(hidden)]
3633 pub fn __rustango_audit_pairs(
3634 &self,
3635 ) -> ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> {
3636 ::std::vec![ #( #pairs ),* ]
3637 }
3638
3639 /// This row's audit PK.
3640 #[doc(hidden)]
3641 pub fn __rustango_audit_pk(&self) -> ::std::string::String {
3642 #pk_str
3643 }
3644
3645 /// Snapshot audit entry for `operation` on this row.
3646 #[doc(hidden)]
3647 pub fn __rustango_audit_entry(
3648 &self,
3649 operation: #root::audit::AuditOp,
3650 ) -> #root::audit::PendingEntry {
3651 #root::audit::PendingEntry {
3652 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3653 entity_pk: self.__rustango_audit_pk(),
3654 operation,
3655 source: #root::audit::current_source(),
3656 changes: #root::audit::snapshot_changes(&self.__rustango_audit_pairs()),
3657 }
3658 }
3659
3660 /// Audited bulk `UPDATE`, behind `Model::__rustango_audited_update`.
3661 #[doc(hidden)]
3662 pub fn __rustango_update_audited<'a>(
3663 pool: &'a #root::sql::Pool,
3664 query: &'a #root::core::UpdateQuery,
3665 op: #root::audit::AuditOp,
3666 ) -> ::std::pin::Pin<::std::boxed::Box<
3667 dyn ::core::future::Future<
3668 Output = ::core::result::Result<u64, #root::sql::ExecError>,
3669 > + ::core::marker::Send + 'a,
3670 >> {
3671 #update_audited_body
3672 }
3673
3674 /// Audited `create` entry for a new row, behind `Model::__rustango_audited_create`.
3675 #[doc(hidden)]
3676 pub fn __rustango_create_audited<'a, 't>(
3677 tx: &'a mut #root::sql::PoolTx<'t>,
3678 pk: #root::core::SqlValue,
3679 ) -> ::std::pin::Pin<::std::boxed::Box<
3680 dyn ::core::future::Future<
3681 Output = ::core::result::Result<(), #root::sql::ExecError>,
3682 > + ::core::marker::Send + 'a,
3683 >> {
3684 ::std::boxed::Box::pin(#root::audit::record_create::<Self>(
3685 tx,
3686 <Self as #root::core::Model>::SCHEMA,
3687 pk,
3688 |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Create),
3689 ))
3690 }
3691
3692 /// Audited one-row `update` entry, behind `Model::__rustango_audited_update_record`.
3693 #[doc(hidden)]
3694 pub fn __rustango_update_record_audited<'a, 't>(
3695 tx: &'a mut #root::sql::PoolTx<'t>,
3696 pk: #root::core::SqlValue,
3697 ) -> ::std::pin::Pin<::std::boxed::Box<
3698 dyn ::core::future::Future<
3699 Output = ::core::result::Result<(), #root::sql::ExecError>,
3700 > + ::core::marker::Send + 'a,
3701 >> {
3702 ::std::boxed::Box::pin(#root::audit::record_update::<Self>(
3703 tx,
3704 <Self as #root::core::Model>::SCHEMA,
3705 pk,
3706 |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Update),
3707 ))
3708 }
3709
3710 /// Rows an FK cascade will delete, behind `Model::__rustango_audited_cascade`.
3711 #[doc(hidden)]
3712 pub fn __rustango_cascade_audited<'a, 't>(
3713 tx: &'a mut #root::sql::PoolTx<'t>,
3714 rows: #root::core::WhereExpr,
3715 ) -> ::std::pin::Pin<::std::boxed::Box<
3716 dyn ::core::future::Future<
3717 Output = ::core::result::Result<
3718 ::std::vec::Vec<(#root::core::SqlValue, #root::audit::PendingEntry)>,
3719 #root::sql::ExecError,
3720 >,
3721 > + ::core::marker::Send + 'a,
3722 >> {
3723 #cascade_audited_body
3724 }
3725
3726 /// Audited bulk `DELETE`, behind `Model::__rustango_audited_delete`.
3727 #[doc(hidden)]
3728 pub fn __rustango_delete_audited<'a>(
3729 pool: &'a #root::sql::Pool,
3730 query: &'a #root::core::DeleteQuery,
3731 ) -> ::std::pin::Pin<::std::boxed::Box<
3732 dyn ::core::future::Future<
3733 Output = ::core::result::Result<u64, #root::sql::ExecError>,
3734 > + ::core::marker::Send + 'a,
3735 >> {
3736 #delete_audited_body
3737 }
3738 }
3739 } else {
3740 quote!()
3741 };
3742 // Builds `_audit_entry`; `written` is the soft-delete column's new
3743 // value, recorded in place of the stale `self` field.
3744 let make_entry = |op_path: TokenStream2, written: Option<TokenStream2>| -> TokenStream2 {
3745 if let (Some(v), Some(col)) = (written, fields.soft_delete_column.as_deref()) {
3746 quote! {
3747 let mut _audit_entry = self.__rustango_audit_entry(#op_path);
3748 _audit_entry.set_tracked(#col, #v);
3749 }
3750 } else {
3751 quote!(let _audit_entry = self.__rustango_audit_entry(#op_path);)
3752 }
3753 };
3754 let soft_deleted_json = quote!(#root::__serde_json::to_value(&_now)
3755 .unwrap_or(#root::__serde_json::Value::Null));
3756 let restored_json = quote!(#root::__serde_json::Value::Null);
3757 // `_on` emit; `affected` paths skip the row when nothing changed.
3758 let make_op_emit =
3759 |op_path: TokenStream2, written: Option<TokenStream2>, affected: bool| -> TokenStream2 {
3760 if audited_fields.is_none() {
3761 return quote!();
3762 }
3763 let entry = make_entry(op_path, written);
3764 let emit = quote! {
3765 #entry
3766 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3767 };
3768 if affected {
3769 quote!(if _affected > 0 { #emit })
3770 } else {
3771 emit
3772 }
3773 };
3774 let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create), None, false);
3775 let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete), None, true);
3776 let audit_softdelete_emit = make_op_emit(
3777 quote!(#root::audit::AuditOp::SoftDelete),
3778 Some(soft_deleted_json.clone()),
3779 true,
3780 );
3781 let audit_restore_emit = make_op_emit(
3782 quote!(#root::audit::AuditOp::Restore),
3783 Some(restored_json.clone()),
3784 true,
3785 );
3786 // `&Pool` update runner for soft_delete / restore: one tx with the
3787 // audit row when audited, a plain UPDATE otherwise.
3788 let make_pool_update = |op_path: TokenStream2, written: TokenStream2| -> TokenStream2 {
3789 if audited_fields.is_some() {
3790 let entry = make_entry(op_path, Some(written));
3791 quote! {
3792 #entry
3793 #root::audit::save_one_with_audit(pool, &_query, &_audit_entry).await
3794 }
3795 } else {
3796 quote!(#root::sql::update_pool(pool, &_query).await)
3797 }
3798 };
3799 let pool_softdelete_run =
3800 make_pool_update(quote!(#root::audit::AuditOp::SoftDelete), soft_deleted_json);
3801 let pool_restore_run = make_pool_update(quote!(#root::audit::AuditOp::Restore), restored_json);
3802
3803 // `save_pool(&Pool)` — emitted for every model with a PK. The
3804 // audited `save_pool` is built below on the diff path; both share
3805 // `save_partial_methods`, which differ only in how they run `_query`.
3806 let save_ret = quote!(::core::result::Result<u64, #root::sql::ExecError>);
3807 let save_keep = quote!(matches!(_saved, ::core::result::Result::Ok(n) if n > 0));
3808 let save_partial_methods = |pk_ident: &syn::Ident,
3809 pk_column_lit: &str,
3810 run: TokenStream2|
3811 -> TokenStream2 {
3812 let assignments = &fields.update_assignments;
3813 // Pushed in step with `update_assignments`: one column each.
3814 let assignment_cols = &fields.upsert_update_columns;
3815 let write = restore_auto_now_unless(
3816 &fields.auto_now_idents,
3817 &save_ret,
3818 &save_keep,
3819 "e! {
3820 // Build only the wanted assignments, so an unnamed
3821 // `auto_now` field is not stamped.
3822 let mut _filtered: ::std::vec::Vec<#root::core::Assignment> =
3823 ::std::vec::Vec::with_capacity(_wanted_cols.len());
3824 #(
3825 if _wanted_cols.contains(#assignment_cols) {
3826 _filtered.push(#assignments);
3827 }
3828 )*
3829 if _filtered.is_empty() {
3830 // All field names valid, but they all map to
3831 // non-assignable slots (PK column, computed/
3832 // virtual fields, relations without an
3833 // assignment). Same no-op as an empty list.
3834 #root::__tracing::warn!(
3835 target: "rustango::save_partial",
3836 model = _schema.name,
3837 "save_partial: every named field maps to a non-assignable column — no-op"
3838 );
3839 return ::core::result::Result::Ok(0);
3840 }
3841 let _query = #root::core::UpdateQuery::new(
3842 _schema,
3843 _filtered,
3844 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3845 #pk_column_lit,
3846 #root::core::Op::Eq,
3847 ::core::convert::Into::<#root::core::SqlValue>::into(
3848 ::core::clone::Clone::clone(&self.#pk_ident)
3849 ),
3850 )),
3851 );
3852 #run
3853 },
3854 );
3855 quote! {
3856 /// Save (UPDATE) only the listed Rust-side fields,
3857 /// leaving every other column untouched. Issue #66.
3858 ///
3859 /// `fields` are Rust-side struct field names; the macro
3860 /// resolves each to its SQL column. Unknown field
3861 /// names return [`#root::core::QueryError::UnknownField`]
3862 /// wrapped in `ExecError::Query`. An empty list is a
3863 /// no-op (returns `Ok(())` and logs a `tracing::warn!`).
3864 /// On an audited model the audit diff covers only these fields.
3865 ///
3866 /// Use this when:
3867 /// * you only mutated a couple of fields on a wide row
3868 /// (avoid re-writing every column on every save), or
3869 /// * two writers diverged after their initial read and
3870 /// you want to preserve the other writer's changes to
3871 /// columns you didn't touch.
3872 ///
3873 /// Auto-PK models with an unset PK return
3874 /// [`#root::core::QueryError::UnknownField`] with
3875 /// field name `<pk>` — `save_partial` is an
3876 /// UPDATE-only path. Call [`Self::insert_pool`]
3877 /// (or [`Self::save_pool`] which dispatches based on
3878 /// PK state) for the INSERT case.
3879 ///
3880 /// # Errors
3881 /// As [`Self::save_pool`], plus `UnknownField` for
3882 /// unknown / empty / Auto-Unset cases.
3883 pub async fn save_partial(
3884 &mut self,
3885 fields: &[&str],
3886 pool: &#root::sql::Pool,
3887 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3888 if fields.is_empty() {
3889 #root::__tracing::warn!(
3890 target: "rustango::save_partial",
3891 model = <Self as #root::core::Model>::SCHEMA.name,
3892 "save_partial called with empty field list — no-op"
3893 );
3894 return ::core::result::Result::Ok(0);
3895 }
3896 let _schema = <Self as #root::core::Model>::SCHEMA;
3897 // Validate field names against the schema.
3898 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3899 ::std::collections::HashSet::with_capacity(fields.len());
3900 for f in fields {
3901 match _schema.field(f) {
3902 ::core::option::Option::Some(fs) => {
3903 _wanted_cols.insert(fs.column);
3904 }
3905 ::core::option::Option::None => {
3906 return ::core::result::Result::Err(
3907 #root::sql::ExecError::Query(
3908 #root::core::QueryError::UnknownField {
3909 model: _schema.name,
3910 field: (*f).to_owned(),
3911 }
3912 )
3913 );
3914 }
3915 }
3916 }
3917 #write
3918 }
3919
3920 /// Typed-column counterpart of [`Self::save_partial`] —
3921 /// issue #67. `fields` is a tuple of [`Column`]
3922 /// constants whose `Model` matches `Self`; typos and
3923 /// model mismatches surface at *compile time*
3924 /// (`Author::name` inside a `Post::save_partial_typed`
3925 /// call is a type error, no runtime check).
3926 ///
3927 /// ```ignore
3928 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3929 /// ```
3930 ///
3931 /// Lowers to [`Self::save_partial`] under the hood — the
3932 /// tuple is reduced to a `&[&str]` slice of Rust-side
3933 /// field names and forwarded.
3934 ///
3935 /// [`Column`]: #root::core::Column
3936 ///
3937 /// # Errors
3938 /// As [`Self::save_partial`].
3939 pub async fn save_partial_typed<
3940 L: #root::core::TypedFieldList<Self>,
3941 >(
3942 &mut self,
3943 fields: L,
3944 pool: &#root::sql::Pool,
3945 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3946 let _names = fields.rust_field_names();
3947 let _refs: ::std::vec::Vec<&str> =
3948 _names.iter().copied().collect();
3949 self.save_partial(&_refs, pool).await
3950 }
3951 }
3952 };
3953 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3954 let pk_column_lit = pk_col.as_str();
3955 let assignments = &fields.update_assignments;
3956 if audited_fields.is_some() {
3957 quote!()
3958 } else {
3959 let partial = save_partial_methods(
3960 pk_ident,
3961 pk_column_lit,
3962 quote!(#root::sql::update_pool(pool, &_query).await),
3963 );
3964 let dispatch_unset = if fields.pk_is_auto {
3965 quote! {
3966 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3967 return self.insert_pool(pool).await.map(|()| 1u64);
3968 }
3969 }
3970 } else {
3971 quote!()
3972 };
3973 let write = restore_auto_now_unless(
3974 &fields.auto_now_idents,
3975 &save_ret,
3976 &save_keep,
3977 "e! {
3978 let _query = #root::core::UpdateQuery::new(
3979 <Self as #root::core::Model>::SCHEMA,
3980 ::std::vec![ #( #assignments ),* ],
3981 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3982 #pk_column_lit,
3983 #root::core::Op::Eq,
3984 ::core::convert::Into::<#root::core::SqlValue>::into(
3985 ::core::clone::Clone::clone(&self.#pk_ident)
3986 ),
3987 )),
3988 );
3989 let _affected = #root::sql::update_pool(pool, &_query).await?;
3990 ::core::result::Result::Ok(_affected)
3991 },
3992 );
3993 quote! {
3994 /// Save this row to its table against either backend.
3995 /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3996 /// `UPDATE` keyed on the PK; global scopes do not apply.
3997 ///
3998 /// # Errors
3999 /// As [`Self::save`].
4000 pub async fn save_pool(
4001 &mut self,
4002 pool: &#root::sql::Pool,
4003 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4004 #dispatch_unset
4005 #write
4006 }
4007
4008 #partial
4009 }
4010 }
4011 } else {
4012 quote!()
4013 };
4014
4015 // Audited `insert_pool` (overrides the placeholder set higher up
4016 // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
4017 // audited models get insert_pool routing through
4018 // audit::insert_one_with_audit (per-backend tx wraps INSERT
4019 // + auto-PK readback + audit emit). Snapshot-style audit (the
4020 // PendingEntry's `changes` carries post-write field values).
4021 let pool_insert_method = if audited_fields.is_some() {
4022 if let Some(_) = primary_key {
4023 let pushes = if fields.has_auto {
4024 fields.insert_pushes.clone()
4025 } else {
4026 // For non-Auto-PK models, the macro normally builds
4027 // {columns, values} from fields.insert_columns +
4028 // fields.insert_values rather than insert_pushes.
4029 // Map those into the pushes shape.
4030 fields
4031 .insert_columns
4032 .iter()
4033 .zip(&fields.insert_values)
4034 .map(|(col, val)| {
4035 quote! {
4036 _columns.push(#col);
4037 _values.push(#val);
4038 }
4039 })
4040 .collect()
4041 };
4042 // Rust-filled Auto PKs (`default_uuid_v7`) leave the list empty (#1934).
4043 let returning_cols: Vec<proc_macro2::TokenStream> = if !fields.returning_cols.is_empty()
4044 {
4045 fields.returning_cols.clone()
4046 } else {
4047 // Non-Auto-PK: still need RETURNING something for the
4048 // audit helper's contract (it errors on empty
4049 // returning). Return the PK column so the audit row
4050 // can carry the assigned PK back. Some non-Auto PKs
4051 // are server-side-default (e.g. UUIDv4 default), so
4052 // RETURNING is genuinely useful.
4053 primary_key
4054 .map(|(_, col)| {
4055 let lit = col.as_str();
4056 vec![quote!(#lit)]
4057 })
4058 .unwrap_or_default()
4059 };
4060 quote! {
4061 /// Insert this row against either backend with audit
4062 /// emission inside the same transaction. Bi-dialect
4063 /// counterpart of [`Self::insert`] for audited models.
4064 ///
4065 /// Snapshot-style audit (post-write field values).
4066 ///
4067 /// # Errors
4068 /// As [`Self::insert`].
4069 pub async fn insert_pool(
4070 &mut self,
4071 pool: &#root::sql::Pool,
4072 ) -> ::core::result::Result<(), #root::sql::ExecError> {
4073 let mut _columns: ::std::vec::Vec<&'static str> =
4074 ::std::vec::Vec::new();
4075 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
4076 ::std::vec::Vec::new();
4077 #( #pushes )*
4078 let _query = #root::core::InsertQuery::new(
4079 <Self as #root::core::Model>::SCHEMA,
4080 _columns,
4081 _values,
4082 )
4083 .returning(::std::vec![ #( #returning_cols ),* ]);
4084 #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self| {
4085 _m.__rustango_audit_entry(#root::audit::AuditOp::Create)
4086 })
4087 .await
4088 }
4089 }
4090 } else {
4091 quote!()
4092 }
4093 } else {
4094 // Keep the non-audited pool_insert_method we built earlier.
4095 pool_insert_method
4096 };
4097
4098 // Update audited save_pool: now that insert_pool is wired for
4099 // audited Auto-PK models, save_pool can dispatch Auto::Unset →
4100 // insert_pool. Non-audited save_pool already does this.
4101 // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
4102 // Replaces the snapshot-only emission with a per-backend transaction
4103 // body that:
4104 // 1. SELECTs the tracked columns by PK (typed Row::try_get per
4105 // column), capturing BEFORE values
4106 // 2. compiles the UPDATE via pool.dialect() and runs it on the tx
4107 // 3. builds AFTER pairs from &self
4108 // 4. diffs BEFORE/AFTER, emits one PendingEntry with
4109 // AuditOp::Update + diff_changes(...) on the same tx connection
4110 // 5. commits
4111 //
4112 // Per-backend arms inline the SQL string + placeholder shape, then
4113 // share the `audit_before_pair_tokens` decoder block (Row::try_get
4114 // is polymorphic over Row type — the same tokens work against
4115 // PgRow and MySqlRow as long as the field's Rust type implements
4116 // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
4117 // primitives + chrono/uuid/serde_json::Value all do).
4118 let pool_save_method = if let Some(tracked) = audited_fields {
4119 if let Some((pk_ident, pk_col)) = primary_key {
4120 let pk_column_lit = pk_col.as_str();
4121 let partial = save_partial_methods(
4122 pk_ident,
4123 pk_column_lit,
4124 quote! {
4125 self.__rustango_save_with_diff(
4126 pool,
4127 &_query,
4128 ::core::option::Option::Some(&_wanted_cols),
4129 )
4130 .await
4131 },
4132 );
4133 // Per-tracked-column BEFORE-pair token list. Each entry
4134 // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
4135 // The Row alias resolves to PgRow / MySqlRow per call site,
4136 // so the same template generates both the PG and MySQL bodies.
4137 let mk_before_pairs =
4138 |getter: &dyn Fn(&ColumnEntry) -> TokenStream2| -> Vec<TokenStream2> {
4139 tracked
4140 .iter()
4141 .map(|c| {
4142 let getter = getter(c);
4143 let column_lit = c.column.as_str();
4144 let value_ty = &c.value_ty;
4145 quote! {
4146 (
4147 #column_lit,
4148 match #getter::<#value_ty>(
4149 _audit_before_row, #column_lit,
4150 ) {
4151 ::core::result::Result::Ok(v) => {
4152 #root::core::__json_value(&v)
4153 }
4154 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
4155 },
4156 )
4157 }
4158 })
4159 .collect()
4160 };
4161 let before_pairs_pg = mk_before_pairs(&|_| quote!(#root::sql::try_get_returning));
4162 let before_pairs_my = mk_before_pairs(&|c| {
4163 if c.uuid_column {
4164 quote!(#root::sql::try_get_flat_my)
4165 } else {
4166 quote!(#root::sql::try_get_returning_my)
4167 }
4168 });
4169 let before_pairs_sqlite =
4170 mk_before_pairs(&|_| quote!(#root::sql::try_get_returning_sqlite));
4171 let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
4172 let pk_value_for_bind = if fields.pk_is_auto {
4173 quote!(self.#pk_ident.get().copied().unwrap_or_default())
4174 } else {
4175 quote!(::core::clone::Clone::clone(&self.#pk_ident))
4176 };
4177 let assignments = &fields.update_assignments;
4178 let unset_dispatch = if fields.has_auto {
4179 quote! {
4180 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
4181 return self.insert_pool(pool).await.map(|()| 1u64);
4182 }
4183 }
4184 } else {
4185 quote!()
4186 };
4187 let write = restore_auto_now_unless(
4188 &fields.auto_now_idents,
4189 &save_ret,
4190 &save_keep,
4191 "e! {
4192 let _query = #root::core::UpdateQuery::new(
4193 <Self as #root::core::Model>::SCHEMA,
4194 ::std::vec![ #( #assignments ),* ],
4195 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
4196 #pk_column_lit,
4197 #root::core::Op::Eq,
4198 ::core::convert::Into::<#root::core::SqlValue>::into(
4199 ::core::clone::Clone::clone(&self.#pk_ident)
4200 ),
4201 )),
4202 );
4203 self.__rustango_save_with_diff(pool, &_query, ::core::option::Option::None)
4204 .await
4205 },
4206 );
4207 quote! {
4208 /// Save this row against either backend with audit
4209 /// emission (diff-style: BEFORE+AFTER) inside the
4210 /// same transaction. Auto::Unset PK routes to
4211 /// insert_pool. Bi-dialect counterpart of
4212 /// [`Self::save`] for audited models.
4213 ///
4214 /// The audit row's `changes` JSON contains one
4215 /// `{ "field": { "before": …, "after": … } }` entry
4216 /// per tracked column whose value actually changed
4217 /// — same shape as the existing &PgPool save() emits.
4218 /// Keyed on the PK; global scopes do not apply.
4219 ///
4220 /// # Errors
4221 /// As [`Self::save`].
4222 pub async fn save_pool(
4223 &mut self,
4224 pool: &#root::sql::Pool,
4225 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4226 #unset_dispatch
4227 #write
4228 }
4229
4230 /// Run `query` and write its diff entry, limited to `only` columns.
4231 #[doc(hidden)]
4232 async fn __rustango_save_with_diff(
4233 &self,
4234 pool: &#root::sql::Pool,
4235 query: &#root::core::UpdateQuery,
4236 only: ::core::option::Option<&::std::collections::HashSet<&'static str>>,
4237 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4238 let mut _after_pairs = self.__rustango_audit_pairs();
4239 if let ::core::option::Option::Some(only) = only {
4240 _after_pairs.retain(|(col, _)| only.contains(col));
4241 }
4242 let _before_query = #root::audit::before_image_query(
4243 <Self as #root::core::Model>::SCHEMA,
4244 #pk_column_lit,
4245 ::core::convert::Into::<#root::core::SqlValue>::into(
4246 #pk_value_for_bind,
4247 ),
4248 &[ #( #tracked_cols ),* ],
4249 );
4250 #root::audit::save_one_with_diff(
4251 pool,
4252 query,
4253 &_before_query,
4254 <Self as #root::core::Model>::SCHEMA.table,
4255 self.__rustango_audit_pk(),
4256 _after_pairs,
4257 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
4258 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
4259 |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
4260 ).await
4261 }
4262
4263 #partial
4264 }
4265 } else {
4266 quote!()
4267 }
4268 } else {
4269 pool_save_method
4270 };
4271
4272 // `delete_pool(&Pool)` — emitted for every model with a PK. Two
4273 // body shapes:
4274 // - non-audited: simple dispatch through `sql::delete_pool`
4275 // - audited: routes through `audit::delete_one_with_audit`,
4276 // which opens a per-backend transaction wrapping DELETE +
4277 // audit emit so the data write and audit row commit atomically.
4278 let pool_delete_method = {
4279 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
4280 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
4281 if let Some(pk_ident) = pk_ident_for_pool {
4282 if audited_fields.is_some() {
4283 quote! {
4284 /// Delete this row against either backend with audit
4285 /// emission inside the same transaction. Bi-dialect
4286 /// counterpart of [`Self::delete`] for audited models.
4287 /// Keyed on the PK; global scopes do not apply.
4288 ///
4289 /// # Errors
4290 /// As [`Self::delete`].
4291 pub async fn delete_pool(
4292 &self,
4293 pool: &#root::sql::Pool,
4294 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4295 let _query = #root::core::DeleteQuery::by_pk(
4296 <Self as #root::core::Model>::SCHEMA,
4297 #pk_column_lit,
4298 ::core::convert::Into::<#root::core::SqlValue>::into(
4299 ::core::clone::Clone::clone(&self.#pk_ident)
4300 ),
4301 );
4302 let _audit_entry =
4303 self.__rustango_audit_entry(#root::audit::AuditOp::Delete);
4304 #root::audit::delete_one_with_audit(
4305 pool, &_query, &_audit_entry,
4306 ).await
4307 }
4308 }
4309 } else {
4310 quote! {
4311 /// Delete the row identified by this instance's primary key
4312 /// against either backend. Equivalent to [`Self::delete`] but
4313 /// takes [`#root::sql::Pool`] and dispatches per backend.
4314 /// Global scopes do not apply.
4315 ///
4316 /// # Errors
4317 /// As [`Self::delete`].
4318 pub async fn delete_pool(
4319 &self,
4320 pool: &#root::sql::Pool,
4321 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4322 let _query = #root::core::DeleteQuery::by_pk(
4323 <Self as #root::core::Model>::SCHEMA,
4324 #pk_column_lit,
4325 ::core::convert::Into::<#root::core::SqlValue>::into(
4326 ::core::clone::Clone::clone(&self.#pk_ident)
4327 ),
4328 );
4329 #root::sql::delete_pool(pool, &_query).await
4330 }
4331 }
4332 }
4333 } else {
4334 quote!()
4335 }
4336 };
4337
4338 // Bulk writers on audited models write one audit row per affected
4339 // row, in the write's transaction (#1747).
4340 let (bulk_delete_run, bulk_update_run, truncate_run) = if audited_fields.is_some() {
4341 (
4342 quote!(#root::audit::delete_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4343 _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4344 })
4345 .await),
4346 quote!(#root::audit::update_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4347 _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
4348 })
4349 .await),
4350 quote! {
4351 let _table = <Self as #root::core::Model>::SCHEMA.table;
4352 let _sql = #root::sql::truncate_sql(pool.dialect(), _table);
4353 #root::audit::truncate_with_audit(pool, _table, &_sql).await
4354 },
4355 )
4356 } else {
4357 (
4358 quote!(#root::sql::delete_pool(pool, &_query).await),
4359 quote!(#root::sql::update_pool(pool, &_query).await),
4360 quote!(#root::sql::truncate(pool, <Self as #root::core::Model>::SCHEMA).await),
4361 )
4362 };
4363
4364 // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
4365 // matching this instance's PK and overwrite the in-memory state
4366 // with the freshly-fetched columns.
4367 // Issue #825. Only emitted when the model declares a PK; non-PK
4368 // models can't address a specific row.
4369 //
4370 // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
4371 // every field from `self`; resets the PK to `Auto::Unset` when
4372 // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
4373 // a fresh autoincrement value. Non-Auto PKs preserve the source
4374 // PK — the caller must overwrite before insert. Pure-Rust, no
4375 // I/O, no dialect surface.
4376 let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
4377 let other_field_clones: Vec<TokenStream2> = fields
4378 .column_entries
4379 .iter()
4380 .filter(|c| &c.ident != pk_ident)
4381 .map(|c| {
4382 let ident = &c.ident;
4383 quote! {
4384 #ident: ::core::clone::Clone::clone(&self.#ident)
4385 }
4386 })
4387 .collect();
4388 let pk_clone_token = if fields.pk_is_auto {
4389 quote! { #pk_ident: #root::sql::Auto::Unset }
4390 } else {
4391 quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
4392 };
4393 let replicate_doc = if fields.pk_is_auto {
4394 quote! {
4395 /// Eloquent-style `replicate()` — return a clone of this
4396 /// row with the primary key reset to [`Auto::Unset`] so
4397 /// the copy is ready for `insert_pool` / `save_pool` to
4398 /// allocate a fresh autoincrement value. Every other
4399 /// field is `Clone`d verbatim. Issue #825.
4400 ///
4401 /// `auto_now_add` / `auto_now` timestamp fields are
4402 /// **not** reset (Eloquent's `replicate` doesn't reset
4403 /// them either) — pass them through the normal insert
4404 /// path if you want fresh values, or assign them
4405 /// explicitly after the call.
4406 }
4407 } else {
4408 quote! {
4409 /// Eloquent-style `replicate()` — clone this row
4410 /// verbatim. Because the primary key is **not** an
4411 /// `Auto<T>`, the clone keeps the source PK; the
4412 /// caller must overwrite `copy.<pk>` before inserting
4413 /// to avoid a unique-key violation. Issue #825.
4414 }
4415 };
4416 // 2026-06-07 — field-name / shortcut collision guard.
4417 //
4418 // The macro emits both `pub const <field>: <field>_col = ...`
4419 // (per-field typed-column const, used by the typed-builder
4420 // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
4421 // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
4422 // model has a field named e.g. `count`, both items would land
4423 // in the same inherent impl with the same name, and the
4424 // compiler rejects the derive with "duplicate definitions".
4425 //
4426 // Drop the conflicting shortcut for that model. Callers can
4427 // still reach the same behavior via
4428 // `QuerySet::<Model>::default().count(&pool)`.
4429 let column_names: ::std::collections::HashSet<String> = fields
4430 .column_entries
4431 .iter()
4432 .map(|c| ident_name(&c.ident))
4433 .collect();
4434 let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
4435 if column_names.contains(name) {
4436 quote! {}
4437 } else {
4438 tokens
4439 }
4440 };
4441 let count_method = emit_if_no_field_collision(
4442 "count",
4443 quote! {
4444 /// Count rows of this model — `SELECT COUNT(*) FROM
4445 /// <table>`. Eloquent `Model::count()` parity.
4446 ///
4447 /// Skipped on models that already declare a field named
4448 /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
4449 /// in that case.
4450 ///
4451 /// # Errors
4452 /// As [`CounterPool::count`].
4453 ///
4454 /// [`CounterPool::count`]: rustango::sql::CounterPool::count
4455 pub async fn count(
4456 pool: &#root::sql::Pool,
4457 ) -> ::core::result::Result<i64, #root::sql::ExecError> {
4458 use #root::sql::CounterPool as _;
4459 #root::query::QuerySet::<Self>::default()
4460 .count(pool)
4461 .await
4462 }
4463 },
4464 );
4465 let value_method = emit_if_no_field_collision(
4466 "value",
4467 quote! {
4468 /// Pluck a single scalar from the first row.
4469 /// Eloquent `Model::query()->value($col)` parity.
4470 ///
4471 /// Skipped on models that already declare a field named
4472 /// `value`. Drop into
4473 /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4474 ///
4475 /// # Errors
4476 /// As `ValuesFlatQuerySet::first`.
4477 pub async fn value<U>(
4478 col: &str,
4479 pool: &#root::sql::Pool,
4480 ) -> ::core::result::Result<
4481 ::core::option::Option<U>,
4482 #root::sql::ExecError,
4483 >
4484 where
4485 U: #root::sql::FlatScalar,
4486 {
4487 let _col_static: &'static str = Self::__resolve_col(col)?;
4488 #root::query::QuerySet::<Self>::default()
4489 .values_list_flat(_col_static)
4490 .first::<U>(pool)
4491 .await
4492 }
4493 },
4494 );
4495 let sum_method = emit_if_no_field_collision(
4496 "sum",
4497 quote! {
4498 /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4499 /// Skipped on models that already declare a field named
4500 /// `sum`.
4501 ///
4502 /// # Errors
4503 /// As [`#root::sql::fetch_aggregate_pool`].
4504 pub async fn sum<U>(
4505 col: &str,
4506 pool: &#root::sql::Pool,
4507 ) -> ::core::result::Result<
4508 ::core::option::Option<U>,
4509 #root::sql::ExecError,
4510 >
4511 where
4512 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4513 + #root::sql::MaybeMyFromRow
4514 + #root::sql::MaybeSqliteFromRow
4515 + ::core::marker::Send
4516 + ::core::marker::Unpin,
4517 {
4518 #root::query::QuerySet::<Self>::default()
4519 .sum::<U>(col, pool)
4520 .await
4521 }
4522 },
4523 );
4524 let avg_method = emit_if_no_field_collision(
4525 "avg",
4526 quote! {
4527 /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4528 /// models that already declare a field named `avg`.
4529 ///
4530 /// # Errors
4531 /// As [`#root::sql::fetch_aggregate_pool`].
4532 pub async fn avg<U>(
4533 col: &str,
4534 pool: &#root::sql::Pool,
4535 ) -> ::core::result::Result<
4536 ::core::option::Option<U>,
4537 #root::sql::ExecError,
4538 >
4539 where
4540 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4541 + #root::sql::MaybeMyFromRow
4542 + #root::sql::MaybeSqliteFromRow
4543 + ::core::marker::Send
4544 + ::core::marker::Unpin,
4545 {
4546 #root::query::QuerySet::<Self>::default()
4547 .avg::<U>(col, pool)
4548 .await
4549 }
4550 },
4551 );
4552 let min_method = emit_if_no_field_collision(
4553 "min",
4554 quote! {
4555 /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4556 /// models that already declare a field named `min`.
4557 ///
4558 /// # Errors
4559 /// As [`#root::sql::fetch_aggregate_pool`].
4560 pub async fn min<U>(
4561 col: &str,
4562 pool: &#root::sql::Pool,
4563 ) -> ::core::result::Result<
4564 ::core::option::Option<U>,
4565 #root::sql::ExecError,
4566 >
4567 where
4568 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4569 + #root::sql::MaybeMyFromRow
4570 + #root::sql::MaybeSqliteFromRow
4571 + ::core::marker::Send
4572 + ::core::marker::Unpin,
4573 {
4574 #root::query::QuerySet::<Self>::default()
4575 .min::<U>(col, pool)
4576 .await
4577 }
4578 },
4579 );
4580 let max_method = emit_if_no_field_collision(
4581 "max",
4582 quote! {
4583 /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4584 /// models that already declare a field named `max`.
4585 ///
4586 /// # Errors
4587 /// As [`#root::sql::fetch_aggregate_pool`].
4588 pub async fn max<U>(
4589 col: &str,
4590 pool: &#root::sql::Pool,
4591 ) -> ::core::result::Result<
4592 ::core::option::Option<U>,
4593 #root::sql::ExecError,
4594 >
4595 where
4596 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4597 + #root::sql::MaybeMyFromRow
4598 + #root::sql::MaybeSqliteFromRow
4599 + ::core::marker::Send
4600 + ::core::marker::Unpin,
4601 {
4602 #root::query::QuerySet::<Self>::default()
4603 .max::<U>(col, pool)
4604 .await
4605 }
4606 },
4607 );
4608 let first_method = emit_if_no_field_collision(
4609 "first",
4610 quote! {
4611 /// First row of this model. Eloquent `Model::first()`.
4612 /// Skipped on models that already declare a field named
4613 /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4614 ///
4615 /// # Errors
4616 /// As `QuerySet::first`.
4617 pub async fn first(
4618 pool: &#root::sql::Pool,
4619 ) -> ::core::result::Result<
4620 ::core::option::Option<Self>,
4621 #root::sql::ExecError,
4622 > {
4623 #root::query::QuerySet::<Self>::default()
4624 .first(pool)
4625 .await
4626 }
4627 },
4628 );
4629 let last_method = emit_if_no_field_collision(
4630 "last",
4631 quote! {
4632 /// Last row of this model by primary-key DESC.
4633 /// Eloquent `Model::query()->latest('id')->first()`
4634 /// parity — fetches the highest-PK row without
4635 /// requiring the caller to spell the PK column.
4636 /// Returns `None` on an empty table.
4637 ///
4638 /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4639 /// Skipped on models that already declare a field
4640 /// named `last`.
4641 ///
4642 /// # Errors
4643 /// As `QuerySet::last`.
4644 pub async fn last(
4645 pool: &#root::sql::Pool,
4646 ) -> ::core::result::Result<
4647 ::core::option::Option<Self>,
4648 #root::sql::ExecError,
4649 > {
4650 #root::query::QuerySet::<Self>::default()
4651 .last(pool)
4652 .await
4653 }
4654 },
4655 );
4656 quote! {
4657 /// Re-SELECT this row by its primary key and overwrite
4658 /// every in-memory field with the freshly-fetched value.
4659 /// Issue #825.
4660 ///
4661 /// Use this when the row may have been modified by another
4662 /// process / connection / job since you read it — e.g. after
4663 /// a queued task callback, or to re-sync stale UI state
4664 /// before re-saving.
4665 ///
4666 /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4667 /// when the primary key no longer matches any row (e.g.
4668 /// the row was deleted concurrently).
4669 ///
4670 /// # Errors
4671 /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4672 /// the PK no longer exists.
4673 ///
4674 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4675 pub async fn refresh_from_db(
4676 &mut self,
4677 pool: &#root::sql::Pool,
4678 ) -> ::core::result::Result<(), #root::sql::ExecError> {
4679 use #root::sql::FetcherPool as _;
4680 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4681 ::core::clone::Clone::clone(&self.#pk_ident),
4682 );
4683 let mut _rows: ::std::vec::Vec<Self> =
4684 #root::query::QuerySet::<Self>::default()
4685 .filter(::core::stringify!(#pk_ident), _pk_val)
4686 .limit(1)
4687 .fetch(pool)
4688 .await?;
4689 match _rows.into_iter().next() {
4690 ::core::option::Option::Some(_fresh) => {
4691 *self = _fresh;
4692 ::core::result::Result::Ok(())
4693 }
4694 ::core::option::Option::None => ::core::result::Result::Err(
4695 #root::sql::ExecError::Driver(
4696 #root::sql::sqlx::Error::RowNotFound,
4697 ),
4698 ),
4699 }
4700 }
4701
4702 /// Atomically increment the integer column `col` by
4703 /// `by` for this row. Equivalent to
4704 /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4705 ///
4706 /// **Doesn't mutate `self`** — the in-memory copy is now
4707 /// stale; call [`Self::refresh_from_db`] /
4708 /// [`Self::fresh`] to re-sync. Returns the rows-
4709 /// affected count (0 when the PK doesn't match any row,
4710 /// 1 on success).
4711 ///
4712 /// `col` is the Rust field name as a string; unknown
4713 /// fields surface as `UnknownField` at runtime. Negative
4714 /// `by` values atomically decrement (see also
4715 /// [`Self::decrement`]).
4716 ///
4717 /// # Errors
4718 /// As [`UpdaterPool::execute_pool`].
4719 ///
4720 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4721 pub async fn increment(
4722 &self,
4723 col: &str,
4724 by: i64,
4725 pool: &#root::sql::Pool,
4726 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4727 Self::__increment_one(self, col, by, pool).await
4728 }
4729
4730 /// Sibling of [`Self::increment`] — atomically
4731 /// decrement this row's `col` by `by`. Eloquent
4732 /// `$model->decrement($col, $by)` parity. Equivalent to
4733 /// `self.increment(col, -by, &pool)`; the separate name
4734 /// keeps call sites readable.
4735 ///
4736 /// # Errors
4737 /// As [`Self::increment`].
4738 pub async fn decrement(
4739 &self,
4740 col: &str,
4741 by: i64,
4742 pool: &#root::sql::Pool,
4743 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4744 Self::__increment_one(self, col, -by, pool).await
4745 }
4746
4747 /// Bulk-increment: add `by` to `col` on every row of the
4748 /// table. Eloquent `Model::query()->increment($col, $by)`
4749 /// parity. Use for counters, score adjustments, view
4750 /// rollups.
4751 ///
4752 /// # Errors
4753 /// As [`UpdaterPool::execute_pool`].
4754 ///
4755 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4756 pub async fn increment_each(
4757 col: &str,
4758 by: i64,
4759 pool: &#root::sql::Pool,
4760 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4761 Self::__increment_all(col, by, pool).await
4762 }
4763
4764 /// Sibling of [`Self::increment_each`] — bulk-decrement.
4765 ///
4766 /// # Errors
4767 /// As [`Self::increment_each`].
4768 pub async fn decrement_each(
4769 col: &str,
4770 by: i64,
4771 pool: &#root::sql::Pool,
4772 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4773 Self::__increment_all(col, -by, pool).await
4774 }
4775
4776 /// Internal: forward to
4777 /// [`#root::sql::model_shortcuts::increment_one_pool`].
4778 /// One-line wrapper kept as a per-Model method so the
4779 /// macro's emitted `increment` / `decrement` instance
4780 /// calls don't have to thread `Self` through manually.
4781 #[doc(hidden)]
4782 pub async fn __increment_one(
4783 this: &Self,
4784 col: &str,
4785 by: i64,
4786 pool: &#root::sql::Pool,
4787 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4788 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4789 ::core::clone::Clone::clone(&this.#pk_ident),
4790 );
4791 #root::sql::model_shortcuts::increment_one_pool::<Self>(
4792 ::core::stringify!(#pk_ident),
4793 _pk_val,
4794 col,
4795 by,
4796 pool,
4797 )
4798 .await
4799 }
4800
4801 /// Internal: run
4802 /// [`#root::sql::model_shortcuts::increment_all_query`].
4803 #[doc(hidden)]
4804 pub async fn __increment_all(
4805 col: &str,
4806 by: i64,
4807 pool: &#root::sql::Pool,
4808 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4809 let _query =
4810 #root::sql::model_shortcuts::increment_all_query::<Self>(col, by)?;
4811 #bulk_update_run
4812 }
4813
4814 /// Internal: forward to
4815 /// [`#root::sql::model_shortcuts::resolve_col`].
4816 #[doc(hidden)]
4817 pub fn __resolve_col(
4818 col: &str,
4819 ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4820 #root::sql::model_shortcuts::resolve_col::<Self>(col)
4821 }
4822
4823 /// Internal: forward to
4824 /// [`#root::sql::model_shortcuts::add_signed_expr`].
4825 #[doc(hidden)]
4826 #[must_use]
4827 pub fn __add_signed_expr(
4828 col_static: &'static str,
4829 signed_by: i64,
4830 ) -> #root::core::Expr {
4831 #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4832 }
4833
4834 /// Re-SELECT this row by its primary key and return a
4835 /// **new** instance with the freshly-fetched fields.
4836 /// Eloquent `Model::fresh()` parity — non-mutating
4837 /// counterpart of [`Self::refresh_from_db`].
4838 ///
4839 /// Returns `Ok(None)` when the row was deleted
4840 /// concurrently — vs [`Self::refresh_from_db`]
4841 /// which surfaces that as `RowNotFound` because
4842 /// in-place mutation has nothing to write to.
4843 ///
4844 /// Useful when you want to compare the in-memory
4845 /// instance against the persisted state (audit-style
4846 /// diffs, conflict detection) without mutating the
4847 /// reference you already hold.
4848 ///
4849 /// # Errors
4850 /// As [`FetcherPool::fetch`].
4851 ///
4852 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4853 pub async fn fresh(
4854 &self,
4855 pool: &#root::sql::Pool,
4856 ) -> ::core::result::Result<
4857 ::core::option::Option<Self>,
4858 #root::sql::ExecError,
4859 > {
4860 use #root::sql::FetcherPool as _;
4861 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4862 ::core::clone::Clone::clone(&self.#pk_ident),
4863 );
4864 let _rows: ::std::vec::Vec<Self> =
4865 #root::query::QuerySet::<Self>::default()
4866 .filter(::core::stringify!(#pk_ident), _pk_val)
4867 .limit(1)
4868 .fetch(pool)
4869 .await?;
4870 ::core::result::Result::Ok(_rows.into_iter().next())
4871 }
4872
4873 #replicate_doc
4874 #[must_use]
4875 pub fn replicate(&self) -> Self {
4876 Self {
4877 #pk_clone_token,
4878 #( #other_field_clones, )*
4879 }
4880 }
4881
4882 #first_method
4883
4884 #last_method
4885
4886 /// Throwing counterpart of [`Self::first`] —
4887 /// errors with `RowNotFound` when the table is empty.
4888 /// Eloquent `Model::firstOrFail()` parity.
4889 ///
4890 /// # Errors
4891 /// As [`Self::first`]; additionally
4892 /// [`sqlx::Error::RowNotFound`] on empty tables.
4893 ///
4894 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4895 pub async fn first_or_fail(
4896 pool: &#root::sql::Pool,
4897 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4898 // Route through the queryset rather than `Self::first`,
4899 // which is suppressed on models with a field named
4900 // `first` (field/shortcut collision guard).
4901 match #root::query::QuerySet::<Self>::default().first(pool).await? {
4902 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4903 ::core::option::Option::None => ::core::result::Result::Err(
4904 #root::sql::ExecError::Driver(
4905 #root::sql::sqlx::Error::RowNotFound,
4906 ),
4907 ),
4908 }
4909 }
4910
4911 /// Single-column projection — `SELECT <col> FROM
4912 /// <table>`. Returns `Vec<U>` where each element is the
4913 /// decoded value of the column. Eloquent
4914 /// `Model::pluck($column)` parity.
4915 ///
4916 /// Thin wrapper over `QuerySet::<Self>::default()
4917 /// .values_list_flat(col).fetch::<U>(pool)`. `U` is a
4918 /// `FlatScalar`; use `Option<_>` for a nullable column.
4919 ///
4920 /// # Errors
4921 /// As `ValuesFlatQuerySet::fetch`.
4922 pub async fn pluck<U>(
4923 col: &'static str,
4924 pool: &#root::sql::Pool,
4925 ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4926 where
4927 U: #root::sql::FlatScalar,
4928 {
4929 #root::query::QuerySet::<Self>::default()
4930 .values_list_flat(col)
4931 .fetch::<U>(pool)
4932 .await
4933 }
4934
4935 /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4936 /// stream every row of this model in batches of `n`,
4937 /// invoking the callback once per batch. Stable PK-ASC
4938 /// ordering so the LIMIT/OFFSET pagination is
4939 /// deterministic across drivers.
4940 ///
4941 /// The callback is async — it can do further DB work
4942 /// (writes, related-row lookups, queue dispatch) per
4943 /// batch. Return `Err(...)` from the callback to abort
4944 /// the iteration early; the error bubbles up.
4945 ///
4946 /// **When to use**: bulk processing flows that can't
4947 /// fit the whole table in memory — sending newsletters,
4948 /// running data migrations, computing summary
4949 /// statistics. For small / known-bounded tables, plain
4950 /// `Self::all(&pool)` is simpler.
4951 ///
4952 /// **Caveat**: ascending OFFSET pagination is O(N²) on
4953 /// large tables. For multi-million-row scans prefer
4954 /// keyset-by-PK (the standard "WHERE id > last_seen"
4955 /// shape) over `chunk(...)`.
4956 ///
4957 /// Skipped on models without a primary key — chunking
4958 /// needs a stable order to avoid skipping / repeating
4959 /// rows across batches.
4960 pub async fn chunk<F, Fut>(
4961 n: i64,
4962 pool: &#root::sql::Pool,
4963 mut cb: F,
4964 ) -> ::core::result::Result<(), #root::sql::ExecError>
4965 where
4966 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4967 Fut: ::core::future::Future<
4968 Output = ::core::result::Result<(), #root::sql::ExecError>,
4969 >,
4970 {
4971 use #root::sql::FetcherPool as _;
4972 let pk_col = match Self::primary_key_column() {
4973 ::core::option::Option::Some(c) => c,
4974 ::core::option::Option::None => {
4975 return ::core::result::Result::Ok(());
4976 }
4977 };
4978 let mut offset: i64 = 0;
4979 loop {
4980 let rows: ::std::vec::Vec<Self> =
4981 #root::query::QuerySet::<Self>::default()
4982 .order_by(&[(pk_col, false)])
4983 .limit(n)
4984 .offset(offset)
4985 .fetch(pool)
4986 .await?;
4987 if rows.is_empty() {
4988 return ::core::result::Result::Ok(());
4989 }
4990 let len = rows.len() as i64;
4991 cb(rows).await?;
4992 if len < n {
4993 return ::core::result::Result::Ok(());
4994 }
4995 offset += n;
4996 }
4997 }
4998
4999 /// Eloquent `Model::chunkById($n, fn (...))` — same
5000 /// per-batch callback shape as [`Self::chunk`], but uses
5001 /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
5002 /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
5003 /// — the right choice for multi-million-row sweeps.
5004 ///
5005 /// Requires the primary key to be a signed integer type
5006 /// (`i64` / `i32`); the keyset comparison rides on
5007 /// `__rustango_pk_value()` lowering through
5008 /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
5009 /// non-integer PKs (UUID, String) — those should use
5010 /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
5011 /// keyset loop.
5012 ///
5013 /// Callback errors abort iteration; the error bubbles up
5014 /// unchanged. Empty table → callback invoked zero times.
5015 pub async fn chunk_by_id<F, Fut>(
5016 n: i64,
5017 pool: &#root::sql::Pool,
5018 mut cb: F,
5019 ) -> ::core::result::Result<(), #root::sql::ExecError>
5020 where
5021 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
5022 Fut: ::core::future::Future<
5023 Output = ::core::result::Result<(), #root::sql::ExecError>,
5024 >,
5025 {
5026 use #root::sql::FetcherPool as _;
5027 let pk_col = match Self::primary_key_column() {
5028 ::core::option::Option::Some(c) => c,
5029 ::core::option::Option::None => {
5030 return ::core::result::Result::Ok(());
5031 }
5032 };
5033 // Track the largest PK seen so the next batch picks
5034 // up from there. `i64::MIN` as the sentinel — the
5035 // very first iteration's `> MIN` matches every row,
5036 // so the loop entry is uniform with subsequent
5037 // iterations.
5038 let mut last_seen: i64 = i64::MIN;
5039 loop {
5040 let key = ::std::format!("{}__gt", pk_col);
5041 let rows: ::std::vec::Vec<Self> =
5042 #root::query::QuerySet::<Self>::default()
5043 .filter(key.as_str(), last_seen)
5044 .order_by(&[(pk_col, false)])
5045 .limit(n)
5046 .fetch(pool)
5047 .await?;
5048 if rows.is_empty() {
5049 return ::core::result::Result::Ok(());
5050 }
5051 let len = rows.len() as i64;
5052 // Capture the last row's PK BEFORE moving rows
5053 // into the callback.
5054 let max_pk = match rows
5055 .last()
5056 .map(|r| r.__rustango_pk_value())
5057 {
5058 ::core::option::Option::Some(
5059 #root::core::SqlValue::I64(v),
5060 ) => v,
5061 ::core::option::Option::Some(
5062 #root::core::SqlValue::I32(v),
5063 ) => i64::from(v),
5064 _ => return ::core::result::Result::Ok(()),
5065 };
5066 cb(rows).await?;
5067 if len < n {
5068 return ::core::result::Result::Ok(());
5069 }
5070 last_seen = max_pk;
5071 }
5072 }
5073
5074 /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
5075 /// per-row callback companion to [`Self::chunk`].
5076 /// Streams every row in keyset-paginated batches of
5077 /// `batch` size, calling `cb` once per row.
5078 ///
5079 /// Inherits the keyset-paginated scan of
5080 /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
5081 /// non-integer PKs are silently a no-op).
5082 ///
5083 /// ```ignore
5084 /// Post::each(500, &pool, |p| async move {
5085 /// reindex(p).await?;
5086 /// Ok(())
5087 /// }).await?;
5088 /// ```
5089 ///
5090 /// Return `Err(...)` from the callback to abort
5091 /// iteration; the error bubbles up unchanged.
5092 pub async fn each<F, Fut>(
5093 batch: i64,
5094 pool: &#root::sql::Pool,
5095 mut cb: F,
5096 ) -> ::core::result::Result<(), #root::sql::ExecError>
5097 where
5098 F: ::core::ops::FnMut(Self) -> Fut,
5099 Fut: ::core::future::Future<
5100 Output = ::core::result::Result<(), #root::sql::ExecError>,
5101 >,
5102 {
5103 use #root::sql::FetcherPool as _;
5104 let pk_col = match Self::primary_key_column() {
5105 ::core::option::Option::Some(c) => c,
5106 ::core::option::Option::None => {
5107 return ::core::result::Result::Ok(());
5108 }
5109 };
5110 let mut last_seen: i64 = i64::MIN;
5111 loop {
5112 let key = ::std::format!("{}__gt", pk_col);
5113 let rows: ::std::vec::Vec<Self> =
5114 #root::query::QuerySet::<Self>::default()
5115 .filter(key.as_str(), last_seen)
5116 .order_by(&[(pk_col, false)])
5117 .limit(batch)
5118 .fetch(pool)
5119 .await?;
5120 if rows.is_empty() {
5121 return ::core::result::Result::Ok(());
5122 }
5123 let len = rows.len() as i64;
5124 let max_pk = match rows
5125 .last()
5126 .map(|r| r.__rustango_pk_value())
5127 {
5128 ::core::option::Option::Some(
5129 #root::core::SqlValue::I64(v),
5130 ) => v,
5131 ::core::option::Option::Some(
5132 #root::core::SqlValue::I32(v),
5133 ) => i64::from(v),
5134 _ => return ::core::result::Result::Ok(()),
5135 };
5136 for row in rows {
5137 cb(row).await?;
5138 }
5139 if len < batch {
5140 return ::core::result::Result::Ok(());
5141 }
5142 last_seen = max_pk;
5143 }
5144 }
5145
5146 /// Delete every row of this model — `TRUNCATE TABLE
5147 /// <table> RESTART IDENTITY CASCADE` on Postgres,
5148 /// `DELETE FROM <table>` on MySQL / SQLite (which don't
5149 /// support `TRUNCATE` inside foreign-key constraints
5150 /// or — for SQLite — at all). Eloquent `Model::truncate()`
5151 /// parity.
5152 ///
5153 /// **Use only in tests / fixture-reset flows.** Production
5154 /// writes through this would silently bypass the
5155 /// `pre_delete` / `post_delete` signals (no per-row hooks
5156 /// fire on a TRUNCATE / bulk DELETE FROM). An audited model
5157 /// gets one bulk audit entry, not one per row. Global scopes
5158 /// do not apply: every row goes, generic M2M links too.
5159 ///
5160 /// # Errors
5161 /// As [`truncate`].
5162 ///
5163 /// [`truncate`]: rustango::sql::truncate
5164 pub async fn truncate(
5165 pool: &#root::sql::Pool,
5166 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5167 #truncate_run
5168 }
5169
5170 /// Bulk-delete every row whose primary key is in
5171 /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
5172 /// Returns the affected row count.
5173 ///
5174 /// Eloquent `Model::destroy([1, 2, 3])` parity.
5175 /// Empty `pks` is a no-op (returns 0).
5176 ///
5177 /// Accepts any iterable whose elements are
5178 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
5179 /// `[i64; N]`, etc.
5180 ///
5181 /// # Errors
5182 /// As `delete_pool`.
5183 pub async fn destroy<V>(
5184 pks: impl ::core::iter::IntoIterator<Item = V>,
5185 pool: &#root::sql::Pool,
5186 ) -> ::core::result::Result<u64, #root::sql::ExecError>
5187 where
5188 V: ::core::convert::Into<#root::core::SqlValue>,
5189 {
5190 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5191 pks.into_iter().map(::core::convert::Into::into).collect();
5192 if _values.is_empty() {
5193 return ::core::result::Result::Ok(0);
5194 }
5195 let _pk = <Self as #root::core::Model>::SCHEMA
5196 .primary_key()
5197 .ok_or_else(|| {
5198 #root::sql::ExecError::Sql(#root::sql::SqlError::MissingPrimaryKey)
5199 })?;
5200 let _query = #root::query::QuerySet::<Self>::default()
5201 .filter_op(
5202 _pk.name,
5203 #root::core::Op::In,
5204 #root::core::SqlValue::List(_values),
5205 )
5206 .compile_delete()?;
5207 #bulk_delete_run
5208 }
5209
5210 /// Fetch every row where `<col> = <val>`. Eloquent
5211 /// `Model::where($col, $val)->get()` parity.
5212 ///
5213 /// Thin wrapper over `QuerySet::<Self>::default()
5214 /// .filter(col, val).fetch(pool)`. For one row,
5215 /// use [`Self::first_where`]; for a chain that
5216 /// needs further `.filter()` / `.order_by()` /
5217 /// `.limit()`, drop down to `Self::query().filter(...)`
5218 /// directly.
5219 ///
5220 /// `val` accepts any value `Into<SqlValue>` so plain
5221 /// strings, ints, UUIDs, etc. all work.
5222 ///
5223 /// # Errors
5224 /// As [`FetcherPool::fetch`].
5225 ///
5226 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5227 pub async fn where_(
5228 col: &str,
5229 val: impl ::core::convert::Into<#root::core::SqlValue>,
5230 pool: &#root::sql::Pool,
5231 ) -> ::core::result::Result<
5232 ::std::vec::Vec<Self>,
5233 #root::sql::ExecError,
5234 > {
5235 use #root::sql::FetcherPool as _;
5236 #root::query::QuerySet::<Self>::default()
5237 .filter(col, val)
5238 .fetch(pool)
5239 .await
5240 }
5241
5242 /// Fetch every row where `<col> IN (vals)`. Eloquent
5243 /// `Model::whereIn($col, $vals)->get()` parity. Empty
5244 /// `vals` returns no rows (matches SQL's empty-IN
5245 /// semantics).
5246 ///
5247 /// `vals` accepts any iterable whose items are
5248 /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
5249 /// etc.
5250 ///
5251 /// # Errors
5252 /// As [`FetcherPool::fetch`].
5253 ///
5254 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5255 pub async fn where_in<V>(
5256 col: &str,
5257 vals: impl ::core::iter::IntoIterator<Item = V>,
5258 pool: &#root::sql::Pool,
5259 ) -> ::core::result::Result<
5260 ::std::vec::Vec<Self>,
5261 #root::sql::ExecError,
5262 >
5263 where
5264 V: ::core::convert::Into<#root::core::SqlValue>,
5265 {
5266 use #root::sql::FetcherPool as _;
5267 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5268 vals.into_iter().map(::core::convert::Into::into).collect();
5269 if _values.is_empty() {
5270 return ::core::result::Result::Ok(::std::vec::Vec::new());
5271 }
5272 let _key = ::std::format!("{}__in", col);
5273 #root::query::QuerySet::<Self>::default()
5274 .filter(&_key, #root::core::SqlValue::List(_values))
5275 .fetch(pool)
5276 .await
5277 }
5278
5279 /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
5280 /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
5281 /// `vals` returns every row (matches SQL's empty-NOT-IN
5282 /// semantics — vacuously true for every row).
5283 ///
5284 /// # Errors
5285 /// As [`FetcherPool::fetch`].
5286 ///
5287 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5288 pub async fn where_not_in<V>(
5289 col: &str,
5290 vals: impl ::core::iter::IntoIterator<Item = V>,
5291 pool: &#root::sql::Pool,
5292 ) -> ::core::result::Result<
5293 ::std::vec::Vec<Self>,
5294 #root::sql::ExecError,
5295 >
5296 where
5297 V: ::core::convert::Into<#root::core::SqlValue>,
5298 {
5299 use #root::sql::FetcherPool as _;
5300 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5301 vals.into_iter().map(::core::convert::Into::into).collect();
5302 if _values.is_empty() {
5303 return #root::query::QuerySet::<Self>::default()
5304 .fetch(pool)
5305 .await;
5306 }
5307 let _key = ::std::format!("{}__not_in", col);
5308 #root::query::QuerySet::<Self>::default()
5309 .filter(&_key, #root::core::SqlValue::List(_values))
5310 .fetch(pool)
5311 .await
5312 }
5313
5314 /// Fetch every row where `<col> IS NULL`. Eloquent
5315 /// `Model::whereNull($col)->get()` parity.
5316 ///
5317 /// # Errors
5318 /// As [`FetcherPool::fetch`].
5319 ///
5320 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5321 pub async fn where_null(
5322 col: &str,
5323 pool: &#root::sql::Pool,
5324 ) -> ::core::result::Result<
5325 ::std::vec::Vec<Self>,
5326 #root::sql::ExecError,
5327 > {
5328 use #root::sql::FetcherPool as _;
5329 let _key = ::std::format!("{}__isnull", col);
5330 #root::query::QuerySet::<Self>::default()
5331 .filter(&_key, #root::core::SqlValue::Bool(true))
5332 .fetch(pool)
5333 .await
5334 }
5335
5336 /// Fetch every row where `<col> IS NOT NULL`. Eloquent
5337 /// `Model::whereNotNull($col)->get()` parity.
5338 ///
5339 /// # Errors
5340 /// As [`FetcherPool::fetch`].
5341 ///
5342 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5343 pub async fn where_not_null(
5344 col: &str,
5345 pool: &#root::sql::Pool,
5346 ) -> ::core::result::Result<
5347 ::std::vec::Vec<Self>,
5348 #root::sql::ExecError,
5349 > {
5350 use #root::sql::FetcherPool as _;
5351 let _key = ::std::format!("{}__isnull", col);
5352 #root::query::QuerySet::<Self>::default()
5353 .filter(&_key, #root::core::SqlValue::Bool(false))
5354 .fetch(pool)
5355 .await
5356 }
5357
5358 /// Fetch up to `n` rows in random order. Eloquent
5359 /// `Model::inRandomOrder()->limit($n)->get()` /
5360 /// `Model::query()->inRandomOrder()->get()->take($n)`
5361 /// parity. **Performance caveat**: random ordering
5362 /// forces a full table scan + per-row random key sort;
5363 /// the optimizer cannot use an index. Prefer a
5364 /// `pk >= rand_offset LIMIT N` walk for huge tables.
5365 ///
5366 /// # Errors
5367 /// As [`FetcherPool::fetch`].
5368 ///
5369 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5370 pub async fn random_n(
5371 n: i64,
5372 pool: &#root::sql::Pool,
5373 ) -> ::core::result::Result<
5374 ::std::vec::Vec<Self>,
5375 #root::sql::ExecError,
5376 > {
5377 use #root::sql::FetcherPool as _;
5378 #root::query::QuerySet::<Self>::default()
5379 .order_random()
5380 .limit(n)
5381 .fetch(pool)
5382 .await
5383 }
5384
5385 /// Fetch one row in random order. Eloquent
5386 /// `Model::inRandomOrder()->first()` parity. Same
5387 /// performance caveat as [`Self::random_n`].
5388 ///
5389 /// # Errors
5390 /// As [`FetcherPool::fetch`].
5391 ///
5392 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5393 pub async fn random(
5394 pool: &#root::sql::Pool,
5395 ) -> ::core::result::Result<
5396 ::core::option::Option<Self>,
5397 #root::sql::ExecError,
5398 > {
5399 ::core::result::Result::Ok(
5400 Self::random_n(1, pool).await?.into_iter().next(),
5401 )
5402 }
5403
5404 /// Fetch every row ordered ASC by `field`. Eloquent
5405 /// `Model::oldest($field)->get()` parity — the multi-row
5406 /// counterpart of [`Self::earliest`].
5407 ///
5408 /// # Errors
5409 /// As [`FetcherPool::fetch`].
5410 ///
5411 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5412 pub async fn oldest(
5413 field: &str,
5414 pool: &#root::sql::Pool,
5415 ) -> ::core::result::Result<
5416 ::std::vec::Vec<Self>,
5417 #root::sql::ExecError,
5418 > {
5419 use #root::sql::FetcherPool as _;
5420 #root::query::QuerySet::<Self>::default()
5421 .order_by(&[(field, false)])
5422 .fetch(pool)
5423 .await
5424 }
5425
5426 /// Fetch every row ordered DESC by `field`. Eloquent
5427 /// `Model::latest($field)->get()` parity — the multi-row
5428 /// counterpart of [`Self::latest`].
5429 ///
5430 /// # Errors
5431 /// As [`FetcherPool::fetch`].
5432 ///
5433 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5434 pub async fn newest(
5435 field: &str,
5436 pool: &#root::sql::Pool,
5437 ) -> ::core::result::Result<
5438 ::std::vec::Vec<Self>,
5439 #root::sql::ExecError,
5440 > {
5441 use #root::sql::FetcherPool as _;
5442 #root::query::QuerySet::<Self>::default()
5443 .order_by(&[(field, true)])
5444 .fetch(pool)
5445 .await
5446 }
5447
5448 /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5449 /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5450 /// Routes through the existing `__year` lookup suffix
5451 /// (issue #829).
5452 ///
5453 /// # Errors
5454 /// As [`FetcherPool::fetch`].
5455 ///
5456 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5457 pub async fn where_year(
5458 col: &str,
5459 year: i64,
5460 pool: &#root::sql::Pool,
5461 ) -> ::core::result::Result<
5462 ::std::vec::Vec<Self>,
5463 #root::sql::ExecError,
5464 > {
5465 use #root::sql::FetcherPool as _;
5466 let _key = ::std::format!("{}__year", col);
5467 #root::query::QuerySet::<Self>::default()
5468 .filter(&_key, #root::core::SqlValue::I64(year))
5469 .fetch(pool)
5470 .await
5471 }
5472
5473 /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5474 /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5475 /// `month` is 1–12.
5476 ///
5477 /// # Errors
5478 /// As [`FetcherPool::fetch`].
5479 ///
5480 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5481 pub async fn where_month(
5482 col: &str,
5483 month: i64,
5484 pool: &#root::sql::Pool,
5485 ) -> ::core::result::Result<
5486 ::std::vec::Vec<Self>,
5487 #root::sql::ExecError,
5488 > {
5489 use #root::sql::FetcherPool as _;
5490 let _key = ::std::format!("{}__month", col);
5491 #root::query::QuerySet::<Self>::default()
5492 .filter(&_key, #root::core::SqlValue::I64(month))
5493 .fetch(pool)
5494 .await
5495 }
5496
5497 /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5498 /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5499 /// `day` is 1–31.
5500 ///
5501 /// # Errors
5502 /// As [`FetcherPool::fetch`].
5503 ///
5504 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5505 pub async fn where_day(
5506 col: &str,
5507 day: i64,
5508 pool: &#root::sql::Pool,
5509 ) -> ::core::result::Result<
5510 ::std::vec::Vec<Self>,
5511 #root::sql::ExecError,
5512 > {
5513 use #root::sql::FetcherPool as _;
5514 let _key = ::std::format!("{}__day", col);
5515 #root::query::QuerySet::<Self>::default()
5516 .filter(&_key, #root::core::SqlValue::I64(day))
5517 .fetch(pool)
5518 .await
5519 }
5520
5521 /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5522 /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5523 /// `hour` is 0–23.
5524 ///
5525 /// # Errors
5526 /// As [`FetcherPool::fetch`].
5527 ///
5528 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5529 pub async fn where_hour(
5530 col: &str,
5531 hour: i64,
5532 pool: &#root::sql::Pool,
5533 ) -> ::core::result::Result<
5534 ::std::vec::Vec<Self>,
5535 #root::sql::ExecError,
5536 > {
5537 use #root::sql::FetcherPool as _;
5538 let _key = ::std::format!("{}__hour", col);
5539 #root::query::QuerySet::<Self>::default()
5540 .filter(&_key, #root::core::SqlValue::I64(hour))
5541 .fetch(pool)
5542 .await
5543 }
5544
5545 /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5546 /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5547 /// `minute` is 0–59.
5548 ///
5549 /// # Errors
5550 /// As [`FetcherPool::fetch`].
5551 ///
5552 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5553 pub async fn where_minute(
5554 col: &str,
5555 minute: i64,
5556 pool: &#root::sql::Pool,
5557 ) -> ::core::result::Result<
5558 ::std::vec::Vec<Self>,
5559 #root::sql::ExecError,
5560 > {
5561 use #root::sql::FetcherPool as _;
5562 let _key = ::std::format!("{}__minute", col);
5563 #root::query::QuerySet::<Self>::default()
5564 .filter(&_key, #root::core::SqlValue::I64(minute))
5565 .fetch(pool)
5566 .await
5567 }
5568
5569 /// Fetch every row where `<col> LIKE <pattern>` —
5570 /// caller-supplied pattern (must include `%` / `_`
5571 /// wildcards manually). Eloquent `Model::whereLike`
5572 /// parity.
5573 ///
5574 /// # Errors
5575 /// As [`FetcherPool::fetch`].
5576 ///
5577 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5578 pub async fn where_like(
5579 col: &str,
5580 pattern: impl ::core::convert::Into<::std::string::String>,
5581 pool: &#root::sql::Pool,
5582 ) -> ::core::result::Result<
5583 ::std::vec::Vec<Self>,
5584 #root::sql::ExecError,
5585 > {
5586 use #root::sql::FetcherPool as _;
5587 let _key = ::std::format!("{}__like", col);
5588 #root::query::QuerySet::<Self>::default()
5589 .filter(
5590 &_key,
5591 #root::core::SqlValue::String(pattern.into()),
5592 )
5593 .fetch(pool)
5594 .await
5595 }
5596
5597 /// Fetch every row where `<col> ILIKE <pattern>` —
5598 /// case-insensitive LIKE (PG native, MySQL/SQLite
5599 /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5600 ///
5601 /// # Errors
5602 /// As [`FetcherPool::fetch`].
5603 ///
5604 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5605 pub async fn where_ilike(
5606 col: &str,
5607 pattern: impl ::core::convert::Into<::std::string::String>,
5608 pool: &#root::sql::Pool,
5609 ) -> ::core::result::Result<
5610 ::std::vec::Vec<Self>,
5611 #root::sql::ExecError,
5612 > {
5613 use #root::sql::FetcherPool as _;
5614 let _key = ::std::format!("{}__ilike", col);
5615 #root::query::QuerySet::<Self>::default()
5616 .filter(
5617 &_key,
5618 #root::core::SqlValue::String(pattern.into()),
5619 )
5620 .fetch(pool)
5621 .await
5622 }
5623
5624 /// Fetch every row where `<col>` starts with `prefix`
5625 /// (auto-appends `%`). Eloquent
5626 /// `whereLike("col", "$prefix%")` parity.
5627 ///
5628 /// # Errors
5629 /// As [`FetcherPool::fetch`].
5630 ///
5631 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5632 pub async fn where_starts_with(
5633 col: &str,
5634 prefix: impl ::core::convert::Into<::std::string::String>,
5635 pool: &#root::sql::Pool,
5636 ) -> ::core::result::Result<
5637 ::std::vec::Vec<Self>,
5638 #root::sql::ExecError,
5639 > {
5640 use #root::sql::FetcherPool as _;
5641 let _key = ::std::format!("{}__startswith", col);
5642 #root::query::QuerySet::<Self>::default()
5643 .filter(
5644 &_key,
5645 #root::core::SqlValue::String(prefix.into()),
5646 )
5647 .fetch(pool)
5648 .await
5649 }
5650
5651 /// Fetch every row where `<col>` ends with `suffix`
5652 /// (auto-prepends `%`). Eloquent
5653 /// `whereLike("col", "%$suffix")` parity.
5654 ///
5655 /// # Errors
5656 /// As [`FetcherPool::fetch`].
5657 ///
5658 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5659 pub async fn where_ends_with(
5660 col: &str,
5661 suffix: impl ::core::convert::Into<::std::string::String>,
5662 pool: &#root::sql::Pool,
5663 ) -> ::core::result::Result<
5664 ::std::vec::Vec<Self>,
5665 #root::sql::ExecError,
5666 > {
5667 use #root::sql::FetcherPool as _;
5668 let _key = ::std::format!("{}__endswith", col);
5669 #root::query::QuerySet::<Self>::default()
5670 .filter(
5671 &_key,
5672 #root::core::SqlValue::String(suffix.into()),
5673 )
5674 .fetch(pool)
5675 .await
5676 }
5677
5678 /// Fetch every row where `<col>` contains `substr`
5679 /// (auto-wraps with `%`).
5680 /// Eloquent `whereLike("col", "%$substr%")` parity.
5681 ///
5682 /// # Errors
5683 /// As [`FetcherPool::fetch`].
5684 ///
5685 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5686 pub async fn where_contains(
5687 col: &str,
5688 substr: impl ::core::convert::Into<::std::string::String>,
5689 pool: &#root::sql::Pool,
5690 ) -> ::core::result::Result<
5691 ::std::vec::Vec<Self>,
5692 #root::sql::ExecError,
5693 > {
5694 use #root::sql::FetcherPool as _;
5695 let _key = ::std::format!("{}__contains", col);
5696 #root::query::QuerySet::<Self>::default()
5697 .filter(
5698 &_key,
5699 #root::core::SqlValue::String(substr.into()),
5700 )
5701 .fetch(pool)
5702 .await
5703 }
5704
5705 /// Fetch every row where `<col> > val`. Eloquent
5706 /// `Model::where($col, ">", $val)->get()` parity.
5707 ///
5708 /// # Errors
5709 /// As [`FetcherPool::fetch`].
5710 ///
5711 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5712 pub async fn where_gt(
5713 col: &str,
5714 val: impl ::core::convert::Into<#root::core::SqlValue>,
5715 pool: &#root::sql::Pool,
5716 ) -> ::core::result::Result<
5717 ::std::vec::Vec<Self>,
5718 #root::sql::ExecError,
5719 > {
5720 use #root::sql::FetcherPool as _;
5721 let _key = ::std::format!("{}__gt", col);
5722 #root::query::QuerySet::<Self>::default()
5723 .filter(&_key, val)
5724 .fetch(pool)
5725 .await
5726 }
5727
5728 /// Fetch every row where `<col> >= val`. Eloquent
5729 /// `Model::where($col, ">=", $val)->get()` parity.
5730 ///
5731 /// # Errors
5732 /// As [`FetcherPool::fetch`].
5733 ///
5734 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5735 pub async fn where_gte(
5736 col: &str,
5737 val: impl ::core::convert::Into<#root::core::SqlValue>,
5738 pool: &#root::sql::Pool,
5739 ) -> ::core::result::Result<
5740 ::std::vec::Vec<Self>,
5741 #root::sql::ExecError,
5742 > {
5743 use #root::sql::FetcherPool as _;
5744 let _key = ::std::format!("{}__gte", col);
5745 #root::query::QuerySet::<Self>::default()
5746 .filter(&_key, val)
5747 .fetch(pool)
5748 .await
5749 }
5750
5751 /// Fetch every row where `<col> < val`. Eloquent
5752 /// `Model::where($col, "<", $val)->get()` parity.
5753 ///
5754 /// # Errors
5755 /// As [`FetcherPool::fetch`].
5756 ///
5757 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5758 pub async fn where_lt(
5759 col: &str,
5760 val: impl ::core::convert::Into<#root::core::SqlValue>,
5761 pool: &#root::sql::Pool,
5762 ) -> ::core::result::Result<
5763 ::std::vec::Vec<Self>,
5764 #root::sql::ExecError,
5765 > {
5766 use #root::sql::FetcherPool as _;
5767 let _key = ::std::format!("{}__lt", col);
5768 #root::query::QuerySet::<Self>::default()
5769 .filter(&_key, val)
5770 .fetch(pool)
5771 .await
5772 }
5773
5774 /// Fetch every row where `<col> <= val`. Eloquent
5775 /// `Model::where($col, "<=", $val)->get()` parity.
5776 ///
5777 /// # Errors
5778 /// As [`FetcherPool::fetch`].
5779 ///
5780 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5781 pub async fn where_lte(
5782 col: &str,
5783 val: impl ::core::convert::Into<#root::core::SqlValue>,
5784 pool: &#root::sql::Pool,
5785 ) -> ::core::result::Result<
5786 ::std::vec::Vec<Self>,
5787 #root::sql::ExecError,
5788 > {
5789 use #root::sql::FetcherPool as _;
5790 let _key = ::std::format!("{}__lte", col);
5791 #root::query::QuerySet::<Self>::default()
5792 .filter(&_key, val)
5793 .fetch(pool)
5794 .await
5795 }
5796
5797 /// Fetch every row where `<col> <> val`. Eloquent
5798 /// `Model::where($col, "!=", $val)->get()` parity.
5799 ///
5800 /// # Errors
5801 /// As [`FetcherPool::fetch`].
5802 ///
5803 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5804 pub async fn where_ne(
5805 col: &str,
5806 val: impl ::core::convert::Into<#root::core::SqlValue>,
5807 pool: &#root::sql::Pool,
5808 ) -> ::core::result::Result<
5809 ::std::vec::Vec<Self>,
5810 #root::sql::ExecError,
5811 > {
5812 use #root::sql::FetcherPool as _;
5813 let _key = ::std::format!("{}__ne", col);
5814 #root::query::QuerySet::<Self>::default()
5815 .filter(&_key, val)
5816 .fetch(pool)
5817 .await
5818 }
5819
5820 /// Fetch every row matching `<col> = val` for ANY of the
5821 /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5822 /// parity. Empty `cols` returns no rows.
5823 ///
5824 /// Resolves each `&str` column to its SCHEMA-registered
5825 /// `&'static str` once and builds a single OR-composed
5826 /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5827 ///
5828 /// # Errors
5829 /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5830 /// when any column is not declared on the model.
5831 ///
5832 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5833 pub async fn where_any(
5834 cols: &[&str],
5835 val: impl ::core::convert::Into<#root::core::SqlValue>,
5836 pool: &#root::sql::Pool,
5837 ) -> ::core::result::Result<
5838 ::std::vec::Vec<Self>,
5839 #root::sql::ExecError,
5840 > {
5841 Self::__where_multi(cols, val, false, pool).await
5842 }
5843
5844 /// Fetch every row matching `<col> = val` for ALL listed
5845 /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5846 /// Empty `cols` returns every row (vacuous AND).
5847 ///
5848 /// # Errors
5849 /// As [`Self::where_any`].
5850 pub async fn where_all(
5851 cols: &[&str],
5852 val: impl ::core::convert::Into<#root::core::SqlValue>,
5853 pool: &#root::sql::Pool,
5854 ) -> ::core::result::Result<
5855 ::std::vec::Vec<Self>,
5856 #root::sql::ExecError,
5857 > {
5858 Self::__where_multi(cols, val, true, pool).await
5859 }
5860
5861 /// Internal: build a Q expression composing `cols` via
5862 /// AND (`all`) or OR (`!all`), then fetch. Backs
5863 /// `where_any` / `where_all`.
5864 #[doc(hidden)]
5865 pub async fn __where_multi(
5866 cols: &[&str],
5867 val: impl ::core::convert::Into<#root::core::SqlValue>,
5868 all: bool,
5869 pool: &#root::sql::Pool,
5870 ) -> ::core::result::Result<
5871 ::std::vec::Vec<Self>,
5872 #root::sql::ExecError,
5873 > {
5874 #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5875 .await
5876 }
5877
5878 /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5879 /// parity. PK-ordered
5880 /// is NOT guaranteed without an explicit `order_by` —
5881 /// drop into `Self::query()` for that.
5882 ///
5883 /// # Errors
5884 /// As [`FetcherPool::fetch`].
5885 ///
5886 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5887 pub async fn take(
5888 n: i64,
5889 pool: &#root::sql::Pool,
5890 ) -> ::core::result::Result<
5891 ::std::vec::Vec<Self>,
5892 #root::sql::ExecError,
5893 > {
5894 use #root::sql::FetcherPool as _;
5895 #root::query::QuerySet::<Self>::default()
5896 .limit(n)
5897 .fetch(pool)
5898 .await
5899 }
5900
5901 /// Fetch the page-th window of `per_page` rows
5902 /// (1-indexed). Eloquent
5903 /// `Model::query()->forPage($page, $perPage)->get()`
5904 /// parity. The DB scans an `OFFSET (page - 1) * per_page
5905 /// LIMIT per_page`; for large offsets this is O(N) —
5906 /// prefer keyset pagination via PK on hot paths.
5907 ///
5908 /// # Errors
5909 /// As [`FetcherPool::fetch`].
5910 ///
5911 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5912 pub async fn for_page(
5913 page: i64,
5914 per_page: i64,
5915 pool: &#root::sql::Pool,
5916 ) -> ::core::result::Result<
5917 ::std::vec::Vec<Self>,
5918 #root::sql::ExecError,
5919 > {
5920 use #root::sql::FetcherPool as _;
5921 let _offset = #root::list_params::page_offset(page, per_page);
5922 #root::query::QuerySet::<Self>::default()
5923 .limit(per_page)
5924 .offset(_offset)
5925 .fetch(pool)
5926 .await
5927 }
5928
5929 /// Eloquent `Model::paginate($per_page, $page)` — fetch
5930 /// one page of rows AND the total row count in one
5931 /// call. Returns `(rows, total)`. Two queries: a
5932 /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5933 /// for the total.
5934 ///
5935 /// Useful for paginated UIs that need both the visible
5936 /// rows AND a "Page X of Y" / total-count widget. For
5937 /// large tables prefer keyset pagination + cached count;
5938 /// every call to `paginate` re-counts the full table.
5939 ///
5940 /// Same 1-indexed `page` convention as [`Self::for_page`].
5941 ///
5942 /// # Errors
5943 /// As [`Self::for_page`] and [`Self::count`].
5944 pub async fn paginate(
5945 page: i64,
5946 per_page: i64,
5947 pool: &#root::sql::Pool,
5948 ) -> ::core::result::Result<
5949 (::std::vec::Vec<Self>, i64),
5950 #root::sql::ExecError,
5951 > {
5952 // Route through the queryset rather than `Self::count`:
5953 // the `count` inherent method is suppressed on models
5954 // that declare a field named `count` (field/shortcut
5955 // collision guard), and `paginate` must still compile
5956 // for those models.
5957 let total = {
5958 use #root::sql::CounterPool as _;
5959 #root::query::QuerySet::<Self>::default()
5960 .count(pool)
5961 .await?
5962 };
5963 let rows = Self::for_page(page, per_page, pool).await?;
5964 ::core::result::Result::Ok((rows, total))
5965 }
5966
5967 /// Bulk-update — set `set_col = set_val` on every row
5968 /// matching `where_col = where_val`. Returns affected row
5969 /// count. Eloquent
5970 /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5971 /// parity.
5972 ///
5973 /// For multi-column updates drop into the queryset
5974 /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5975 ///
5976 /// # Errors
5977 /// As [`UpdaterPool::execute_pool`].
5978 ///
5979 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5980 pub async fn update_where(
5981 where_col: &str,
5982 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5983 set_col: &str,
5984 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5985 pool: &#root::sql::Pool,
5986 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5987 let _query = #root::query::QuerySet::<Self>::default()
5988 .filter(where_col, where_val)
5989 .update()
5990 .set(set_col, set_val)
5991 .compile()?;
5992 #bulk_update_run
5993 }
5994
5995 /// Bulk-delete — remove every row matching
5996 /// `where_col = where_val`. Returns affected row count.
5997 /// Eloquent
5998 /// `Model::where($where_col, $where_val)->delete()` parity.
5999 ///
6000 /// # Errors
6001 /// As [`#root::sql::delete_pool`].
6002 pub async fn delete_where(
6003 where_col: &str,
6004 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
6005 pool: &#root::sql::Pool,
6006 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6007 let _query = #root::query::QuerySet::<Self>::default()
6008 .filter_op(where_col, #root::core::Op::Eq, where_val)
6009 .compile_delete()?;
6010 #bulk_delete_run
6011 }
6012
6013 /// Bulk-update — set `set_col = set_val` on EVERY row of
6014 /// this model's table. **No WHERE clause** — use with
6015 /// care. Eloquent `Model::query()->update([$col => $val])`
6016 /// parity.
6017 ///
6018 /// Use for backfills, one-shot reset flows, etc. The
6019 /// returned count is rows affected.
6020 ///
6021 /// # Errors
6022 /// As [`UpdaterPool::execute_pool`].
6023 ///
6024 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
6025 pub async fn update_all(
6026 set_col: &str,
6027 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
6028 pool: &#root::sql::Pool,
6029 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6030 let _query = #root::query::QuerySet::<Self>::default()
6031 .update()
6032 .set(set_col, set_val)
6033 .compile()?;
6034 #bulk_update_run
6035 }
6036
6037 /// Fetch every row where `<col> NOT LIKE <pattern>`.
6038 /// Eloquent `Model::whereNotLike` parity. Pattern is
6039 /// passed verbatim — caller controls `%` / `_`.
6040 ///
6041 /// # Errors
6042 /// As [`FetcherPool::fetch`].
6043 ///
6044 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6045 pub async fn where_not_like(
6046 col: &str,
6047 pattern: impl ::core::convert::Into<::std::string::String>,
6048 pool: &#root::sql::Pool,
6049 ) -> ::core::result::Result<
6050 ::std::vec::Vec<Self>,
6051 #root::sql::ExecError,
6052 > {
6053 use #root::sql::FetcherPool as _;
6054 let _key = ::std::format!("{}__not_like", col);
6055 #root::query::QuerySet::<Self>::default()
6056 .filter(
6057 &_key,
6058 #root::core::SqlValue::String(pattern.into()),
6059 )
6060 .fetch(pool)
6061 .await
6062 }
6063
6064 /// Fetch every row where `<col> NOT ILIKE <pattern>` —
6065 /// case-insensitive `NOT LIKE` (PG native, MySQL /
6066 /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
6067 ///
6068 /// # Errors
6069 /// As [`FetcherPool::fetch`].
6070 ///
6071 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6072 pub async fn where_not_ilike(
6073 col: &str,
6074 pattern: impl ::core::convert::Into<::std::string::String>,
6075 pool: &#root::sql::Pool,
6076 ) -> ::core::result::Result<
6077 ::std::vec::Vec<Self>,
6078 #root::sql::ExecError,
6079 > {
6080 use #root::sql::FetcherPool as _;
6081 let _key = ::std::format!("{}__not_ilike", col);
6082 #root::query::QuerySet::<Self>::default()
6083 .filter(
6084 &_key,
6085 #root::core::SqlValue::String(pattern.into()),
6086 )
6087 .fetch(pool)
6088 .await
6089 }
6090
6091 /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
6092 /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
6093 /// parity.
6094 ///
6095 /// # Errors
6096 /// As [`FetcherPool::fetch`].
6097 ///
6098 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6099 pub async fn where_not_between(
6100 col: &str,
6101 lo: impl ::core::convert::Into<#root::core::SqlValue>,
6102 hi: impl ::core::convert::Into<#root::core::SqlValue>,
6103 pool: &#root::sql::Pool,
6104 ) -> ::core::result::Result<
6105 ::std::vec::Vec<Self>,
6106 #root::sql::ExecError,
6107 > {
6108 use #root::sql::FetcherPool as _;
6109 let _key = ::std::format!("{}__not_between", col);
6110 let _vals = #root::core::SqlValue::List(::std::vec![
6111 ::core::convert::Into::into(lo),
6112 ::core::convert::Into::into(hi),
6113 ]);
6114 #root::query::QuerySet::<Self>::default()
6115 .filter(&_key, _vals)
6116 .fetch(pool)
6117 .await
6118 }
6119
6120 /// Returns the SQL table name for this model. Eloquent
6121 /// `$model->getTable()` parity.
6122 #[must_use]
6123 pub fn table_name() -> &'static str {
6124 <Self as #root::core::Model>::SCHEMA.table
6125 }
6126
6127 /// Returns the SQL column name of this model's primary
6128 /// key, or `None` when the model has no
6129 /// `#[rustango(primary_key)]`. Eloquent
6130 /// `$model->getKeyName()` parity.
6131 #[must_use]
6132 pub fn primary_key_column() -> ::core::option::Option<&'static str> {
6133 <Self as #root::core::Model>::SCHEMA
6134 .primary_key()
6135 .map(|f| f.column)
6136 }
6137
6138 /// Fetch every row where `<col> BETWEEN lo AND hi`
6139 /// (inclusive on both ends — same as SQL). Eloquent
6140 /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
6141 ///
6142 /// # Errors
6143 /// As [`FetcherPool::fetch`].
6144 ///
6145 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6146 pub async fn where_between(
6147 col: &str,
6148 lo: impl ::core::convert::Into<#root::core::SqlValue>,
6149 hi: impl ::core::convert::Into<#root::core::SqlValue>,
6150 pool: &#root::sql::Pool,
6151 ) -> ::core::result::Result<
6152 ::std::vec::Vec<Self>,
6153 #root::sql::ExecError,
6154 > {
6155 use #root::sql::FetcherPool as _;
6156 let _key = ::std::format!("{}__between", col);
6157 let _vals = #root::core::SqlValue::List(::std::vec![
6158 ::core::convert::Into::into(lo),
6159 ::core::convert::Into::into(hi),
6160 ]);
6161 #root::query::QuerySet::<Self>::default()
6162 .filter(&_key, _vals)
6163 .fetch(pool)
6164 .await
6165 }
6166
6167 /// Fetch the first row where `<col> = <val>`. Returns
6168 /// `Ok(None)` when no row matches. Eloquent
6169 /// `Model::firstWhere($col, $val)` parity.
6170 ///
6171 /// Thin wrapper over `QuerySet::<Self>::default()
6172 /// .filter(col, val).first(pool)`. Use this when you
6173 /// want one row identified by a non-PK column (e.g.
6174 /// `User::first_where_pool("email", "x@y.com", &pool)`).
6175 ///
6176 /// `val` accepts any value `Into<SqlValue>` so plain
6177 /// strings, ints, UUIDs, etc. all work.
6178 ///
6179 /// # Errors
6180 /// As `QuerySet::first`.
6181 pub async fn first_where(
6182 col: &str,
6183 val: impl ::core::convert::Into<#root::core::SqlValue>,
6184 pool: &#root::sql::Pool,
6185 ) -> ::core::result::Result<
6186 ::core::option::Option<Self>,
6187 #root::sql::ExecError,
6188 > {
6189 #root::query::QuerySet::<Self>::default()
6190 .filter(col, val)
6191 .first(pool)
6192 .await
6193 }
6194
6195 #value_method
6196
6197 /// Fetch the row with the largest `field` value —
6198 /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
6199 /// `Ok(None)` for an empty table. Eloquent
6200 /// `Model::latest($field)->first()` parity.
6201 /// Thin wrapper over `QuerySet::<Self>::default()
6202 /// .latest(field, pool)`.
6203 ///
6204 /// **Field name** is the Rust field ident as a string
6205 /// (not the SQL column). It is a runtime `&str`, so an
6206 /// unknown one compiles and surfaces as
6207 /// `ExecError::Query(QueryError::UnknownField)` when the
6208 /// query runs.
6209 ///
6210 /// # Errors
6211 /// As `QuerySet::latest`.
6212 pub async fn latest(
6213 field: &str,
6214 pool: &#root::sql::Pool,
6215 ) -> ::core::result::Result<
6216 ::core::option::Option<Self>,
6217 #root::sql::ExecError,
6218 > {
6219 #root::query::QuerySet::<Self>::default()
6220 .latest(field, pool)
6221 .await
6222 }
6223
6224 /// Sibling of [`Self::latest`] — fetches the row
6225 /// with the smallest `field` value (`ORDER BY <field>
6226 /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
6227 /// ->first()` parity.
6228 ///
6229 /// # Errors
6230 /// As [`Self::latest`].
6231 pub async fn earliest(
6232 field: &str,
6233 pool: &#root::sql::Pool,
6234 ) -> ::core::result::Result<
6235 ::core::option::Option<Self>,
6236 #root::sql::ExecError,
6237 > {
6238 #root::query::QuerySet::<Self>::default()
6239 .earliest(field, pool)
6240 .await
6241 }
6242
6243 #count_method
6244
6245 /// `true` when the table contains at least one row.
6246 /// Eloquent `Model::query()->exists()` parity. Thin wrapper
6247 /// over `QuerySet::<Self>::default().exists(pool)`.
6248 ///
6249 /// # Errors
6250 /// As [`ExistsPool::exists`].
6251 ///
6252 /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
6253 pub async fn exists(
6254 pool: &#root::sql::Pool,
6255 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6256 use #root::sql::ExistsPool as _;
6257 #root::query::QuerySet::<Self>::default()
6258 .exists(pool)
6259 .await
6260 }
6261
6262 /// Inverse of [`Self::exists`] — returns `true` when
6263 /// the table has zero rows. Eloquent
6264 /// `Model::doesntExist()` parity.
6265 ///
6266 /// # Errors
6267 /// As [`Self::exists`].
6268 pub async fn doesnt_exist(
6269 pool: &#root::sql::Pool,
6270 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6271 Self::exists(pool).await.map(|e| !e)
6272 }
6273
6274 /// Eloquent `Model::query()->whereKey($pk)->exists()` —
6275 /// `true` when a row with primary key `pk` exists in
6276 /// the table. Sugar over
6277 /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
6278 ///
6279 /// Differs from [`Self::exists`] (which checks "any row
6280 /// in the table") by checking a specific PK existence.
6281 /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
6282 /// because the row is never materialized — the SQL is
6283 /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
6284 ///
6285 /// # Errors
6286 /// As [`#root::sql::ExistsPool::contains_pk`].
6287 pub async fn contains_pk(
6288 pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
6289 pool: &#root::sql::Pool,
6290 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6291 use #root::sql::ExistsPool as _;
6292 #root::query::QuerySet::<Self>::default()
6293 .contains_pk(pool, pk)
6294 .await
6295 }
6296
6297 #sum_method
6298 #avg_method
6299 #min_method
6300 #max_method
6301
6302 /// Fetch every row of this model from `pool`. Eloquent
6303 /// `Model::all()` parity — a thin wrapper over
6304 /// `QuerySet::<Self>::default().fetch(pool)`.
6305 ///
6306 /// **Use with care on large tables** — there's no
6307 /// pagination or limit; the entire table is materialized
6308 /// into memory. For anything beyond fixture / lookup
6309 /// tables, page through `QuerySet::<Self>::default()
6310 /// .order_by(...).limit(N).offset(M).fetch(pool)`
6311 /// or stream via `.iterator(chunk_size)`.
6312 ///
6313 /// # Errors
6314 /// As [`FetcherPool::fetch`].
6315 ///
6316 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6317 pub async fn all(
6318 pool: &#root::sql::Pool,
6319 ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
6320 {
6321 use #root::sql::FetcherPool as _;
6322 #root::query::QuerySet::<Self>::default()
6323 .fetch(pool)
6324 .await
6325 }
6326
6327 /// Look up every row whose primary key is in `pks`.
6328 /// Returns the matching rows in **inventory** order — NOT
6329 /// the order of `pks`. Empty `pks` returns an empty
6330 /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
6331 /// with a list) parity.
6332 ///
6333 /// Thin wrapper over `QuerySet::<Self>::default()
6334 /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
6335 /// Caller-supplied PKs that don't match a row are
6336 /// silently skipped (the returned vec is shorter than
6337 /// the input list). For an order-preserving / "fail
6338 /// when any missing" variant, build the queryset
6339 /// explicitly with `in_bulk` instead.
6340 ///
6341 /// Accepts any iterable whose elements are
6342 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
6343 /// `Vec<Uuid>`, etc.
6344 ///
6345 /// # Errors
6346 /// As [`FetcherPool::fetch`].
6347 ///
6348 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6349 pub async fn find_many<V>(
6350 pks: impl ::core::iter::IntoIterator<Item = V>,
6351 pool: &#root::sql::Pool,
6352 ) -> ::core::result::Result<
6353 ::std::vec::Vec<Self>,
6354 #root::sql::ExecError,
6355 >
6356 where
6357 V: ::core::convert::Into<#root::core::SqlValue>,
6358 {
6359 use #root::sql::FetcherPool as _;
6360 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6361 pks.into_iter().map(::core::convert::Into::into).collect();
6362 if _values.is_empty() {
6363 return ::core::result::Result::Ok(::std::vec::Vec::new());
6364 }
6365 let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
6366 #root::query::QuerySet::<Self>::default()
6367 .filter(&_key, #root::core::SqlValue::List(_values))
6368 .fetch(pool)
6369 .await
6370 }
6371
6372 /// Look up the row whose primary key equals `pk`. Returns
6373 /// `Ok(None)` when no row matches, rather than erroring.
6374 /// Eloquent `Model::find`
6375 /// shape — accepts any value `Into<SqlValue>`.
6376 ///
6377 /// One-liner shortcut for the common
6378 /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6379 /// .limit(1).fetch(pool).await?.into_iter().next()`
6380 /// dance.
6381 ///
6382 /// # Errors
6383 /// As [`FetcherPool::fetch`].
6384 ///
6385 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6386 pub async fn find(
6387 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6388 pool: &#root::sql::Pool,
6389 ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6390 {
6391 use #root::sql::FetcherPool as _;
6392 let _pk_val: #root::core::SqlValue = pk.into();
6393 let mut _rows: ::std::vec::Vec<Self> =
6394 #root::query::QuerySet::<Self>::default()
6395 .filter(::core::stringify!(#pk_ident), _pk_val)
6396 .limit(1)
6397 .fetch(pool)
6398 .await?;
6399 ::core::result::Result::Ok(_rows.into_iter().next())
6400 }
6401
6402 /// Look up the row whose primary key equals `pk`. Errors
6403 /// when no row matches — the throwing counterpart of
6404 /// [`Self::find`]. Eloquent `Model::findOrFail`
6405 /// parity.
6406 ///
6407 /// Translates the miss into
6408 /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6409 /// so callers can `?`-bubble straight through the typical
6410 /// `ExecError` error chain.
6411 ///
6412 /// # Errors
6413 /// As [`Self::find`]; additionally
6414 /// [`sqlx::Error::RowNotFound`] when no row matches.
6415 ///
6416 /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6417 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6418 pub async fn find_or_fail(
6419 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6420 pool: &#root::sql::Pool,
6421 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6422 match Self::find(pk, pool).await? {
6423 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6424 ::core::option::Option::None => ::core::result::Result::Err(
6425 #root::sql::ExecError::Driver(
6426 #root::sql::sqlx::Error::RowNotFound,
6427 ),
6428 ),
6429 }
6430 }
6431
6432 /// Look up multiple rows by primary key, **failing** if
6433 /// any requested PK is missing. Eloquent
6434 /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6435 /// inventory order (NOT request order — same as
6436 /// [`Self::find_many`]).
6437 ///
6438 /// Empty `pks` returns an empty `Vec` (no rows requested
6439 /// → nothing to fail on).
6440 ///
6441 /// Differs from [`Self::find_many`] in that this method
6442 /// requires every PK to resolve. If even one is missing,
6443 /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6444 /// in `ExecError::Driver`). Rows are deduped at the SQL
6445 /// layer, so passing the same PK twice in `pks` counts as
6446 /// one expected row.
6447 ///
6448 /// # Errors
6449 /// As [`Self::find_many`], plus `RowNotFound` when the
6450 /// returned row count is less than the count of distinct
6451 /// requested PKs.
6452 pub async fn find_many_or_fail<V>(
6453 pks: impl ::core::iter::IntoIterator<Item = V>,
6454 pool: &#root::sql::Pool,
6455 ) -> ::core::result::Result<
6456 ::std::vec::Vec<Self>,
6457 #root::sql::ExecError,
6458 >
6459 where
6460 V: ::core::convert::Into<#root::core::SqlValue>,
6461 {
6462 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6463 pks.into_iter().map(::core::convert::Into::into).collect();
6464 if _values.is_empty() {
6465 return ::core::result::Result::Ok(::std::vec::Vec::new());
6466 }
6467 // Dedup the requested PK list before counting so
6468 // duplicate-PK requests don't false-fail (the SQL
6469 // `IN (...)` clause naturally dedups too).
6470 let mut _seen: ::std::collections::HashSet<
6471 ::std::string::String,
6472 > = ::std::collections::HashSet::new();
6473 for v in &_values {
6474 _seen.insert(v.to_display_string());
6475 }
6476 let _expected = _seen.len();
6477 let _rows = Self::find_many(_values, pool).await?;
6478 if _rows.len() < _expected {
6479 return ::core::result::Result::Err(
6480 #root::sql::ExecError::Driver(
6481 #root::sql::sqlx::Error::RowNotFound,
6482 ),
6483 );
6484 }
6485 ::core::result::Result::Ok(_rows)
6486 }
6487
6488 /// Find by primary key or run `fallback` to produce a
6489 /// default row to return. Eloquent
6490 /// `Model::findOr($pk, fn() => …)` parity.
6491 ///
6492 /// Unlike [`Self::find_or_fail`] (which raises on
6493 /// miss), this is the "give me something sensible"
6494 /// branch: typical use is "fetch the user's row, else
6495 /// fall back to an anonymous/guest stub".
6496 ///
6497 /// `fallback` runs only when no row matches — the DB
6498 /// round-trip happens unconditionally.
6499 ///
6500 /// # Errors
6501 /// As [`Self::find`].
6502 pub async fn find_or<F>(
6503 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6504 pool: &#root::sql::Pool,
6505 fallback: F,
6506 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6507 where
6508 F: ::core::ops::FnOnce() -> Self,
6509 {
6510 ::core::result::Result::Ok(
6511 Self::find(pk, pool).await?.unwrap_or_else(fallback),
6512 )
6513 }
6514
6515 /// Same as [`Self::find_or`] but also returns a `bool`
6516 /// indicating whether the row was found in the DB
6517 /// (`true`) or freshly built from `fallback` (`false`).
6518 /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6519 /// in PHP the returned model also exposes `->exists`;
6520 /// here that's surfaced as the second tuple element.
6521 ///
6522 /// Useful for edit-or-create form handlers where the
6523 /// caller needs to know whether to PATCH or POST when
6524 /// the user submits the form.
6525 ///
6526 /// # Errors
6527 /// As [`Self::find`].
6528 pub async fn find_or_new<F>(
6529 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6530 pool: &#root::sql::Pool,
6531 fallback: F,
6532 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6533 where
6534 F: ::core::ops::FnOnce() -> Self,
6535 {
6536 match Self::find(pk, pool).await? {
6537 ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6538 ::core::option::Option::None => {
6539 ::core::result::Result::Ok((fallback(), false))
6540 }
6541 }
6542 }
6543
6544 /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6545 /// [`Self::find_or_new`] but **persists** the new row
6546 /// when the PK isn't found. Returns `(row, exists: bool)`
6547 /// — `exists=true` when the PK was found,
6548 /// `false` when a fresh row was inserted.
6549 ///
6550 /// ```ignore
6551 /// let (post, found) = Post::find_or_insert(
6552 /// pk,
6553 /// &pool,
6554 /// || Post { id: Auto::default(), title: "new".into() },
6555 /// ).await?;
6556 /// // `found` true → returned existing row;
6557 /// // `found` false → fallback was inserted, `post.id` populated.
6558 /// ```
6559 ///
6560 /// **Caveat**: two concurrent calls that both miss the
6561 /// find can both INSERT, violating uniqueness. Wrap in a
6562 /// transaction or rely on a UNIQUE constraint + handle
6563 /// the conflict error if you need race-free semantics.
6564 ///
6565 /// # Errors
6566 /// As [`Self::find`] and [`Self::save_pool`].
6567 pub async fn find_or_insert<F>(
6568 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6569 pool: &#root::sql::Pool,
6570 fallback: F,
6571 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6572 where
6573 F: ::core::ops::FnOnce() -> Self,
6574 {
6575 if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6576 return ::core::result::Result::Ok((_row, true));
6577 }
6578 let mut _new = fallback();
6579 _new.save_pool(pool).await?;
6580 ::core::result::Result::Ok((_new, false))
6581 }
6582
6583 /// Fetch the first row of the table, or run `fallback`
6584 /// when the table is empty. Eloquent
6585 /// `Model::firstOr(fn() => …)` parity.
6586 ///
6587 /// # Errors
6588 /// As [`Self::first`].
6589 pub async fn first_or<F>(
6590 pool: &#root::sql::Pool,
6591 fallback: F,
6592 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6593 where
6594 F: ::core::ops::FnOnce() -> Self,
6595 {
6596 // Route through the queryset rather than `Self::first`,
6597 // which is suppressed on models with a field named
6598 // `first` (field/shortcut collision guard).
6599 ::core::result::Result::Ok(
6600 #root::query::QuerySet::<Self>::default()
6601 .first(pool)
6602 .await?
6603 .unwrap_or_else(fallback),
6604 )
6605 }
6606
6607 /// Fetch exactly one row matching `<col> = <val>`. Errors
6608 /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6609 /// or more than one matches
6610 /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6611 /// Eloquent `Model::sole($col, $val)` parity.
6612 ///
6613 /// # Errors
6614 /// As [`Self::where_`] plus the explicit
6615 /// `RowNotFound` / `MultipleRowsReturned` cases above.
6616 pub async fn sole(
6617 col: &str,
6618 val: impl ::core::convert::Into<#root::core::SqlValue>,
6619 pool: &#root::sql::Pool,
6620 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6621 let mut _rows = Self::where_(col, val, pool).await?;
6622 match _rows.len() {
6623 0 => ::core::result::Result::Err(
6624 #root::sql::ExecError::Driver(
6625 #root::sql::sqlx::Error::RowNotFound,
6626 ),
6627 ),
6628 1 => ::core::result::Result::Ok(_rows.remove(0)),
6629 n => ::core::result::Result::Err(
6630 #root::sql::ExecError::MultipleRowsReturned {
6631 op: "sole",
6632 table: <Self as #root::core::Model>::SCHEMA.name,
6633 count: n,
6634 },
6635 ),
6636 }
6637 }
6638 }
6639 } else {
6640 quote!()
6641 };
6642
6643 // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6644 // the non-audited `_pool` methods but execute against an open
6645 // `PoolTx` so the writes participate in the caller's transaction.
6646 // Auditing inside TX is deferred; these always use the plain
6647 // executor primitives regardless of whether the model is audited.
6648 let tx_insert_method = if fields.has_auto {
6649 let pushes = &fields.insert_pushes;
6650 let returning_cols = &fields.returning_cols;
6651 quote! {
6652 /// Insert this row inside an open transaction, populating
6653 /// any `Auto<T>` PK from the auto-assigned value. Works
6654 /// against any backend that `tx` wraps.
6655 ///
6656 /// # Errors
6657 /// As [`Self::insert_pool`].
6658 pub async fn insert_tx(
6659 &mut self,
6660 tx: &mut #root::sql::PoolTx<'_>,
6661 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6662 let mut _columns: ::std::vec::Vec<&'static str> =
6663 ::std::vec::Vec::new();
6664 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6665 ::std::vec::Vec::new();
6666 #( #pushes )*
6667 let _query = #root::core::InsertQuery::new(
6668 <Self as #root::core::Model>::SCHEMA,
6669 _columns,
6670 _values,
6671 )
6672 .returning(::std::vec![ #( #returning_cols ),* ]);
6673 let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6674 #root::sql::apply_auto_pk(_result, self)
6675 }
6676 }
6677 } else {
6678 let insert_columns = &fields.insert_columns;
6679 let insert_values = &fields.insert_values;
6680 quote! {
6681 /// Insert this row inside an open transaction.
6682 ///
6683 /// # Errors
6684 /// As [`Self::insert_pool`].
6685 pub async fn insert_tx(
6686 &self,
6687 tx: &mut #root::sql::PoolTx<'_>,
6688 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6689 let _query = #root::core::InsertQuery::new(
6690 <Self as #root::core::Model>::SCHEMA,
6691 ::std::vec![ #( #insert_columns ),* ],
6692 ::std::vec![ #( #insert_values ),* ],
6693 );
6694 #root::sql::insert_tx(tx, &_query).await
6695 }
6696 }
6697 };
6698
6699 let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6700 let pk_column_lit = pk_col.as_str();
6701 let assignments = &fields.update_assignments;
6702 let dispatch_unset = if fields.pk_is_auto {
6703 quote! {
6704 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6705 return self.insert_tx(tx).await.map(|()| 1u64);
6706 }
6707 }
6708 } else {
6709 quote!()
6710 };
6711 let write = restore_auto_now_unless(
6712 &fields.auto_now_idents,
6713 &save_ret,
6714 &save_keep,
6715 "e! {
6716 let _query = #root::core::UpdateQuery::new(
6717 <Self as #root::core::Model>::SCHEMA,
6718 ::std::vec![ #( #assignments ),* ],
6719 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6720 #pk_column_lit,
6721 #root::core::Op::Eq,
6722 ::core::convert::Into::<#root::core::SqlValue>::into(
6723 ::core::clone::Clone::clone(&self.#pk_ident)
6724 ),
6725 )),
6726 );
6727 let _affected = #root::sql::update_tx(tx, &_query).await?;
6728 ::core::result::Result::Ok(_affected)
6729 },
6730 );
6731 quote! {
6732 /// Save this row inside an open transaction. `INSERT` when
6733 /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6734 /// PK. Works against any backend that `tx` wraps.
6735 ///
6736 /// # Errors
6737 /// As [`Self::save_pool`].
6738 pub async fn save_tx(
6739 &mut self,
6740 tx: &mut #root::sql::PoolTx<'_>,
6741 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6742 #dispatch_unset
6743 #write
6744 }
6745 }
6746 } else {
6747 quote!()
6748 };
6749
6750 let tx_delete_method = {
6751 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6752 let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6753 if let Some(pk_ident) = pk_ident_for_tx {
6754 quote! {
6755 /// Delete the row identified by this instance's PK
6756 /// inside an open transaction. Works against any backend
6757 /// that `tx` wraps.
6758 ///
6759 /// # Errors
6760 /// As [`Self::delete_pool`].
6761 pub async fn delete_tx(
6762 &self,
6763 tx: &mut #root::sql::PoolTx<'_>,
6764 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6765 let _query = #root::core::DeleteQuery::by_pk(
6766 <Self as #root::core::Model>::SCHEMA,
6767 #pk_column_lit,
6768 ::core::convert::Into::<#root::core::SqlValue>::into(
6769 ::core::clone::Clone::clone(&self.#pk_ident)
6770 ),
6771 );
6772 #root::sql::delete_tx(tx, &_query).await
6773 }
6774 }
6775 } else {
6776 quote!()
6777 }
6778 };
6779
6780 // Update emission captures both BEFORE and AFTER state — runs an
6781 // extra SELECT against `_executor` BEFORE the UPDATE, captures
6782 // each tracked field's prior value, then after the UPDATE diffs
6783 // against the in-memory `&self`. `diff_changes` drops unchanged
6784 // columns so the JSON only contains the actual delta.
6785 //
6786 // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6787 // and binds `_audit_before_pairs`; `audit_update_post` runs
6788 // after the UPDATE and emits the PendingEntry.
6789 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6790 audited_fields
6791 {
6792 if tracked.is_empty() {
6793 (quote!(), quote!())
6794 } else {
6795 let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
6796 let pk_column_lit_for_select =
6797 primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6798 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6799 if fields.pk_is_auto {
6800 quote!(self.#pk_ident.get().copied().unwrap_or_default())
6801 } else {
6802 quote!(::core::clone::Clone::clone(&self.#pk_ident))
6803 }
6804 } else {
6805 quote!(0_i64)
6806 };
6807 let before_pairs = tracked.iter().map(|c| {
6808 let column_lit = c.column.as_str();
6809 let value_ty = &c.value_ty;
6810 quote! {
6811 (
6812 #column_lit,
6813 match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6814 &_audit_before_row, #column_lit,
6815 ) {
6816 ::core::result::Result::Ok(v) => #root::core::__json_value(&v),
6817 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6818 },
6819 )
6820 }
6821 });
6822 let pre = quote! {
6823 let _audit_before_query = #root::audit::before_image_query(
6824 <Self as #root::core::Model>::SCHEMA,
6825 #pk_column_lit_for_select,
6826 ::core::convert::Into::<#root::core::SqlValue>::into(#pk_value_for_bind),
6827 &[ #( #tracked_cols ),* ],
6828 );
6829 // A failed pre-read must not let the UPDATE run unaudited (#1907).
6830 let _audit_before_pairs:
6831 ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6832 #root::sql::select_rows_on(&mut *_executor, &_audit_before_query)
6833 .await?
6834 .into_iter()
6835 .next()
6836 .map(|_audit_before_row| ::std::vec![ #( #before_pairs ),* ]);
6837 };
6838 let post = quote! {
6839 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6840 let _audit_after = self.__rustango_audit_pairs();
6841 if let ::core::option::Option::Some(_audit_entry) =
6842 #root::audit::PendingEntry::update_diff(
6843 <Self as #root::core::Model>::SCHEMA.table,
6844 self.__rustango_audit_pk(),
6845 &_audit_before,
6846 &_audit_after,
6847 )
6848 {
6849 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6850 }
6851 }
6852 };
6853 (pre, post)
6854 }
6855 } else {
6856 (quote!(), quote!())
6857 };
6858
6859 // Bulk-insert audit: one `Create` entry per row once RETURNING has
6860 // set its PK, written in one batched INSERT.
6861 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6862 quote! {
6863 let _audit_entries: ::std::vec::Vec<#root::audit::PendingEntry> = rows
6864 .iter()
6865 .map(|_row| _row.__rustango_audit_entry(#root::audit::AuditOp::Create))
6866 .collect();
6867 #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6868 }
6869 } else {
6870 quote!()
6871 };
6872
6873 let save_method = if fields.pk_is_auto {
6874 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6875 let pk_column_lit = pk_column.as_str();
6876 let assignments = &fields.update_assignments;
6877 let upsert_cols = &fields.upsert_update_columns;
6878 let upsert_pushes = &fields.insert_pushes;
6879 // Read before the pushes, which fill each unset id.
6880 let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
6881 fields.uuid_v7_fields.iter().cloned().unzip();
6882 let upsert_returning = &fields.returning_cols;
6883 let upsert_auto_assigns = &fields.auto_assigns;
6884 let auto_now_idents = &fields.auto_now_idents;
6885 // Conflict target: prefer the first declared `unique_together`
6886 // when it exists. Plain `Auto<T>` PKs are server-assigned via
6887 // `BIGSERIAL` and never collide on insert, so a PK-only target
6888 // would silently turn `upsert()` into "always-insert" for
6889 // surrogate-PK models with composite UNIQUE constraints — see
6890 // `RolePermission` / `UserRole` / `UserPermission` in the
6891 // tenancy permission engine. When no `unique_together` is
6892 // declared we keep the PK target (the original behaviour).
6893 let upsert_target_columns: Vec<String> =
6894 upsert_unique.map_or_else(|| vec![pk_column.clone()], <[String]>::to_vec);
6895 let upsert_target_lits = upsert_target_columns
6896 .iter()
6897 .map(String::as_str)
6898 .collect::<Vec<_>>();
6899 let conflict_clause = if fields.upsert_update_columns.is_empty() {
6900 quote!(#root::core::ConflictClause::DoNothing)
6901 } else {
6902 quote!(#root::core::ConflictClause::DoUpdate {
6903 target: ::std::vec![ #( #upsert_target_lits ),* ],
6904 update_columns: ::std::vec![ #( #upsert_cols ),* ],
6905 })
6906 };
6907 // Audited: the upsert reports whether it inserted or updated (#1795).
6908 let (upsert_run, audit_upsert_emit) = if audited_fields.is_some() {
6909 (
6910 quote! {
6911 let (_returning_row_v, _audit_op) =
6912 #root::audit::upsert_returning_on(&mut *_executor, &query).await?;
6913 },
6914 quote! {
6915 let _audit_entry = self.__rustango_audit_entry(_audit_op);
6916 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6917 },
6918 )
6919 } else {
6920 (
6921 quote! {
6922 let _returning_row_v =
6923 #root::sql::__macro_internals::insert_returning_on(_executor, &query)
6924 .await?;
6925 },
6926 quote!(),
6927 )
6928 };
6929 let save_on_write = restore_auto_now_unless(
6930 auto_now_idents,
6931 &save_ret,
6932 &save_keep,
6933 "e! {
6934 #audit_update_pre
6935 let _query = #root::core::UpdateQuery::new(
6936 <Self as #root::core::Model>::SCHEMA,
6937 ::std::vec![ #( #assignments ),* ],
6938 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6939 #pk_column_lit,
6940 #root::core::Op::Eq,
6941 ::core::convert::Into::<#root::core::SqlValue>::into(
6942 ::core::clone::Clone::clone(&self.#pk_ident)
6943 ),
6944 )),
6945 );
6946 let _affected = #root::sql::__macro_internals::update_on(
6947 #executor_passes_to_data_write,
6948 &_query,
6949 ).await?;
6950 #audit_update_post
6951 ::core::result::Result::Ok(_affected)
6952 },
6953 );
6954 let upsert_write = restore_auto_now_unless(
6955 auto_now_idents,
6956 "e!(::core::result::Result<(), #root::sql::ExecError>),
6957 "e!(_saved.is_ok()),
6958 "e! {
6959 // The UPDATE branch writes `auto_now` too, so stamp it like `save`.
6960 #( self.#auto_now_idents = #root::sql::Auto::Set(#root::__chrono::Utc::now()); )*
6961 #( #upsert_pushes )*
6962 let query = #root::core::InsertQuery::new(
6963 <Self as #root::core::Model>::SCHEMA,
6964 _columns,
6965 _values,
6966 )
6967 .returning(::std::vec![ #( #upsert_returning ),* ])
6968 .on_conflict(#conflict_clause)
6969 .generated(_generated);
6970 #upsert_run
6971 let _returning_row = &_returning_row_v;
6972 #( #upsert_auto_assigns )*
6973 #audit_upsert_emit
6974 ::core::result::Result::Ok(())
6975 },
6976 );
6977 Some(quote! {
6978 /// Insert this row if its `Auto<T>` primary key is
6979 /// `Unset`, otherwise update the existing row matching the
6980 /// PK. The caller doesn't need to pick `insert` vs the
6981 /// bulk-update path manually.
6982 ///
6983 /// On the insert branch, populates the PK from `RETURNING`
6984 /// (same behavior as `insert`). On the update branch,
6985 /// writes every non-PK column back; if no row matches the
6986 /// PK, returns `Ok(())` silently.
6987 ///
6988 /// Only generated when the primary key is declared as
6989 /// `Auto<T>`. Models with a manually-managed PK must use
6990 /// `insert` or the QuerySet update builder.
6991 ///
6992 /// **Postgres only — prefer [`Self::save_pool`].** This takes a
6993 /// driver-specific `PgPool`, so it does not exist at all on a
6994 /// build without the `postgres` feature: selecting `sqlite`
6995 /// makes the method vanish rather than fail with anything that
6996 /// names the cause.
6997 ///
6998 /// [`Self::save_pool`] takes `rustango::sql::Pool` and works on
6999 /// all three backends. The naming is inverted — the short name
7000 /// is the narrow one — and that is tracked in #1293.
7001 ///
7002 /// # Errors
7003 /// Returns [`#root::sql::ExecError`] for SQL-writing
7004 /// or driver failures.
7005 #[cfg(feature = "postgres")]
7006 pub async fn save(
7007 &mut self,
7008 pool: &#root::sql::sqlx::PgPool,
7009 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7010 #pool_to_save_on
7011 }
7012
7013 /// Like [`Self::save`] but accepts any sqlx executor —
7014 /// `&PgPool`, `&mut PgConnection`, or a transaction. The
7015 /// escape hatch for tenant-scoped writes: schema-mode
7016 /// tenants share the registry pool but rely on a per-
7017 /// checkout `SET search_path`, so passing `&PgPool` would
7018 /// silently hit the wrong schema. Acquire a connection
7019 /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
7020 ///
7021 /// # Errors
7022 /// As [`Self::save`].
7023 #[cfg(feature = "postgres")]
7024 pub async fn save_on #executor_generics (
7025 &mut self,
7026 #executor_param,
7027 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7028 #executor_where
7029 {
7030 // #1029 — INSERT writes exactly one row → 1; UPDATE returns
7031 // the rows-affected count (0 when the PK no longer exists).
7032 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
7033 return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
7034 }
7035 #save_on_write
7036 }
7037
7038 /// Per-call override for the audit source. Runs
7039 /// [`Self::save_on`] inside an [`#root::audit::with_source`]
7040 /// scope so the resulting audit entry records `source`
7041 /// instead of the task-local default. Useful for seed
7042 /// scripts and one-off CLI tools that don't sit inside an
7043 /// admin handler. The override applies only to this call;
7044 /// no global state changes.
7045 ///
7046 /// # Errors
7047 /// As [`Self::save_on`].
7048 #[cfg(feature = "postgres")]
7049 pub async fn save_on_with #executor_generics (
7050 &mut self,
7051 #executor_param,
7052 source: #root::audit::AuditSource,
7053 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7054 #executor_where
7055 {
7056 #root::audit::with_source(source, self.save_on(_executor)).await
7057 }
7058
7059 /// Insert this row or update it in-place if the primary key already
7060 /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
7061 ///
7062 /// With `Auto::Unset` PK the server assigns a new key and no conflict
7063 /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
7064 /// inserted if absent or all non-PK columns are overwritten if present.
7065 ///
7066 /// # Errors
7067 /// As [`Self::insert_on`].
7068 #[cfg(feature = "postgres")]
7069 pub async fn upsert(
7070 &mut self,
7071 pool: &#root::sql::sqlx::PgPool,
7072 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7073 #pool_to_upsert_on
7074 }
7075
7076 /// Like [`Self::upsert`] but accepts any sqlx executor.
7077 /// See [`Self::save_on`] for tenancy-scoped rationale.
7078 ///
7079 /// # Errors
7080 /// As [`Self::upsert`].
7081 #[cfg(feature = "postgres")]
7082 pub async fn upsert_on #executor_generics (
7083 &mut self,
7084 #executor_param,
7085 ) -> ::core::result::Result<(), #root::sql::ExecError>
7086 #executor_where
7087 {
7088 let mut _columns: ::std::vec::Vec<&'static str> =
7089 ::std::vec::Vec::new();
7090 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7091 ::std::vec::Vec::new();
7092 #[allow(unused_mut)]
7093 let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7094 #(
7095 if matches!(&self.#gen_idents, #root::sql::Auto::Unset) {
7096 _generated.push(#gen_cols);
7097 }
7098 )*
7099 #upsert_write
7100 }
7101 })
7102 } else {
7103 None
7104 };
7105
7106 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
7107 let pk_column_lit = pk_column.as_str();
7108 // Optional `soft_delete_on` / `restore_on` companions when the
7109 // model has a `#[rustango(soft_delete)]` column. They land
7110 // alongside the regular `delete_on` so callers have both
7111 // options — a hard delete (audit-tracked as a real DELETE) and
7112 // a logical delete (audit-tracked as an UPDATE setting the
7113 // deleted_at column to NOW()).
7114 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
7115 let col_lit = col;
7116 let sd_field_ident = fields
7117 .soft_delete_field_ident
7118 .clone()
7119 .expect("soft_delete_column without ident");
7120 quote! {
7121 /// Soft-delete this row by setting its
7122 /// `#[rustango(soft_delete)]` column to `NOW()`.
7123 /// The row stays in the table; query helpers can
7124 /// filter it out by checking the column for `IS NOT
7125 /// NULL`.
7126 ///
7127 /// # Errors
7128 /// As [`Self::delete`].
7129 #[cfg(feature = "postgres")]
7130 pub async fn soft_delete_on #executor_generics (
7131 &self,
7132 #executor_param,
7133 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7134 #executor_where
7135 {
7136 let _now = #root::__chrono::Utc::now();
7137 let _query = #root::soft_delete::__mark_query(
7138 <Self as #root::core::Model>::SCHEMA,
7139 #col_lit,
7140 #pk_column_lit,
7141 ::core::convert::Into::<#root::core::SqlValue>::into(
7142 ::core::clone::Clone::clone(&self.#pk_ident)
7143 ),
7144 ::core::option::Option::Some(_now),
7145 );
7146 let _affected = #root::sql::__macro_internals::update_on(
7147 #executor_passes_to_data_write,
7148 &_query,
7149 ).await?;
7150 #audit_softdelete_emit
7151 ::core::result::Result::Ok(_affected)
7152 }
7153
7154 /// Inverse of [`Self::soft_delete_on`] — clears the
7155 /// soft-delete column back to NULL so the row is
7156 /// considered live again.
7157 ///
7158 /// # Errors
7159 /// As [`Self::delete`].
7160 #[cfg(feature = "postgres")]
7161 pub async fn restore_on #executor_generics (
7162 &self,
7163 #executor_param,
7164 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7165 #executor_where
7166 {
7167 let _query = #root::soft_delete::__mark_query(
7168 <Self as #root::core::Model>::SCHEMA,
7169 #col_lit,
7170 #pk_column_lit,
7171 ::core::convert::Into::<#root::core::SqlValue>::into(
7172 ::core::clone::Clone::clone(&self.#pk_ident)
7173 ),
7174 ::core::option::Option::None,
7175 );
7176 let _affected = #root::sql::__macro_internals::update_on(
7177 #executor_passes_to_data_write,
7178 &_query,
7179 ).await?;
7180 #audit_restore_emit
7181 ::core::result::Result::Ok(_affected)
7182 }
7183
7184 /// Tri-dialect counterpart of [`Self::soft_delete_on`]
7185 /// — takes [`#root::sql::Pool`] and dispatches per
7186 /// backend. Eloquent `Model::delete()` semantics on
7187 /// soft-delete-enabled models (closes #821 partial).
7188 ///
7189 /// Sets the `#[rustango(soft_delete)]` column to
7190 /// `NOW()` on every backend. Query helpers
7191 /// (`QuerySet::active()` / `only_trashed()`,
7192 /// `soft_delete::active_filter` /
7193 /// `compose_with_active`) filter trashed rows out by
7194 /// reading `IS NULL` on the same column. Keyed on the PK;
7195 /// global scopes do not apply.
7196 ///
7197 /// # Errors
7198 /// As [`#root::sql::update_pool`].
7199 pub async fn soft_delete(
7200 &self,
7201 pool: &#root::sql::Pool,
7202 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7203 let _now = #root::__chrono::Utc::now();
7204 let _query = #root::soft_delete::__mark_query(
7205 <Self as #root::core::Model>::SCHEMA,
7206 #col_lit,
7207 #pk_column_lit,
7208 ::core::convert::Into::<#root::core::SqlValue>::into(
7209 ::core::clone::Clone::clone(&self.#pk_ident)
7210 ),
7211 ::core::option::Option::Some(_now),
7212 );
7213 #pool_softdelete_run
7214 }
7215
7216 /// Tri-dialect counterpart of [`Self::restore_on`].
7217 /// Clears the `#[rustango(soft_delete)]` column back
7218 /// to `NULL`, marking the row live again. Eloquent
7219 /// `Model::restore()` parity. Keyed on the PK; global
7220 /// scopes do not apply.
7221 ///
7222 /// # Errors
7223 /// As [`#root::sql::update_pool`].
7224 pub async fn restore(
7225 &self,
7226 pool: &#root::sql::Pool,
7227 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7228 let _query = #root::soft_delete::__mark_query(
7229 <Self as #root::core::Model>::SCHEMA,
7230 #col_lit,
7231 #pk_column_lit,
7232 ::core::convert::Into::<#root::core::SqlValue>::into(
7233 ::core::clone::Clone::clone(&self.#pk_ident)
7234 ),
7235 ::core::option::Option::None,
7236 );
7237 #pool_restore_run
7238 }
7239
7240 /// Hard-delete this row, ignoring the soft-delete
7241 /// column. Eloquent `Model::forceDelete()` parity —
7242 /// the escape hatch when you need to actually purge
7243 /// data (GDPR, fixture cleanup, etc.).
7244 ///
7245 /// Equivalent to [`Self::delete_pool`] (the framework's
7246 /// non-soft delete) — exposed under the Eloquent name
7247 /// for muscle-memory + so soft-delete-enabled models
7248 /// have all three operations (soft / restore / force)
7249 /// in one place.
7250 ///
7251 /// # Errors
7252 /// As [`Self::delete_pool`].
7253 pub async fn force_delete(
7254 &self,
7255 pool: &#root::sql::Pool,
7256 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7257 Self::delete_pool(self, pool).await
7258 }
7259
7260 /// Returns `true` when this row is soft-deleted (its
7261 /// `#[rustango(soft_delete)]` column is currently
7262 /// set — Eloquent `$model->trashed()` parity).
7263 ///
7264 /// Pure in-memory predicate over `&self`; does not
7265 /// hit the database. Useful in admin/template code
7266 /// like `{% if post.trashed() %}…{% endif %}` and in
7267 /// guard clauses on restore/force-delete flows.
7268 pub fn trashed(&self) -> bool {
7269 ::core::option::Option::is_some(&self.#sd_field_ident)
7270 }
7271
7272 /// Fetch every row whose `#[rustango(soft_delete)]`
7273 /// column is `NULL` (a.k.a. the live, non-trashed
7274 /// rows). Eloquent's default `Model::all()` behavior on
7275 /// a soft-delete model (Eloquent auto-scopes trashed
7276 /// rows out; rustango doesn't have auto-scoping yet —
7277 /// see issue #820 — so this is the explicit shortcut).
7278 ///
7279 /// One-liner over `QuerySet::<Self>::default()
7280 /// .active().fetch(pool)`. Closes #821 partial.
7281 ///
7282 /// # Errors
7283 /// As [`#root::sql::FetcherPool::fetch`].
7284 pub async fn active(
7285 pool: &#root::sql::Pool,
7286 ) -> ::core::result::Result<
7287 ::std::vec::Vec<Self>,
7288 #root::sql::ExecError,
7289 > {
7290 use #root::sql::FetcherPool as _;
7291 #root::query::QuerySet::<Self>::default()
7292 .active()
7293 .fetch(pool)
7294 .await
7295 }
7296
7297 /// Fetch ONLY soft-deleted rows. Eloquent
7298 /// `Model::onlyTrashed()->get()` parity — drives the
7299 /// admin "Trash" page, restore flows, GDPR purge
7300 /// scans, etc.
7301 ///
7302 /// One-liner over `QuerySet::<Self>::default()
7303 /// .only_trashed().fetch(pool)`. Closes #821
7304 /// partial.
7305 ///
7306 /// # Errors
7307 /// As [`#root::sql::FetcherPool::fetch`].
7308 pub async fn only_trashed(
7309 pool: &#root::sql::Pool,
7310 ) -> ::core::result::Result<
7311 ::std::vec::Vec<Self>,
7312 #root::sql::ExecError,
7313 > {
7314 use #root::sql::FetcherPool as _;
7315 #root::query::QuerySet::<Self>::default()
7316 .only_trashed()
7317 .fetch(pool)
7318 .await
7319 }
7320
7321 /// Fetch every row, both live and soft-deleted.
7322 /// Eloquent `Model::withTrashed()->get()` parity.
7323 ///
7324 /// Today every queryset already includes trashed rows
7325 /// (rustango has no global-scope tracking yet — issue
7326 /// #820), so this is functionally equivalent to
7327 /// [`Self::all`]. Exposed as a named shortcut so
7328 /// soft-delete-aware code reads `Model::with_trashed_pool`
7329 /// rather than `Model::all_pool` — keeps intent visible
7330 /// in callers and stays correct when auto-scoping lands.
7331 ///
7332 /// Closes #821 partial.
7333 ///
7334 /// # Errors
7335 /// As [`#root::sql::FetcherPool::fetch`].
7336 pub async fn with_trashed(
7337 pool: &#root::sql::Pool,
7338 ) -> ::core::result::Result<
7339 ::std::vec::Vec<Self>,
7340 #root::sql::ExecError,
7341 > {
7342 use #root::sql::FetcherPool as _;
7343 #root::query::QuerySet::<Self>::default()
7344 .with_trashed()
7345 .fetch(pool)
7346 .await
7347 }
7348
7349 }
7350 } else {
7351 quote!()
7352 };
7353 quote! {
7354 /// Delete the row identified by this instance's primary key.
7355 ///
7356 /// Returns the number of rows affected (0 or 1).
7357 ///
7358 /// **Postgres only — prefer [`Self::delete_pool`].** This takes
7359 /// a driver-specific `PgPool`, so it does not exist on a build
7360 /// without the `postgres` feature. [`Self::delete_pool`] takes
7361 /// `rustango::sql::Pool` and works on all three. The naming is
7362 /// inverted; tracked in #1293.
7363 ///
7364 /// # Errors
7365 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7366 /// driver failures.
7367 #[cfg(feature = "postgres")]
7368 pub async fn delete(
7369 &self,
7370 pool: &#root::sql::sqlx::PgPool,
7371 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7372 #pool_to_delete_on
7373 }
7374
7375 /// Like [`Self::delete`] but accepts any sqlx executor —
7376 /// for tenant-scoped deletes against an explicitly-acquired
7377 /// connection. See [`Self::save_on`] for the rationale.
7378 ///
7379 /// # Errors
7380 /// As [`Self::delete`].
7381 #[cfg(feature = "postgres")]
7382 pub async fn delete_on #executor_generics (
7383 &self,
7384 #executor_param,
7385 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7386 #executor_where
7387 {
7388 let query = #root::core::DeleteQuery::by_pk(
7389 <Self as #root::core::Model>::SCHEMA,
7390 #pk_column_lit,
7391 ::core::convert::Into::<#root::core::SqlValue>::into(
7392 ::core::clone::Clone::clone(&self.#pk_ident)
7393 ),
7394 );
7395 let _affected = #root::sql::__macro_internals::delete_on(
7396 #executor_passes_to_data_write,
7397 &query,
7398 ).await?;
7399 #audit_delete_emit
7400 ::core::result::Result::Ok(_affected)
7401 }
7402
7403 /// Per-call audit-source override for [`Self::delete_on`].
7404 /// See [`Self::save_on_with`] for shape rationale.
7405 ///
7406 /// # Errors
7407 /// As [`Self::delete_on`].
7408 #[cfg(feature = "postgres")]
7409 pub async fn delete_on_with #executor_generics (
7410 &self,
7411 #executor_param,
7412 source: #root::audit::AuditSource,
7413 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7414 #executor_where
7415 {
7416 #root::audit::with_source(source, self.delete_on(_executor)).await
7417 }
7418 #pool_delete_method
7419 #pool_insert_method
7420 #audit_entry_method
7421 #pool_save_method
7422 #refresh_replicate_methods
7423 #tx_delete_method
7424 #tx_insert_method
7425 #tx_save_method
7426 #soft_delete_methods
7427
7428 /// Returns `true` when `other` represents the same DB
7429 /// row as `self` — i.e. their primary keys compare
7430 /// equal. Eloquent `$model->is($other)` parity.
7431 ///
7432 /// Because both arguments are typed `&Self`, the
7433 /// model/table check is automatic — `Post::is` cannot
7434 /// be invoked against a `Comment` at compile time. Only
7435 /// the PK has to be compared at runtime.
7436 pub fn is(&self, other: &Self) -> bool {
7437 self.#pk_ident == other.#pk_ident
7438 }
7439
7440 /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7441 /// parity.
7442 pub fn is_not(&self, other: &Self) -> bool {
7443 self.#pk_ident != other.#pk_ident
7444 }
7445
7446 /// Returns this row's primary-key value as an
7447 /// [`#root::core::SqlValue`]. Eloquent
7448 /// `$model->getKey()` parity.
7449 ///
7450 /// Useful when you need to thread the PK through a
7451 /// generic `Into<SqlValue>`-bound API without knowing the
7452 /// concrete PK type (`i64` vs `Uuid` vs `String`).
7453 #[must_use]
7454 pub fn get_key(&self) -> #root::core::SqlValue {
7455 ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7456 }
7457
7458 }
7459 });
7460
7461 let insert_method = if fields.has_auto {
7462 let pushes = &fields.insert_pushes;
7463 let returning_cols = &fields.returning_cols;
7464 let auto_assigns = &fields.auto_assigns;
7465 quote! {
7466 /// Insert this row into its table. Skips columns whose
7467 /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7468 /// sequence fills them in, then reads each `Auto` column
7469 /// back via `RETURNING` and stores it on `self`.
7470 ///
7471 /// **Postgres only — prefer [`Self::insert_pool`].** This takes
7472 /// a driver-specific `PgPool`, so it does not exist on a build
7473 /// without the `postgres` feature. [`Self::insert_pool`] takes
7474 /// `rustango::sql::Pool` and works on all three. The naming is
7475 /// inverted; tracked in #1293.
7476 ///
7477 /// # Errors
7478 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7479 /// driver failures.
7480 #[cfg(feature = "postgres")]
7481 pub async fn insert(
7482 &mut self,
7483 pool: &#root::sql::sqlx::PgPool,
7484 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7485 #pool_to_insert_on
7486 }
7487
7488 /// Like [`Self::insert`] but accepts any sqlx executor.
7489 /// See [`Self::save_on`] for tenancy-scoped rationale.
7490 ///
7491 /// # Errors
7492 /// As [`Self::insert`].
7493 #[cfg(feature = "postgres")]
7494 pub async fn insert_on #executor_generics (
7495 &mut self,
7496 #executor_param,
7497 ) -> ::core::result::Result<(), #root::sql::ExecError>
7498 #executor_where
7499 {
7500 let mut _columns: ::std::vec::Vec<&'static str> =
7501 ::std::vec::Vec::new();
7502 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7503 ::std::vec::Vec::new();
7504 #( #pushes )*
7505 let query = #root::core::InsertQuery::new(
7506 <Self as #root::core::Model>::SCHEMA,
7507 _columns,
7508 _values,
7509 )
7510 .returning(::std::vec![ #( #returning_cols ),* ]);
7511 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7512 #executor_passes_to_data_write,
7513 &query,
7514 ).await?;
7515 let _returning_row = &_returning_row_v;
7516 #( #auto_assigns )*
7517 #audit_insert_emit
7518 ::core::result::Result::Ok(())
7519 }
7520
7521 /// Per-call audit-source override for [`Self::insert_on`].
7522 /// See [`Self::save_on_with`] for shape rationale.
7523 ///
7524 /// # Errors
7525 /// As [`Self::insert_on`].
7526 #[cfg(feature = "postgres")]
7527 pub async fn insert_on_with #executor_generics (
7528 &mut self,
7529 #executor_param,
7530 source: #root::audit::AuditSource,
7531 ) -> ::core::result::Result<(), #root::sql::ExecError>
7532 #executor_where
7533 {
7534 #root::audit::with_source(source, self.insert_on(_executor)).await
7535 }
7536 }
7537 } else {
7538 let insert_columns = &fields.insert_columns;
7539 let insert_values = &fields.insert_values;
7540 quote! {
7541 /// Insert this row into its table.
7542 ///
7543 /// # Errors
7544 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7545 /// driver failures.
7546 #[cfg(feature = "postgres")]
7547 pub async fn insert(
7548 &self,
7549 pool: &#root::sql::sqlx::PgPool,
7550 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7551 self.insert_on(pool).await
7552 }
7553
7554 /// Like [`Self::insert`] but accepts any sqlx executor.
7555 /// See [`Self::save_on`] for tenancy-scoped rationale.
7556 ///
7557 /// # Errors
7558 /// As [`Self::insert`].
7559 #[cfg(feature = "postgres")]
7560 pub async fn insert_on<'_c, _E>(
7561 &self,
7562 _executor: _E,
7563 ) -> ::core::result::Result<(), #root::sql::ExecError>
7564 where
7565 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7566 {
7567 let query = #root::core::InsertQuery::new(
7568 <Self as #root::core::Model>::SCHEMA,
7569 ::std::vec![ #( #insert_columns ),* ],
7570 ::std::vec![ #( #insert_values ),* ],
7571 );
7572 #root::sql::__macro_internals::insert_on(_executor, &query).await
7573 }
7574 }
7575 };
7576
7577 let bulk_insert_method = if fields.has_auto {
7578 let cols_no_auto = &fields.bulk_columns_no_auto;
7579 let cols_all = &fields.bulk_columns_all;
7580 let pushes_no_auto = &fields.bulk_pushes_no_auto;
7581 let pushes_all = &fields.bulk_pushes_all;
7582 let returning_cols = &fields.returning_cols;
7583 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7584 let uniformity = &fields.bulk_auto_uniformity;
7585 let fill_in_place = &fields.bulk_fill_in_place;
7586 // Nothing to read back when every Auto field is filled Rust-side (#1934).
7587 let read_back = if fields.returning_cols.is_empty() {
7588 quote!()
7589 } else {
7590 quote! {
7591 if _returned.len() != rows.len() {
7592 return ::core::result::Result::Err(
7593 #root::sql::ExecError::Sql(
7594 #root::sql::SqlError::BulkInsertReturningMismatch {
7595 expected: rows.len(),
7596 actual: _returned.len(),
7597 }
7598 )
7599 );
7600 }
7601 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7602 #auto_assigns_for_row
7603 }
7604 }
7605 };
7606 // The DB-filled `Auto` picks the path; Rust-filled ones fill per row (#1950).
7607 let first_auto_ident = fields
7608 .first_db_auto
7609 .as_ref()
7610 .map(|(ident, _)| ident)
7611 .or(fields.first_auto_ident.as_ref())
7612 .expect("has_auto implies first_auto_ident is Some");
7613 quote! {
7614 /// Bulk-insert `rows` in a single round-trip. Every row's
7615 /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7616 /// (sequence fills them in) or uniformly `Auto::Set(_)`
7617 /// (caller-supplied values). Mixed Set/Unset is rejected
7618 /// — call `insert` per row for that case.
7619 ///
7620 /// Empty slice is a no-op. Each row's `Auto` fields are
7621 /// populated from the `RETURNING` clause in input order
7622 /// before this returns.
7623 ///
7624 /// # Errors
7625 /// Returns [`#root::sql::ExecError`] for validation,
7626 /// SQL-writing, mixed-Auto rejection, or driver failures.
7627 #[cfg(feature = "postgres")]
7628 pub async fn bulk_insert(
7629 rows: &mut [Self],
7630 pool: &#root::sql::sqlx::PgPool,
7631 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7632 #pool_to_bulk_insert_on
7633 }
7634
7635 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7636 /// See [`Self::save_on`] for tenancy-scoped rationale.
7637 ///
7638 /// # Errors
7639 /// As [`Self::bulk_insert`].
7640 #[cfg(feature = "postgres")]
7641 pub async fn bulk_insert_on #executor_generics (
7642 rows: &mut [Self],
7643 #executor_param,
7644 ) -> ::core::result::Result<(), #root::sql::ExecError>
7645 #executor_where
7646 {
7647 if rows.is_empty() {
7648 return ::core::result::Result::Ok(());
7649 }
7650 let _first_unset = matches!(
7651 rows[0].#first_auto_ident,
7652 #root::sql::Auto::Unset
7653 );
7654 #( #uniformity )*
7655 for _row in rows.iter_mut() {
7656 #( #fill_in_place )*
7657 }
7658
7659 let mut _all_rows: ::std::vec::Vec<
7660 ::std::vec::Vec<#root::core::SqlValue>,
7661 > = ::std::vec::Vec::with_capacity(rows.len());
7662 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7663 for _row in rows.iter() {
7664 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7665 ::std::vec::Vec::new();
7666 #( #pushes_no_auto )*
7667 _all_rows.push(_row_vals);
7668 }
7669 ::std::vec![ #( #cols_no_auto ),* ]
7670 } else {
7671 for _row in rows.iter() {
7672 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7673 ::std::vec::Vec::new();
7674 #( #pushes_all )*
7675 _all_rows.push(_row_vals);
7676 }
7677 ::std::vec![ #( #cols_all ),* ]
7678 };
7679
7680 let _query = #root::core::BulkInsertQuery::new(
7681 <Self as #root::core::Model>::SCHEMA,
7682 _columns,
7683 _all_rows,
7684 )
7685 .returning(::std::vec![ #( #returning_cols ),* ]);
7686 let _returned = #root::sql::__macro_internals::bulk_insert_on(
7687 #executor_passes_to_data_write,
7688 &_query,
7689 ).await?;
7690 #read_back
7691 #audit_bulk_insert_emit
7692 ::core::result::Result::Ok(())
7693 }
7694 }
7695 } else {
7696 let cols_all = &fields.bulk_columns_all;
7697 let pushes_all = &fields.bulk_pushes_all;
7698 quote! {
7699 /// Bulk-insert `rows` in a single round-trip. Every row's
7700 /// fields are written verbatim — there are no `Auto<T>`
7701 /// fields on this model.
7702 ///
7703 /// Empty slice is a no-op.
7704 ///
7705 /// # Errors
7706 /// Returns [`#root::sql::ExecError`] for validation,
7707 /// SQL-writing, or driver failures.
7708 #[cfg(feature = "postgres")]
7709 pub async fn bulk_insert(
7710 rows: &[Self],
7711 pool: &#root::sql::sqlx::PgPool,
7712 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7713 #pool_to_bulk_insert_on
7714 }
7715
7716 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7717 /// See [`Self::save_on`] for tenancy-scoped rationale.
7718 ///
7719 /// # Errors
7720 /// As [`Self::bulk_insert`].
7721 #[cfg(feature = "postgres")]
7722 pub async fn bulk_insert_on #executor_generics (
7723 rows: &[Self],
7724 #executor_param,
7725 ) -> ::core::result::Result<(), #root::sql::ExecError>
7726 #executor_where
7727 {
7728 if rows.is_empty() {
7729 return ::core::result::Result::Ok(());
7730 }
7731 let mut _all_rows: ::std::vec::Vec<
7732 ::std::vec::Vec<#root::core::SqlValue>,
7733 > = ::std::vec::Vec::with_capacity(rows.len());
7734 for _row in rows.iter() {
7735 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7736 ::std::vec::Vec::new();
7737 #( #pushes_all )*
7738 _all_rows.push(_row_vals);
7739 }
7740 let _query = #root::core::BulkInsertQuery::new(
7741 <Self as #root::core::Model>::SCHEMA,
7742 ::std::vec![ #( #cols_all ),* ],
7743 _all_rows,
7744 );
7745 let _ = #root::sql::__macro_internals::bulk_insert_on(
7746 #executor_passes_to_data_write,
7747 &_query,
7748 ).await?;
7749 #audit_bulk_insert_emit
7750 ::core::result::Result::Ok(())
7751 }
7752 }
7753 };
7754
7755 // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7756 // (no postgres-feature gate); routes through the existing
7757 // `bulk_insert_pool` + per-dialect conflict writer.
7758 //
7759 // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7760 // sequence picks the PK for fresh inserts; the UPDATE branch never
7761 // touches the Auto column.
7762 // Audited models audit each written row; MySQL, with no RETURNING,
7763 // is refused there (#1747, #1795).
7764 let run_conflict_bulk_insert = match (audited_fields.is_some(), primary_key.is_some()) {
7765 (false, _) => quote!(#root::sql::bulk_insert_pool(pool, &_query).await),
7766 (true, true) => quote! {
7767 #root::audit::bulk_insert_with_audit::<Self>(pool, &_query, |_r: &Self, _op| {
7768 _r.__rustango_audit_entry(_op)
7769 })
7770 .await
7771 },
7772 (true, false) => quote! {
7773 ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
7774 table: <Self as #root::core::Model>::SCHEMA.table,
7775 })
7776 },
7777 };
7778 let bulk_upsert_pool_method = {
7779 let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
7780 fields.uuid_v7_fields.iter().cloned().unzip();
7781 // Pick the "no Auto" columns when the model has Auto fields,
7782 // else every column.
7783 let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7784 (
7785 fields.bulk_columns_no_auto.clone(),
7786 fields.bulk_pushes_no_auto.clone(),
7787 )
7788 } else {
7789 (
7790 fields.bulk_columns_all.clone(),
7791 fields.bulk_pushes_all.clone(),
7792 )
7793 };
7794 quote! {
7795 /// Tri-dialect bulk insert that updates on conflict — the
7796 /// "import a batch idempotently" shape. Issue #267 / T1.5.
7797 ///
7798 /// Per-row values are extracted and lowered into a
7799 /// [`#root::core::BulkInsertQuery`] with
7800 /// `on_conflict = DoUpdate { target, update_columns }`. The
7801 /// writer dispatches per-dialect:
7802 /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7803 /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)`; refused when
7804 /// `target` is not the only unique key, since MySQL matches every UNIQUE index
7805 ///
7806 /// `target` names the column(s) whose unique constraint
7807 /// defines the conflict (typically a `unique` or
7808 /// `unique_together` natural-key column, NOT the `Auto<T>`
7809 /// PK). `update_cols` names the columns to overwrite on
7810 /// conflict — every other column is left untouched on the
7811 /// existing row.
7812 ///
7813 /// Auto-PK rows must all have `Auto::Unset` (the sequence
7814 /// picks the PK on insert; the update path never touches
7815 /// the Auto column). Auto-set rows trigger a hard error.
7816 /// Empty slice is a no-op. On an audited model each written
7817 /// row gets a `create` or `update` audit row; MySQL refuses it.
7818 ///
7819 /// # Errors
7820 /// Returns [`#root::sql::ExecError`] for validation,
7821 /// SQL-writing, or driver failures.
7822 pub async fn bulk_upsert_pool(
7823 rows: &[Self],
7824 target: &[&'static str],
7825 update_cols: &[&'static str],
7826 pool: &#root::sql::Pool,
7827 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7828 if rows.is_empty() {
7829 return ::core::result::Result::Ok(());
7830 }
7831 let mut _all_rows: ::std::vec::Vec<
7832 ::std::vec::Vec<#root::core::SqlValue>,
7833 > = ::std::vec::Vec::with_capacity(rows.len());
7834 for _row in rows.iter() {
7835 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7836 ::std::vec::Vec::new();
7837 #( #upsert_pushes )*
7838 _all_rows.push(_row_vals);
7839 }
7840 #[allow(unused_mut)]
7841 let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7842 #(
7843 if rows.iter().all(|_r| matches!(&_r.#gen_idents, #root::sql::Auto::Unset)) {
7844 _generated.push(#gen_cols);
7845 }
7846 )*
7847 let _query = #root::core::BulkInsertQuery::new(
7848 <Self as #root::core::Model>::SCHEMA,
7849 ::std::vec![ #( #upsert_cols ),* ],
7850 _all_rows,
7851 )
7852 .on_conflict_do_update(target, update_cols)
7853 .generated(_generated);
7854 #run_conflict_bulk_insert
7855 }
7856
7857 /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7858 /// skip rows that would violate a unique constraint. Issue
7859 /// #267 / T1.5. Same per-dialect dispatch as
7860 /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7861 /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7862 /// <pivot> = <pivot>` (MySQL no-op write).
7863 ///
7864 /// # Errors
7865 /// As [`Self::bulk_upsert_pool`].
7866 pub async fn bulk_insert_or_ignore_pool(
7867 rows: &[Self],
7868 pool: &#root::sql::Pool,
7869 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7870 if rows.is_empty() {
7871 return ::core::result::Result::Ok(());
7872 }
7873 let mut _all_rows: ::std::vec::Vec<
7874 ::std::vec::Vec<#root::core::SqlValue>,
7875 > = ::std::vec::Vec::with_capacity(rows.len());
7876 for _row in rows.iter() {
7877 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7878 ::std::vec::Vec::new();
7879 #( #upsert_pushes )*
7880 _all_rows.push(_row_vals);
7881 }
7882 let _query = #root::core::BulkInsertQuery::new(
7883 <Self as #root::core::Model>::SCHEMA,
7884 ::std::vec![ #( #upsert_cols ),* ],
7885 _all_rows,
7886 )
7887 .on_conflict_do_nothing();
7888 #run_conflict_bulk_insert
7889 }
7890 }
7891 };
7892
7893 // Ergonomic `Model::bulk_update(objs, fields)`.
7894 // The SQL/IR/executor stack
7895 // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7896 // `write_bulk_update_*` writers) already existed; what was missing
7897 // was the per-model constructor that maps `&[Self]` + a runtime
7898 // column list into rows of `[pk, col_vals…]` so callers don't
7899 // hand-build the IR. Emitted only when the model has a primary key
7900 // (the PK is the join key and can't itself be updated).
7901 let bulk_update_rows_run = if audited_fields.is_some() {
7902 quote!(#root::audit::bulk_update_with_audit::<Self>(pool, &_query, |_r: &Self| {
7903 _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
7904 })
7905 .await)
7906 } else {
7907 quote!(#root::sql::bulk_update_pool(pool, &_query).await)
7908 };
7909 let bulk_update_method = match &fields.primary_key {
7910 None => quote! {},
7911 Some((pk_ident, pk_col)) => {
7912 // One pair of match arms per non-PK column: a validation arm
7913 // resolving the runtime name to its `&'static str` column,
7914 // and a value arm pushing that field off the row.
7915 let mut col_arms: Vec<TokenStream2> = Vec::new();
7916 let mut val_arms: Vec<TokenStream2> = Vec::new();
7917 for entry in &fields.column_entries {
7918 if &entry.column == pk_col {
7919 continue;
7920 }
7921 let col = &entry.column;
7922 let ident = &entry.ident;
7923 col_arms.push(quote! { #col => #col, });
7924 val_arms.push(quote! {
7925 #col => _row_vals.push(
7926 ::core::convert::Into::<#root::core::SqlValue>::into(
7927 ::core::clone::Clone::clone(&_o.#ident)
7928 )
7929 ),
7930 });
7931 }
7932 quote! {
7933 /// Write per-row-different values for the named `fields`
7934 /// across every object in `objs`, matched by primary key.
7935 /// Past the backend's bind cap the rows go in batches inside
7936 /// one transaction, so the update is all or none.
7937 ///
7938 /// `fields` names the **columns** to update. The primary
7939 /// key identifies each row and cannot itself be updated
7940 /// (pass it and you get
7941 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7942 /// Empty `objs` or `fields` is a no-op returning `0`.
7943 /// Objects whose PK matches no row are simply not updated.
7944 /// Returns the number of rows affected.
7945 ///
7946 /// Tri-dialect: lowers to a
7947 /// [`#root::core::BulkUpdateQuery`] and dispatches
7948 /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7949 /// a CTE + correlated subquery on SQLite, an inner
7950 /// `JOIN (VALUES …)` on MySQL.
7951 ///
7952 /// # Errors
7953 /// [`#root::core::QueryError::UnknownField`] for a name
7954 /// that isn't a column on this model,
7955 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7956 /// `fields` names the PK, or [`#root::sql::ExecError`] for
7957 /// SQL-writing / driver failures.
7958 pub async fn bulk_update(
7959 objs: &[Self],
7960 fields: &[&str],
7961 pool: &#root::sql::Pool,
7962 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7963 if objs.is_empty() || fields.is_empty() {
7964 return ::core::result::Result::Ok(0);
7965 }
7966 let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7967 let mut _update_columns: ::std::vec::Vec<&'static str> =
7968 ::std::vec::Vec::with_capacity(fields.len());
7969 for &_f in fields {
7970 let _col: &'static str = match _f {
7971 #pk_col => {
7972 return ::core::result::Result::Err(
7973 ::core::convert::Into::into(
7974 #root::core::QueryError::BulkUpdatePrimaryKey {
7975 model: _model_name,
7976 field: ::std::string::ToString::to_string(_f),
7977 }
7978 )
7979 );
7980 }
7981 #( #col_arms )*
7982 _ => {
7983 return ::core::result::Result::Err(
7984 ::core::convert::Into::into(
7985 #root::core::QueryError::UnknownField {
7986 model: _model_name,
7987 field: ::std::string::ToString::to_string(_f),
7988 }
7989 )
7990 );
7991 }
7992 };
7993 // A model whose only field is the primary key emits no
7994 // non-PK arms, so every arm above diverges and this push
7995 // is genuinely unreachable — correct, since such a model
7996 // has nothing to bulk-update. Scoped to the statement
7997 // rather than the fn so a real unreachability elsewhere
7998 // in this body still warns. Without it every PK-only
7999 // model warns in the *user's* crate, which they cannot
8000 // act on (#1290).
8001 #[allow(unreachable_code)]
8002 _update_columns.push(_col);
8003 }
8004 let mut _rows: ::std::vec::Vec<
8005 ::std::vec::Vec<#root::core::SqlValue>,
8006 > = ::std::vec::Vec::with_capacity(objs.len());
8007 for _o in objs.iter() {
8008 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
8009 ::std::vec::Vec::with_capacity(fields.len() + 1);
8010 // PK first — the writers expect `[pk, …update cols]`.
8011 _row_vals.push(
8012 ::core::convert::Into::<#root::core::SqlValue>::into(
8013 ::core::clone::Clone::clone(&_o.#pk_ident)
8014 )
8015 );
8016 for &_f in fields {
8017 match _f {
8018 #( #val_arms )*
8019 // Unreachable: every name was validated
8020 // against the same arm set above.
8021 _ => {}
8022 }
8023 }
8024 _rows.push(_row_vals);
8025 }
8026 let _query = #root::core::BulkUpdateQuery::new(
8027 <Self as #root::core::Model>::SCHEMA,
8028 _update_columns,
8029 _rows,
8030 );
8031 #bulk_update_rows_run
8032 }
8033 }
8034 }
8035 };
8036
8037 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
8038 quote! {
8039 /// Hidden runtime accessor for the primary-key value as a
8040 /// [`SqlValue`]. Used by reverse-relation helpers
8041 /// (`<parent>::<child>_set`) emitted from sibling models'
8042 /// FK fields. Not part of the public API.
8043 #[doc(hidden)]
8044 pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
8045 ::core::convert::Into::<#root::core::SqlValue>::into(
8046 ::core::clone::Clone::clone(&self.#pk_ident)
8047 )
8048 }
8049 }
8050 });
8051
8052 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
8053 let column_arms = fields.column_entries.iter().map(|entry| {
8054 let col = &entry.column;
8055 let ident = &entry.ident;
8056 quote! {
8057 #col => ::core::option::Option::Some(
8058 ::core::convert::Into::<#root::core::SqlValue>::into(
8059 ::core::clone::Clone::clone(&self.#ident)
8060 )
8061 ),
8062 }
8063 });
8064 quote! {
8065 impl #root::sql::HasPkValue for #struct_name {
8066 fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
8067 ::core::convert::Into::<#root::core::SqlValue>::into(
8068 ::core::clone::Clone::clone(&self.#pk_ident)
8069 )
8070 }
8071 fn __rustango_column_value(
8072 &self,
8073 column: &str,
8074 ) -> ::core::option::Option<#root::core::SqlValue> {
8075 match column {
8076 #( #column_arms )*
8077 _ => ::core::option::Option::None,
8078 }
8079 }
8080 }
8081 }
8082 });
8083
8084 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
8085
8086 // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
8087 // dispatch to the right per-backend body without the macro emitting
8088 // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
8089 // so audited models with non-Auto PKs (which still go through
8090 // `insert_one_with_audit` → `apply_auto_pk`) link.
8091 let assign_auto_pk_pool_impl = {
8092 let auto_assigns = &fields.auto_assigns;
8093 // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
8094 // so the body is structurally identical to `auto_assigns` —
8095 // only the helper name swaps from `try_get_returning` to
8096 // `try_get_returning_sqlite` so the closure typechecks against
8097 // a `SqliteRow` instead of a `PgRow`.
8098 let auto_assigns_sqlite: Vec<TokenStream2> = fields
8099 .auto_field_idents
8100 .iter()
8101 .map(|(ident, column, uuid)| {
8102 // A UUID a column default wrote is TEXT, not the 16 bytes sqlx binds.
8103 if *uuid {
8104 return quote! {
8105 self.#ident = #root::sql::Auto::Set(#root::sql::try_get_flat_sqlite(
8106 _returning_row, #column
8107 )?);
8108 };
8109 }
8110 quote! {
8111 self.#ident = #root::sql::try_get_returning_sqlite(
8112 _returning_row, #column
8113 )?;
8114 }
8115 })
8116 .collect();
8117 // #1028 — decode each `generated_as` column from the same
8118 // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
8119 // own type drives the decode (no `Auto<T>` wrapper).
8120 let generated_assigns: Vec<TokenStream2> = fields
8121 .generated_field_idents
8122 .iter()
8123 .map(|(ident, column)| {
8124 quote! {
8125 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
8126 }
8127 })
8128 .collect();
8129 let generated_assigns_sqlite: Vec<TokenStream2> = fields
8130 .generated_field_idents
8131 .iter()
8132 .map(|(ident, column)| {
8133 quote! {
8134 self.#ident = #root::sql::try_get_returning_sqlite(
8135 _returning_row, #column
8136 )?;
8137 }
8138 })
8139 .collect();
8140 let mysql_body = if let Some((first, value_ty)) = fields.first_db_auto.as_ref() {
8141 // The MySQL `LAST_INSERT_ID()` is always i64. Route through
8142 // `MysqlAutoIdSet` so Auto<i32> narrows safely and
8143 // Auto<Uuid>/etc. fail to link against MySQL (intended —
8144 // those models can't use AUTO_INCREMENT). The trait is only
8145 // touched on the MySQL arm at runtime, so PG-only consumers
8146 // never see the bound failure.
8147 //
8148 // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
8149 // Auto<i64> PK + auto_now_add timestamp) errored hard at
8150 // runtime with "multi-column RETURNING". MySQL has no
8151 // multi-column RETURNING semantic and a follow-up SELECT
8152 // would need cross-trait plumbing. Pragmatic shape: succeed
8153 // with the FIRST Auto field populated from LAST_INSERT_ID();
8154 // any other Auto fields stay `Auto::Unset`. Callers that
8155 // need the DB-defaulted timestamp / UUID can re-fetch the
8156 // row by PK after `save_pool`. Fixes the cookbook chapter
8157 // 12 dialect divergence.
8158 quote! {
8159 let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
8160 ::rustango_from_mysql_auto_id(_id)?;
8161 self.#first = #root::sql::Auto::Set(_converted);
8162 ::core::result::Result::Ok(())
8163 }
8164 } else {
8165 quote! {
8166 let _ = _id;
8167 ::core::result::Result::Ok(())
8168 }
8169 };
8170 quote! {
8171 impl #root::sql::AssignAutoPkPool for #struct_name {
8172 fn __rustango_assign_from_pg_row(
8173 &mut self,
8174 _returning_row: &#root::sql::PgReturningRow,
8175 ) -> ::core::result::Result<(), #root::sql::ExecError> {
8176 #( #auto_assigns )*
8177 #( #generated_assigns )*
8178 ::core::result::Result::Ok(())
8179 }
8180 fn __rustango_assign_from_mysql_id(
8181 &mut self,
8182 _id: i64,
8183 ) -> ::core::result::Result<(), #root::sql::ExecError> {
8184 #mysql_body
8185 }
8186 fn __rustango_assign_from_sqlite_row(
8187 &mut self,
8188 _returning_row: &#root::sql::SqliteReturningRow,
8189 ) -> ::core::result::Result<(), #root::sql::ExecError> {
8190 #( #auto_assigns_sqlite )*
8191 #( #generated_assigns_sqlite )*
8192 ::core::result::Result::Ok(())
8193 }
8194 }
8195 }
8196 };
8197
8198 let from_aliased_row_inits = &fields.from_aliased_row_inits;
8199 let aliased_row_helper = quote! {
8200 /// Decode a row's aliased target columns (produced by
8201 /// `select_related`'s LEFT JOIN) into a fresh instance of
8202 /// this model. Reads each column via
8203 /// `format!("{prefix}__{col}")`, matching the alias the
8204 /// SELECT writer emitted. Slice 9.0d.
8205 #[doc(hidden)]
8206 #[cfg(feature = "postgres")]
8207 pub fn __rustango_from_aliased_row(
8208 row: &#root::sql::sqlx::postgres::PgRow,
8209 prefix: &str,
8210 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8211 ::core::result::Result::Ok(Self {
8212 #( #from_aliased_row_inits ),*
8213 })
8214 }
8215 };
8216 // v0.23.0-batch8 — MySQL counterpart, gated through the
8217 // cfg-aware macro_rules so PG-only builds expand to nothing.
8218 let from_aliased_row_inits_my = &fields.from_aliased_row_inits_my;
8219 let aliased_row_helper_my = quote! {
8220 #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
8221 #( #from_aliased_row_inits_my ),*
8222 });
8223 };
8224
8225 // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
8226 // pattern + cfg gate on the `sqlite` feature.
8227 let aliased_row_helper_sqlite = quote! {
8228 #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
8229 #( #from_aliased_row_inits ),*
8230 });
8231 };
8232
8233 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
8234 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
8235 let load_related_impl_sqlite =
8236 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
8237
8238 // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
8239 // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
8240 // default `Self::objects()`. Each accessor returns a fresh
8241 // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
8242 // methods the user defined.
8243 let extra_manager_fns: Vec<TokenStream2> = manager_fns
8244 .iter()
8245 .map(|fn_ident| {
8246 let model_name_str = struct_name.to_string();
8247 let fn_name_str = fn_ident.to_string();
8248 let doc = format!(
8249 "Custom-named QuerySet accessor for [`{model_name_str}`]. \
8250 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
8251 equivalent to `Self::objects()`. Chains with any \
8252 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
8253 extension methods."
8254 );
8255 quote! {
8256 #[doc = #doc]
8257 #[must_use]
8258 pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
8259 #root::query::QuerySet::new()
8260 }
8261 }
8262 })
8263 .collect();
8264
8265 quote! {
8266 impl #struct_name {
8267 /// Start a new `QuerySet` over this model.
8268 #[must_use]
8269 pub fn objects() -> #root::query::QuerySet<#struct_name> {
8270 #root::query::QuerySet::new()
8271 }
8272
8273 /// Eloquent-shape alias of [`Self::objects`]. Returns
8274 /// a fresh `QuerySet<Self>` ready for `.filter()` /
8275 /// `.where_()` / etc. Matches Laravel muscle-memory:
8276 ///
8277 /// ```ignore
8278 /// // Eloquent: Post::query()->where('published', true)
8279 /// // rustango: Post::query().filter("published", true)
8280 /// // or: Post::objects().filter("published", true)
8281 /// ```
8282 ///
8283 /// Both names point at the same underlying constructor;
8284 /// neither is preferred.
8285 #[must_use]
8286 pub fn query() -> #root::query::QuerySet<#struct_name> {
8287 #root::query::QuerySet::new()
8288 }
8289
8290 #( #extra_manager_fns )*
8291
8292 #insert_method
8293
8294 #bulk_insert_method
8295
8296 #bulk_upsert_pool_method
8297
8298 #bulk_update_method
8299
8300 #save_method
8301
8302 #pk_methods
8303
8304 #pk_value_helper
8305
8306 #aliased_row_helper
8307
8308 #column_consts
8309 }
8310
8311 #aliased_row_helper_my
8312
8313 #aliased_row_helper_sqlite
8314
8315 #load_related_impl
8316
8317 #load_related_impl_my
8318
8319 #load_related_impl_sqlite
8320
8321 #has_pk_value_impl
8322
8323 #fk_pk_access_impl
8324
8325 #assign_auto_pk_pool_impl
8326 }
8327}
8328
8329/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
8330/// `auto_assigns` but reading from `_returning_row` and writing to
8331/// `_row_mut` instead of `self`.
8332fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
8333 let root = rustango_root();
8334 let lines = fields.auto_field_idents.iter().map(|(ident, column, _)| {
8335 let col_lit = column.as_str();
8336 quote! {
8337 _row_mut.#ident = #root::sql::sqlx::Row::try_get(
8338 _returning_row,
8339 #col_lit,
8340 )?;
8341 }
8342 });
8343 quote! { #( #lines )* }
8344}
8345
8346/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
8347fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
8348 let lines = entries.iter().map(|e| {
8349 let ident = &e.ident;
8350 let col_ty = column_type_ident(ident);
8351 quote! {
8352 #[allow(non_upper_case_globals)]
8353 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
8354 }
8355 });
8356 quote! { #(#lines)* }
8357}
8358
8359/// Emit a hidden per-model module carrying one zero-sized type per field,
8360/// each with a `Column` impl pointing back at the model.
8361fn column_module_tokens(
8362 module_ident: &syn::Ident,
8363 struct_name: &syn::Ident,
8364 entries: &[ColumnEntry],
8365) -> TokenStream2 {
8366 let root = rustango_root();
8367 let items = entries.iter().map(|e| {
8368 let col_ty = column_type_ident(&e.ident);
8369 let value_ty = &e.value_ty;
8370 let name = &e.name;
8371 let column = &e.column;
8372 let field_type_tokens = &e.field_type_tokens;
8373 quote! {
8374 #[derive(::core::clone::Clone, ::core::marker::Copy)]
8375 pub struct #col_ty;
8376
8377 impl #root::core::Column for #col_ty {
8378 type Model = super::#struct_name;
8379 type Value = #value_ty;
8380 const NAME: &'static str = #name;
8381 const COLUMN: &'static str = #column;
8382 const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
8383 }
8384 }
8385 });
8386 quote! {
8387 #[doc(hidden)]
8388 #[allow(non_camel_case_types, non_snake_case)]
8389 pub mod #module_ident {
8390 // Re-import the parent scope so field types referencing
8391 // sibling models (e.g. `ForeignKey<Author>`) resolve
8392 // inside this submodule. Without this we'd hit
8393 // `proc_macro_derive_resolution_fallback` warnings.
8394 #[allow(unused_imports)]
8395 use super::*;
8396 #(#items)*
8397 }
8398 }
8399}
8400
8401fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
8402 let name = ident_name(field_ident);
8403 syn::Ident::new(&format!("{name}_col"), field_ident.span())
8404}
8405
8406/// Run `body` (of type `ret`) and put the `auto_now` fields back unless
8407/// `keep` holds for `_saved`, so a failed or no-row write keeps the old stamp.
8408fn restore_auto_now_unless(
8409 idents: &[syn::Ident],
8410 ret: &TokenStream2,
8411 keep: &TokenStream2,
8412 body: &TokenStream2,
8413) -> TokenStream2 {
8414 if idents.is_empty() {
8415 return body.clone();
8416 }
8417 let prev: Vec<syn::Ident> = (0..idents.len())
8418 .map(|i| quote::format_ident!("__rustango_auto_now_prev_{}", i))
8419 .collect();
8420 quote! {
8421 #( let #prev = ::core::clone::Clone::clone(&self.#idents); )*
8422 let _saved: #ret = async { #body }.await;
8423 if !(#keep) {
8424 #( self.#idents = #prev; )*
8425 }
8426 _saved
8427 }
8428}
8429
8430/// A field's name without the `r#` of a raw ident: `r#type` is `type` (#1937).
8431fn ident_name(ident: &syn::Ident) -> String {
8432 syn::ext::IdentExt::unraw(ident).to_string()
8433}
8434
8435/// The ident for a field named `name`, raw when it is a keyword (`type` → `r#type`).
8436fn ident_from_name(name: &str, span: proc_macro2::Span) -> syn::Ident {
8437 match syn::parse_str::<syn::Ident>(name) {
8438 Ok(mut ident) => {
8439 ident.set_span(span);
8440 ident
8441 }
8442 Err(_) => syn::Ident::new_raw(name, span),
8443 }
8444}
8445
8446fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8447 syn::Ident::new(
8448 &format!("__rustango_cols_{struct_name}"),
8449 struct_name.span(),
8450 )
8451}
8452
8453fn from_row_impl_tokens(
8454 struct_name: &syn::Ident,
8455 from_row_inits: &[TokenStream2],
8456 from_row_inits_my: &[TokenStream2],
8457) -> TokenStream2 {
8458 let root = rustango_root();
8459 // The Postgres impl is always emitted — every rustango build pulls in
8460 // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8461 // routed through `#root::__impl_my_from_row!`, a cfg-gated
8462 // macro_rules whose body collapses to nothing when rustango is built
8463 // without the `mysql` feature. No user-facing feature shim required.
8464 //
8465 // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8466 // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8467 // back into a comma-separated list inside square brackets. Since each
8468 // entry is already in `field: expr` shape, the existing tokens slot in.
8469 quote! {
8470 #[cfg(feature = "postgres")]
8471 impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8472 for #struct_name
8473 {
8474 fn from_row(
8475 row: &'r #root::sql::sqlx::postgres::PgRow,
8476 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8477 ::core::result::Result::Ok(Self {
8478 #( #from_row_inits ),*
8479 })
8480 }
8481 }
8482
8483 #root::__impl_my_from_row!(#struct_name, |row| {
8484 #( #from_row_inits_my ),*
8485 });
8486
8487 #root::__impl_sqlite_from_row!(#struct_name, |row| {
8488 #( #from_row_inits ),*
8489 });
8490 }
8491}
8492
8493struct ContainerAttrs {
8494 table: Option<String>,
8495 display: Option<(String, proc_macro2::Span)>,
8496 /// Explicit app label from `#[rustango(app = "blog")]`.
8497 /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8498 /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8499 /// at runtime — this attribute is the override for cases where
8500 /// the inference is wrong (e.g. a model that conceptually belongs
8501 /// to one app but is physically in another module).
8502 app: Option<String>,
8503 /// Per-model admin knobs from
8504 /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8505 /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8506 /// admin code falls back to `AdminConfig::DEFAULT`.
8507 admin: Option<AdminAttrs>,
8508 /// Per-model audit configuration from `#[rustango(audit(...))]`.
8509 /// `None` when the model isn't audited — write paths emit no
8510 /// audit entries. When present, single-row writes capture
8511 /// before/after for the listed fields and bulk writes batch
8512 /// snapshots into one INSERT into `rustango_audit_log`.
8513 audit: Option<AuditAttrs>,
8514 /// `true` when `#[rustango(permissions)]` is present. Signals that
8515 /// `auto_create_permissions` should seed the four CRUD codenames for
8516 /// this model.
8517 permissions: bool,
8518 /// Many-to-many relations declared via
8519 /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8520 /// src = "post_id", dst = "tag_id"))]`.
8521 m2m: Vec<M2MAttr>,
8522 /// Polymorphic M2M relations declared via
8523 /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8524 /// pk_column = "taggable_id", ct_column = "taggable_type",
8525 /// related_column = "tag_id"))]` (issue #818).
8526 generic_m2m: Vec<GenericM2MAttr>,
8527 /// Composite indexes declared via
8528 /// `#[rustango(index("col1, col2"))]` or
8529 /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8530 /// Single-column indexes from `#[rustango(index)]` on fields are
8531 /// accumulated here during field collection.
8532 indexes: Vec<IndexAttr>,
8533 /// Table-level CHECK constraints declared via
8534 /// `#[rustango(check(name = "…", expr = "…"))]`.
8535 checks: Vec<CheckAttr>,
8536 /// Table-level PG `EXCLUDE` constraints declared via
8537 /// `#[rustango(exclude(name = "…", using = "gist", elements =
8538 /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8539 /// migration writer renders them on Postgres and skips with a
8540 /// warning on MySQL/SQLite. Issue #319.
8541 excludes: Vec<ExcludeAttr>,
8542 /// Composite (multi-column) FKs declared via
8543 /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8544 /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8545 composite_fks: Vec<CompositeFkAttr>,
8546 /// Generic ("any model") FKs declared via
8547 /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8548 /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8549 generic_fks: Vec<GenericFkAttr>,
8550 /// Where this model lives in a tenancy deployment, declared via
8551 /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8552 /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8553 /// to partition diff output between registry-scoped and
8554 /// tenant-scoped migration files.
8555 scope: Option<String>,
8556 /// Extra QuerySet accessor names from
8557 /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8558 /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8559 /// the default `Self::objects()`. Multiple attributes allowed.
8560 manager_fns: Vec<syn::Ident>,
8561 /// Default ordering declared via `#[rustango(default_order =
8562 /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8563 /// `(column_name, desc_flag, span_for_error_reporting)` — the
8564 /// `-` prefix means descending; the `+` prefix or no prefix means
8565 /// ascending.
8566 default_order: Vec<(String, bool, proc_macro2::Span)>,
8567 /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8568 /// Routes the emitted schema's `is_view = true` so the migration
8569 /// snapshot skips this model (its underlying SQL view is operator-
8570 /// managed, not rustango-managed).
8571 is_view: bool,
8572 /// `#[rustango(managed = false)]` — who owns the table.
8573 /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8574 /// the migration snapshot skips this model so `makemigrations` /
8575 /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8576 /// TABLE` against it (operator-managed schema).
8577 managed: bool,
8578 /// Name of the model's base manager, from
8579 /// `#[rustango(base_manager_name = "...")]`. Threaded into
8580 /// `ModelSchema::base_manager_name`. Declarative-only today.
8581 base_manager_name: Option<String>,
8582 /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8583 /// field this model's instances are ordered relative to.
8584 /// Declarative-only today; threaded onto
8585 /// `ModelSchema::order_with_respect_to`.
8586 order_with_respect_to: Option<String>,
8587 /// `#[rustango(proxy)]` / `#[rustango(proxy = true)]`.
8588 /// Marks the model as a proxy that
8589 /// shares its DB table with another struct. Threaded into
8590 /// `ModelSchema::proxy` so future codegen can skip table-owning
8591 /// behavior for proxies.
8592 proxy: bool,
8593 /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8594 /// Each comma-separated capability token surfaces on
8595 /// `ModelSchema::required_db_features` so `manage check --deploy`
8596 /// can warn when the active dialect lacks one.
8597 required_db_features: Vec<String>,
8598 /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8599 /// Normalized to the dialect name `manage check --deploy`
8600 /// compares against `Settings.database.backend`. Aliases
8601 /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8602 /// stored under the canonical name.
8603 required_db_vendor: Option<String>,
8604 /// `#[rustango(default_related_name = "...")]`. Threaded into
8605 /// `ModelSchema::default_related_name`. Reverse-relation accessor
8606 /// name to use when an FK / M2M field doesn't override it.
8607 /// Today rustango doesn't auto-emit reverse managers; the
8608 /// metadata is the foundation for that work.
8609 default_related_name: Option<String>,
8610 /// `#[rustango(db_table_comment = "...")]`. Threaded into
8611 /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8612 /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8613 /// inline `COMMENT='...'`, SQLite: no-op).
8614 db_table_comment: Option<String>,
8615 /// `#[rustango(get_latest_by = "created_at")]` /
8616 /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8617 /// `(column, descending)` where `descending = true` when the
8618 /// attribute value starts with `-`. Threaded into
8619 /// `ModelSchema::get_latest_by`.
8620 get_latest_by: Option<(String, bool, proc_macro2::Span)>,
8621 /// Extra `(codename, label)` permissions
8622 /// from `#[rustango(extra_permissions = "approve:Can approve,
8623 /// archive:Can archive")]`. Comma-separated `codename:label`
8624 /// pairs. Threaded into `ModelSchema::extra_permissions`.
8625 extra_permissions: Vec<(String, String)>,
8626 /// Which CRUD codenames (`"add"` / `"change"` / `"delete"` /
8627 /// `"view"`) the framework
8628 /// auto-creates. Empty `Vec` (default) means **all four**. Set via
8629 /// `#[rustango(default_permissions = "view,change")]` to opt out.
8630 /// Validated at parse time; unknown actions fail with a span-pointing
8631 /// error.
8632 default_permissions: Vec<String>,
8633 /// `#[rustango(verbose_name = "blog post")]` — the
8634 /// human-readable singular label for the model. Threaded into
8635 /// `ModelSchema::verbose_name` so admin section headers /
8636 /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8637 /// over the Rust struct identifier.
8638 verbose_name: Option<String>,
8639 /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8640 /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8641 /// When unset, `display_label_plural()` auto-suffixes `s`.
8642 verbose_name_plural: Option<String>,
8643 /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8644 /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8645 /// a name (used by `QuerySet::without_global_scope`) with a
8646 /// `fn() -> WhereExpr` path that the macro emits into
8647 /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8648 global_scopes: Vec<GlobalScopeAttr>,
8649 /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8650 /// `#[rustango(through(name, far, far_fk_column, intermediate,
8651 /// intermediate_fk_column, intermediate_pk_column))]` — issue
8652 /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8653 /// entry emits an inherent `<name>_through(&self)` accessor that
8654 /// returns a `QuerySet<Far>` filtered via a correlated subquery
8655 /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8656 /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8657 through_relations: Vec<ThroughAttr>,
8658 /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8659 /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8660 /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8661 /// Each entry emits two associated functions on the parent —
8662 /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8663 /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8664 /// over a correlated subquery against the child table. Users
8665 /// drop the result into `QuerySet::where_raw(...)`.
8666 reverse_has_relations: Vec<ReverseHasAttr>,
8667 /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8668 /// issue #830. Each emits a `Model::generic_reverse_relations()`
8669 /// entry so the relation-existence family resolves polymorphic
8670 /// children by name.
8671 generic_has_relations: Vec<GenericHasAttr>,
8672}
8673
8674/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8675/// declaration. Each entry becomes one `core::GlobalScope` in the
8676/// emitted schema literal; `apply` resolves at macro-expand time
8677/// against the consumer's scope so the function must be in scope at
8678/// the use site. Issue #820.
8679struct GlobalScopeAttr {
8680 name: String,
8681 apply: syn::Path,
8682}
8683
8684/// Parsed `#[rustango(through(...))]` declaration. Issue
8685/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8686/// `hasManyThrough` / `hasOneThrough` parity.
8687///
8688/// `Country hasManyThrough Post via User` declares as:
8689///
8690/// ```ignore
8691/// #[rustango(through(
8692/// name = "posts",
8693/// far = "Post",
8694/// far_fk_column = "author_id",
8695/// intermediate = "User",
8696/// intermediate_fk_column = "country_id",
8697/// ))]
8698/// struct Country { ... }
8699/// ```
8700///
8701/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8702/// which returns a queryset filtered via a correlated subquery —
8703/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8704/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8705/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8706/// because the subquery lives inside a `WhereExpr::InSubquery` node
8707/// and the outer queryset's pending list stays empty.
8708///
8709/// All four required arguments use **SQL column / table names**
8710/// (not Rust field names) to sidestep the multi-hop-filter substrate
8711/// gap. Once that substrate lands, a higher-level Rust-field-name
8712/// shorthand can be added without breaking this surface.
8713struct ThroughAttr {
8714 /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8715 name: String,
8716 /// Far model type identifier. `far = "Post"` → returns
8717 /// `QuerySet<Post>`. Resolved verbatim against the scope where
8718 /// the derive expands.
8719 far: syn::Ident,
8720 /// SQL column on the far model's table that references the
8721 /// intermediate model's primary key. For `Post`'s
8722 /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8723 /// default `<field>_id` convention) or whatever the user
8724 /// declared via `#[rustango(db_column = "...")]`.
8725 far_fk_column: String,
8726 /// Intermediate model type identifier. Needed to look up its
8727 /// `SCHEMA` so the subquery's `FROM` clause points at the
8728 /// intermediate table. `intermediate = "User"`.
8729 intermediate: syn::Ident,
8730 /// SQL column on the intermediate's table that references the
8731 /// source (this) model's primary key. For `User`'s
8732 /// `country: ForeignKey<Country>` the column is `"country_id"`.
8733 intermediate_fk_column: String,
8734 /// SQL primary-key column on the intermediate's table — the
8735 /// column the subquery projects. Optional; defaults to `"id"`
8736 /// (rustango's default PK column name). Override when the
8737 /// intermediate declares a custom PK column.
8738 intermediate_pk_column: String,
8739}
8740
8741/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8742/// child_fk_column = "..."))]` declaration. Issue
8743/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8744/// `whereHas` / `whereDoesntHave` parity.
8745///
8746/// `Post hasMany Comment` declares as:
8747///
8748/// ```ignore
8749/// #[rustango(reverse_has(
8750/// name = "comments",
8751/// child = "Comment",
8752/// child_fk_column = "post_id",
8753/// ))]
8754/// struct Post { ... }
8755/// ```
8756///
8757/// The macro emits two associated functions on `Post`:
8758///
8759/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8760/// `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8761/// <outer>.<self_pk_column>)`.
8762/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8763/// but `NOT EXISTS`, the `whereDoesntHave` analog.
8764///
8765/// User code:
8766///
8767/// ```ignore
8768/// // Posts with at least one comment:
8769/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8770/// // Posts with no comments:
8771/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8772/// ```
8773///
8774/// As with #817, all column / table identifiers are **SQL names**
8775/// (not Rust field names) so the substrate is independent of the
8776/// outstanding multi-hop filter resolver gap. The emitted
8777/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8778/// SQL-emit time via the writer's scope stack.
8779struct ReverseHasAttr {
8780 /// Accessor name. `name = "comments"` → emits
8781 /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8782 name: String,
8783 /// Child model type identifier. `child = "Comment"` — needed to
8784 /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8785 /// points at the child table.
8786 child: syn::Ident,
8787 /// SQL column on the child's table that references this model's
8788 /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8789 /// column is `"post_id"`.
8790 child_fk_column: String,
8791 /// Column on **this** model's table the child FK references — the
8792 /// `OuterRef` target. Defaults to the PK column (`"id"` without one).
8793 self_pk_column: Option<String>,
8794}
8795
8796impl ReverseHasAttr {
8797 fn self_pk_column(&self) -> &str {
8798 self.self_pk_column.as_deref().unwrap_or("id")
8799 }
8800}
8801
8802/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8803/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8804/// relation-existence family (issue #830). The child is a polymorphic,
8805/// content-type-discriminated model; emits a `Model::
8806/// generic_reverse_relations()` entry the queryset resolves by name.
8807struct GenericHasAttr {
8808 /// Accessor name, e.g. `name = "tags"`.
8809 name: String,
8810 /// Child model type identifier (`child = "Tag"`) — looked up for its
8811 /// `SCHEMA` so the subquery's `FROM` points at the child table.
8812 child: syn::Ident,
8813 /// Column on the child table holding the parent's content-type id.
8814 /// Optional; defaults to `"content_type_id"`.
8815 ct_column: String,
8816 /// Column on the child table holding the parent's PK value.
8817 /// Optional; defaults to `"object_pk"`.
8818 pk_column: String,
8819 /// SQL primary-key column on **this** (parent) model's table.
8820 /// Defaults to the PK column (`"id"` without one).
8821 self_pk_column: Option<String>,
8822}
8823
8824impl GenericHasAttr {
8825 fn self_pk_column(&self) -> &str {
8826 self.self_pk_column.as_deref().unwrap_or("id")
8827 }
8828}
8829
8830/// Parsed form of one index declaration (field-level or container-level).
8831struct IndexAttr {
8832 /// The declaring attribute, for diagnostics.
8833 span: proc_macro2::Span,
8834 /// Index name; auto-derived when `None` at parse time.
8835 name: Option<String>,
8836 /// Column names in the index.
8837 columns: Vec<String>,
8838 /// `true` for `CREATE UNIQUE INDEX`.
8839 unique: bool,
8840 /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8841 /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8842 /// `"btree"` when the attribute is absent — the DDL writer omits
8843 /// the `USING` clause and the backend uses its own default
8844 /// (btree on every supported dialect).
8845 method: String,
8846 /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8847 /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8848 /// condition = "...", name = "..."))]`. `None` for plain indexes.
8849 where_clause: Option<String>,
8850 /// Covering-index columns (PG 11+ `INCLUDE (...)` clause).
8851 /// Empty `Vec` (the default) means "no covering columns".
8852 include: Vec<String>,
8853}
8854
8855/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8856struct CheckAttr {
8857 name: String,
8858 expr: String,
8859}
8860
8861/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8862/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8863/// PG-only — surfaced on every backend in the macro emit; the migration
8864/// writer skips the op on MySQL/SQLite. Issue #319.
8865struct ExcludeAttr {
8866 /// Constraint name (free-form Rust identifier).
8867 name: String,
8868 /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8869 using: String,
8870 /// Comma-separated `(column, operator)` pairs, in declaration
8871 /// order. Parsed from `"col WITH op, col WITH op"`.
8872 elements: Vec<(String, String)>,
8873 /// Optional `WHERE` predicate (raw SQL).
8874 where_clause: Option<String>,
8875}
8876
8877/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8878/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8879/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8880/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8881/// the model, not the field.
8882struct CompositeFkAttr {
8883 /// Logical relation name (free-form Rust identifier).
8884 name: String,
8885 /// SQL table name of the target.
8886 to: String,
8887 /// Source-side column names, in declaration order.
8888 from: Vec<String>,
8889 /// Target-side column names, same length / order as `from`.
8890 on: Vec<String>,
8891}
8892
8893/// Parsed form of one `#[rustango(generic_fk(name = "target",
8894/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8895/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8896/// generic ("any model") FKs live on the model, not the field.
8897struct GenericFkAttr {
8898 /// Logical relation name (free-form Rust identifier).
8899 name: String,
8900 /// Source-side column carrying the `content_type_id` value.
8901 ct_column: String,
8902 /// Source-side column carrying the target row's primary key.
8903 pk_column: String,
8904}
8905
8906/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8907struct M2MAttr {
8908 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8909 name: String,
8910 /// Target table (e.g. `"app_tags"`).
8911 to: String,
8912 /// Junction table (e.g. `"post_tags"`).
8913 through: String,
8914 /// Source FK column in the junction table (e.g. `"post_id"`).
8915 src: String,
8916 /// Destination FK column in the junction table (e.g. `"tag_id"`).
8917 dst: String,
8918 /// Whether the migration writer should auto-create the junction
8919 /// table. Default `true`. Set `auto_create = false` (#324) when
8920 /// the operator declares the through table as its own
8921 /// `#[derive(Model)]` struct with extra columns.
8922 auto_create: bool,
8923}
8924
8925/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8926/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8927/// junction carries a ContentType discriminator so unrelated models
8928/// share one pivot + related set.
8929struct GenericM2MAttr {
8930 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8931 name: String,
8932 /// Polymorphic junction table (e.g. `"taggables"`).
8933 through: String,
8934 /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8935 pk_column: String,
8936 /// Junction column holding the owning model's `content_type_id`
8937 /// discriminator (e.g. `"taggable_type"`).
8938 ct_column: String,
8939 /// Junction column holding the related model PK (e.g. `"tag_id"`).
8940 related_column: String,
8941}
8942
8943/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8944/// "user"))]`. `track` is a comma-separated list of field names whose
8945/// before/after values land in the JSONB `changes` column. `source`
8946/// is informational only — it pins a default source when the model
8947/// is written outside any `audit::with_source(...)` scope (rare).
8948#[derive(Default)]
8949struct AuditAttrs {
8950 /// Field names to capture in the `changes` JSONB. Validated
8951 /// against declared scalar fields at compile time. Empty means
8952 /// "track every scalar field".
8953 track: Option<(Vec<String>, proc_macro2::Span)>,
8954}
8955
8956/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8957/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8958/// comma-separated strings; we validate each ident against the model's
8959/// declared fields at compile time.
8960#[derive(Default)]
8961struct AdminAttrs {
8962 list_display: Option<(Vec<String>, proc_macro2::Span)>,
8963 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8964 list_per_page: Option<usize>,
8965 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8966 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8967 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8968 /// Bulk action names. No field-validation against model fields —
8969 /// these are action handlers, not column references.
8970 actions: Option<(Vec<String>, proc_macro2::Span)>,
8971 /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8972 /// sections, comma-separated fields per section, optional
8973 /// `Title:` prefix. Empty title omits the `<legend>`.
8974 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8975 /// `admin(list_display_links = "title")`. Names
8976 /// from `list_display` whose cells should link to detail/edit.
8977 /// Issue #350.
8978 list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8979 /// `admin(search_help_text = "...")`. Short
8980 /// caption rendered beside the admin list view's search box.
8981 /// Issue #353.
8982 search_help_text: Option<String>,
8983 /// `admin(actions_on_top = false)`. Hides the
8984 /// action-bar above the table. Default `true`. Issue #354.
8985 actions_on_top: Option<bool>,
8986 /// `admin(actions_on_bottom = true)`. Renders an
8987 /// additional action-bar below the table. Default `false`.
8988 /// Issue #354.
8989 actions_on_bottom: Option<bool>,
8990 /// `admin(date_hierarchy = "created_at")`. Name of
8991 /// a date / datetime field whose values render as a clickable
8992 /// year / month / day drill-down strip above the list table.
8993 /// Empty / unset disables the strip. Issue #355.
8994 date_hierarchy: Option<String>,
8995 /// `admin(prepopulated_fields = "slug:title")`.
8996 /// Each entry is `target:source[+source2]`; multiple entries are
8997 /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8998 /// The admin change-form emits JS that slugifies the source values
8999 /// into the target field on every keystroke. Issue #356.
9000 prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
9001 /// `admin(raw_id_fields = "parent, owner")`. Names
9002 /// of FK fields whose change-form widget renders a lookup link
9003 /// next to the input. Issue #357.
9004 raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
9005 /// `admin(autocomplete_fields = "author_id")`.
9006 /// Names of FK fields whose change-form widget renders an
9007 /// Ajax-driven typeahead populated from a `__autocomplete`
9008 /// endpoint on the target model. Issue #358.
9009 autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
9010 /// `admin(list_select_related = "all" | "none" | "author, …")`
9011 /// — tunes the admin list view's FK auto-JOIN
9012 /// policy. Default `"all"` matches rustango's join-everything
9013 /// behavior; `"none"` opts out; CSV restricts. Issue #352.
9014 list_select_related: Option<String>,
9015 /// `admin(formfield_overrides = "field:widget, field2:widget2")`.
9016 /// Each entry is `field_name:widget_name`; multiple
9017 /// entries comma-separated. Empty / unset → no overrides. The
9018 /// list of widget names supported is documented on
9019 /// `AdminConfig::formfield_overrides`. Issue #359.
9020 formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
9021}
9022
9023fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
9024 let mut out = ContainerAttrs {
9025 table: None,
9026 display: None,
9027 app: None,
9028 admin: None,
9029 audit: None,
9030 // Default `permissions = true` so every `#[derive(Model)]`
9031 // gets the four CRUD codenames seeded by `auto_create_permissions`
9032 // and is visible to non-superusers in the tenant admin without
9033 // manual per-model annotation. Models that intentionally don't
9034 // want permission rows (registry-internal types, framework
9035 // tables operators shouldn't manage directly) opt out via
9036 // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
9037 // out-of-the-box admin invisibility regression (#62).
9038 permissions: true,
9039 m2m: Vec::new(),
9040 generic_m2m: Vec::new(),
9041 indexes: Vec::new(),
9042 checks: Vec::new(),
9043 excludes: Vec::new(),
9044 composite_fks: Vec::new(),
9045 generic_fks: Vec::new(),
9046 scope: None,
9047 manager_fns: Vec::new(),
9048 default_order: Vec::new(),
9049 is_view: false,
9050 managed: true,
9051 verbose_name: None,
9052 verbose_name_plural: None,
9053 base_manager_name: None,
9054 order_with_respect_to: None,
9055 proxy: false,
9056 required_db_features: Vec::new(),
9057 required_db_vendor: None,
9058 default_related_name: None,
9059 db_table_comment: None,
9060 get_latest_by: None,
9061 extra_permissions: Vec::new(),
9062 default_permissions: Vec::new(),
9063 global_scopes: Vec::new(),
9064 through_relations: Vec::new(),
9065 reverse_has_relations: Vec::new(),
9066 generic_has_relations: Vec::new(),
9067 };
9068 for attr in &input.attrs {
9069 if !attr.path().is_ident("rustango") {
9070 continue;
9071 }
9072 attr.parse_nested_meta(|meta| {
9073 if meta.path.is_ident("table") {
9074 let s: LitStr = meta.value()?.parse()?;
9075 let name = s.value();
9076 // v0.27.3 (#65) — macro-time guard against table names
9077 // that compile but break SQL downstream. Hyphens are
9078 // the common footgun: PostgreSQL accepts a quoted
9079 // `"intermediate-region"` in CREATE TABLE, but the
9080 // FK / index name derivation in `migrate::ddl`
9081 // emits `intermediate-region_field_fkey` unquoted,
9082 // which then fails the SQL parser. Same shape rule
9083 // as Postgres regular identifiers so the safe path
9084 // is the only path.
9085 validate_table_name(&name, s.span())?;
9086 out.table = Some(name);
9087 return Ok(());
9088 }
9089 if meta.path.is_ident("display") {
9090 let s: LitStr = meta.value()?.parse()?;
9091 out.display = Some((s.value(), s.span()));
9092 return Ok(());
9093 }
9094 if meta.path.is_ident("app") {
9095 let s: LitStr = meta.value()?.parse()?;
9096 out.app = Some(s.value());
9097 return Ok(());
9098 }
9099 if meta.path.is_ident("scope") {
9100 let s: LitStr = meta.value()?.parse()?;
9101 let val = s.value();
9102 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
9103 return Err(meta.error(format!(
9104 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
9105 )));
9106 }
9107 out.scope = Some(val);
9108 return Ok(());
9109 }
9110 if meta.path.is_ident("admin") {
9111 let mut admin = AdminAttrs::default();
9112 meta.parse_nested_meta(|inner| {
9113 if inner.path.is_ident("list_display") {
9114 let s: LitStr = inner.value()?.parse()?;
9115 admin.list_display =
9116 Some((split_field_list(&s.value()), s.span()));
9117 return Ok(());
9118 }
9119 if inner.path.is_ident("search_fields") {
9120 let s: LitStr = inner.value()?.parse()?;
9121 admin.search_fields =
9122 Some((split_field_list(&s.value()), s.span()));
9123 return Ok(());
9124 }
9125 if inner.path.is_ident("readonly_fields") {
9126 let s: LitStr = inner.value()?.parse()?;
9127 admin.readonly_fields =
9128 Some((split_field_list(&s.value()), s.span()));
9129 return Ok(());
9130 }
9131 if inner.path.is_ident("list_per_page") {
9132 let lit: syn::LitInt = inner.value()?.parse()?;
9133 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
9134 return Ok(());
9135 }
9136 if inner.path.is_ident("ordering") {
9137 let s: LitStr = inner.value()?.parse()?;
9138 admin.ordering = Some((
9139 parse_ordering_list(&s.value()),
9140 s.span(),
9141 ));
9142 return Ok(());
9143 }
9144 if inner.path.is_ident("list_filter") {
9145 let s: LitStr = inner.value()?.parse()?;
9146 admin.list_filter =
9147 Some((split_field_list(&s.value()), s.span()));
9148 return Ok(());
9149 }
9150 if inner.path.is_ident("actions") {
9151 let s: LitStr = inner.value()?.parse()?;
9152 admin.actions =
9153 Some((split_field_list(&s.value()), s.span()));
9154 return Ok(());
9155 }
9156 if inner.path.is_ident("fieldsets") {
9157 let s: LitStr = inner.value()?.parse()?;
9158 admin.fieldsets =
9159 Some((parse_fieldset_list(&s.value()), s.span()));
9160 return Ok(());
9161 }
9162 if inner.path.is_ident("list_display_links") {
9163 let s: LitStr = inner.value()?.parse()?;
9164 admin.list_display_links =
9165 Some((split_field_list(&s.value()), s.span()));
9166 return Ok(());
9167 }
9168 if inner.path.is_ident("search_help_text") {
9169 let s: LitStr = inner.value()?.parse()?;
9170 admin.search_help_text = Some(s.value());
9171 return Ok(());
9172 }
9173 if inner.path.is_ident("actions_on_top") {
9174 let lit: syn::LitBool = inner.value()?.parse()?;
9175 admin.actions_on_top = Some(lit.value);
9176 return Ok(());
9177 }
9178 if inner.path.is_ident("actions_on_bottom") {
9179 let lit: syn::LitBool = inner.value()?.parse()?;
9180 admin.actions_on_bottom = Some(lit.value);
9181 return Ok(());
9182 }
9183 if inner.path.is_ident("date_hierarchy") {
9184 let s: LitStr = inner.value()?.parse()?;
9185 admin.date_hierarchy = Some(s.value());
9186 return Ok(());
9187 }
9188 if inner.path.is_ident("prepopulated_fields") {
9189 let s: LitStr = inner.value()?.parse()?;
9190 admin.prepopulated_fields =
9191 Some((parse_prepopulated_list(&s.value()), s.span()));
9192 return Ok(());
9193 }
9194 if inner.path.is_ident("raw_id_fields") {
9195 let s: LitStr = inner.value()?.parse()?;
9196 admin.raw_id_fields =
9197 Some((split_field_list(&s.value()), s.span()));
9198 return Ok(());
9199 }
9200 if inner.path.is_ident("autocomplete_fields") {
9201 let s: LitStr = inner.value()?.parse()?;
9202 admin.autocomplete_fields =
9203 Some((split_field_list(&s.value()), s.span()));
9204 return Ok(());
9205 }
9206 if inner.path.is_ident("list_select_related") {
9207 let s: LitStr = inner.value()?.parse()?;
9208 admin.list_select_related = Some(s.value());
9209 return Ok(());
9210 }
9211 if inner.path.is_ident("formfield_overrides") {
9212 let s: LitStr = inner.value()?.parse()?;
9213 admin.formfield_overrides =
9214 Some((parse_formfield_overrides(&s.value()), s.span()));
9215 return Ok(());
9216 }
9217 Err(inner.error(
9218 "unknown admin attribute (supported: \
9219 `list_display`, `list_display_links`, \
9220 `search_fields`, `search_help_text`, \
9221 `readonly_fields`, \
9222 `list_filter`, `list_per_page`, `ordering`, `actions`, \
9223 `actions_on_top`, `actions_on_bottom`, \
9224 `date_hierarchy`, \
9225 `prepopulated_fields`, \
9226 `raw_id_fields`, \
9227 `autocomplete_fields`, \
9228 `list_select_related`, \
9229 `formfield_overrides`, \
9230 `fieldsets`)",
9231 ))
9232 })?;
9233 out.admin = Some(admin);
9234 return Ok(());
9235 }
9236 if meta.path.is_ident("manager") {
9237 // Its empty trait could not take methods (#2132).
9238 return Err(meta.error(
9239 "`manager(ext = ...)` was removed (#2132): declare your own \
9240 `trait FooManagerExt: Sized { … }` and implement it for `QuerySet<Foo>`",
9241 ));
9242 }
9243 if meta.path.is_ident("manager_fn") {
9244 // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
9245 // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
9246 // next to the default `Self::objects()`. Multiple
9247 // attributes accumulate.
9248 let s: LitStr = meta.value()?.parse()?;
9249 let name = s.value();
9250 if name.is_empty() {
9251 return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
9252 }
9253 if name == "objects" {
9254 return Err(meta.error(
9255 "`manager_fn = \"objects\"` collides with the default \
9256 accessor — pick a different name",
9257 ));
9258 }
9259 let ident = syn::Ident::new(&name, s.span());
9260 if out.manager_fns.iter().any(|prev| *prev == ident) {
9261 return Err(meta.error(format!(
9262 "duplicate `manager_fn = \"{name}\"`"
9263 )));
9264 }
9265 out.manager_fns.push(ident);
9266 return Ok(());
9267 }
9268 if meta.path.is_ident("default_order") {
9269 // `#[rustango(default_order = "-created_at, status")]`
9270 // — issue #291 / T2.5. Comma-separated list; `-prefix`
9271 // means descending, `+prefix` or bare name means ascending.
9272 // Per-query opt-in via `QuerySet::with_default_order()`.
9273 let s: LitStr = meta.value()?.parse()?;
9274 let raw = s.value();
9275 let span = s.span();
9276 let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
9277 Vec::new();
9278 for entry in raw.split(',') {
9279 let trimmed = entry.trim();
9280 if trimmed.is_empty() {
9281 return Err(syn::Error::new(
9282 span,
9283 "`default_order = \"...\"` has an empty entry — \
9284 check for a stray comma",
9285 ));
9286 }
9287 let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
9288 (true, rest.trim().to_owned())
9289 } else if let Some(rest) = trimmed.strip_prefix('+') {
9290 (false, rest.trim().to_owned())
9291 } else {
9292 (false, trimmed.to_owned())
9293 };
9294 if name.is_empty() {
9295 return Err(syn::Error::new(
9296 span,
9297 "`default_order` entry has no column name after the prefix",
9298 ));
9299 }
9300 if parsed.iter().any(|(n, _, _)| *n == name) {
9301 return Err(syn::Error::new(
9302 span,
9303 format!("duplicate column `{name}` in `default_order`"),
9304 ));
9305 }
9306 parsed.push((name, desc, span));
9307 }
9308 if parsed.is_empty() {
9309 return Err(syn::Error::new(
9310 span,
9311 "`default_order = \"...\"` cannot be empty",
9312 ));
9313 }
9314 out.default_order = parsed;
9315 return Ok(());
9316 }
9317 if meta.path.is_ident("global_scope") {
9318 // `#[rustango(global_scope(name = "active", apply =
9319 // path::to::fn))]` — issue #820. The apply function
9320 // path resolves at macro-expand time in the consumer's
9321 // scope; the macro re-emits it verbatim into the
9322 // `ModelSchema::global_scopes` slice literal.
9323 let span = meta.path.span();
9324 let mut scope_name: Option<String> = None;
9325 let mut apply_path: Option<syn::Path> = None;
9326 meta.parse_nested_meta(|inner| {
9327 if inner.path.is_ident("name") {
9328 let s: LitStr = inner.value()?.parse()?;
9329 let raw = s.value();
9330 if raw.trim().is_empty() {
9331 return Err(syn::Error::new(
9332 s.span(),
9333 "`global_scope(name = \"...\")` must not be empty",
9334 ));
9335 }
9336 scope_name = Some(raw);
9337 return Ok(());
9338 }
9339 if inner.path.is_ident("apply") {
9340 let p: syn::Path = inner.value()?.parse()?;
9341 apply_path = Some(p);
9342 return Ok(());
9343 }
9344 Err(inner.error(
9345 "unknown `global_scope` attribute (supported: \
9346 `name`, `apply`)",
9347 ))
9348 })?;
9349 let Some(name) = scope_name else {
9350 return Err(syn::Error::new(
9351 span,
9352 "`global_scope` requires `name = \"...\"`",
9353 ));
9354 };
9355 let Some(apply) = apply_path else {
9356 return Err(syn::Error::new(
9357 span,
9358 "`global_scope` requires `apply = fn_path`",
9359 ));
9360 };
9361 if out.global_scopes.iter().any(|s| s.name == name) {
9362 return Err(syn::Error::new(
9363 span,
9364 format!(
9365 "duplicate global scope name `{name}` — \
9366 pick a unique identifier so \
9367 `QuerySet::without_global_scope(\"{name}\")` \
9368 is unambiguous"
9369 ),
9370 ));
9371 }
9372 out.global_scopes.push(GlobalScopeAttr { name, apply });
9373 return Ok(());
9374 }
9375 if meta.path.is_ident("through") {
9376 // `#[rustango(through(name = "posts", far = "Post",
9377 // far_fk_column = "author_id", intermediate = "User",
9378 // intermediate_fk_column = "country_id"
9379 // [, intermediate_pk_column = "id"]))]` — issue #817.
9380 let span = meta.path.span();
9381 let mut accessor_name: Option<String> = None;
9382 let mut far_ident: Option<syn::Ident> = None;
9383 let mut far_fk_column: Option<String> = None;
9384 let mut intermediate_ident: Option<syn::Ident> = None;
9385 let mut intermediate_fk_column: Option<String> = None;
9386 let mut intermediate_pk_column: Option<String> = None;
9387 fn parse_nonempty_string(
9388 inner: &syn::meta::ParseNestedMeta<'_>,
9389 field: &str,
9390 ) -> syn::Result<String> {
9391 let s: LitStr = inner.value()?.parse()?;
9392 let raw = s.value();
9393 let trimmed = raw.trim();
9394 if trimmed.is_empty() {
9395 return Err(syn::Error::new(
9396 s.span(),
9397 format!("`through({field} = \"...\")` must not be empty"),
9398 ));
9399 }
9400 Ok(trimmed.to_owned())
9401 }
9402 meta.parse_nested_meta(|inner| {
9403 if inner.path.is_ident("name") {
9404 accessor_name = Some(parse_nonempty_string(&inner, "name")?);
9405 return Ok(());
9406 }
9407 if inner.path.is_ident("far") {
9408 // Far model name accepted as a string literal
9409 // so the attribute fits inside the existing
9410 // `parse_nested_meta` shape; parsed back into a
9411 // `syn::Ident` so the emitted accessor can
9412 // reference the type directly.
9413 let s: LitStr = inner.value()?.parse()?;
9414 let raw = s.value();
9415 let trimmed = raw.trim();
9416 if trimmed.is_empty() {
9417 return Err(syn::Error::new(
9418 s.span(),
9419 "`through(far = \"...\")` must not be empty",
9420 ));
9421 }
9422 far_ident = Some(syn::Ident::new(trimmed, s.span()));
9423 return Ok(());
9424 }
9425 if inner.path.is_ident("far_fk_column") {
9426 far_fk_column =
9427 Some(parse_nonempty_string(&inner, "far_fk_column")?);
9428 return Ok(());
9429 }
9430 if inner.path.is_ident("intermediate") {
9431 let s: LitStr = inner.value()?.parse()?;
9432 let raw = s.value();
9433 let trimmed = raw.trim();
9434 if trimmed.is_empty() {
9435 return Err(syn::Error::new(
9436 s.span(),
9437 "`through(intermediate = \"...\")` must not be empty",
9438 ));
9439 }
9440 intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9441 return Ok(());
9442 }
9443 if inner.path.is_ident("intermediate_fk_column") {
9444 intermediate_fk_column =
9445 Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9446 return Ok(());
9447 }
9448 if inner.path.is_ident("intermediate_pk_column") {
9449 intermediate_pk_column =
9450 Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9451 return Ok(());
9452 }
9453 Err(inner.error(
9454 "unknown `through` attribute (supported: \
9455 `name`, `far`, `far_fk_column`, \
9456 `intermediate`, `intermediate_fk_column`, \
9457 `intermediate_pk_column`)",
9458 ))
9459 })?;
9460 let Some(name) = accessor_name else {
9461 return Err(syn::Error::new(
9462 span,
9463 "`through` requires `name = \"...\"`",
9464 ));
9465 };
9466 let Some(far) = far_ident else {
9467 return Err(syn::Error::new(
9468 span,
9469 "`through` requires `far = \"FarModelType\"`",
9470 ));
9471 };
9472 let Some(far_fk_column) = far_fk_column else {
9473 return Err(syn::Error::new(
9474 span,
9475 "`through` requires `far_fk_column = \"<column>\"`",
9476 ));
9477 };
9478 let Some(intermediate) = intermediate_ident else {
9479 return Err(syn::Error::new(
9480 span,
9481 "`through` requires `intermediate = \"IntermediateModelType\"`",
9482 ));
9483 };
9484 let Some(intermediate_fk_column) = intermediate_fk_column else {
9485 return Err(syn::Error::new(
9486 span,
9487 "`through` requires `intermediate_fk_column = \"<column>\"`",
9488 ));
9489 };
9490 let intermediate_pk_column =
9491 intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9492 if out.through_relations.iter().any(|t| t.name == name) {
9493 return Err(syn::Error::new(
9494 span,
9495 format!(
9496 "duplicate `through(name = \"{name}\")` — \
9497 pick a unique accessor name"
9498 ),
9499 ));
9500 }
9501 out.through_relations.push(ThroughAttr {
9502 name,
9503 far,
9504 far_fk_column,
9505 intermediate,
9506 intermediate_fk_column,
9507 intermediate_pk_column,
9508 });
9509 return Ok(());
9510 }
9511 if meta.path.is_ident("reverse_has") {
9512 // `#[rustango(reverse_has(name = "comments",
9513 // child = "Comment", child_fk_column = "post_id"
9514 // [, self_pk_column = "id"]))]` — issue #830.
9515 let span = meta.path.span();
9516 let mut accessor_name: Option<String> = None;
9517 let mut child_ident: Option<syn::Ident> = None;
9518 let mut child_fk_column: Option<String> = None;
9519 let mut self_pk_column: Option<String> = None;
9520 meta.parse_nested_meta(|inner| {
9521 if inner.path.is_ident("name") {
9522 let s: LitStr = inner.value()?.parse()?;
9523 let raw = s.value();
9524 if raw.trim().is_empty() {
9525 return Err(syn::Error::new(
9526 s.span(),
9527 "`reverse_has(name = \"...\")` must not be empty",
9528 ));
9529 }
9530 accessor_name = Some(raw);
9531 return Ok(());
9532 }
9533 if inner.path.is_ident("child") {
9534 let s: LitStr = inner.value()?.parse()?;
9535 let raw = s.value();
9536 let trimmed = raw.trim();
9537 if trimmed.is_empty() {
9538 return Err(syn::Error::new(
9539 s.span(),
9540 "`reverse_has(child = \"...\")` must not be empty",
9541 ));
9542 }
9543 child_ident = Some(syn::Ident::new(trimmed, s.span()));
9544 return Ok(());
9545 }
9546 if inner.path.is_ident("child_fk_column") {
9547 let s: LitStr = inner.value()?.parse()?;
9548 let raw = s.value();
9549 let trimmed = raw.trim();
9550 if trimmed.is_empty() {
9551 return Err(syn::Error::new(
9552 s.span(),
9553 "`reverse_has(child_fk_column = \"...\")` must not be empty",
9554 ));
9555 }
9556 child_fk_column = Some(trimmed.to_owned());
9557 return Ok(());
9558 }
9559 if inner.path.is_ident("self_pk_column") {
9560 let s: LitStr = inner.value()?.parse()?;
9561 let raw = s.value();
9562 let trimmed = raw.trim();
9563 if trimmed.is_empty() {
9564 return Err(syn::Error::new(
9565 s.span(),
9566 "`reverse_has(self_pk_column = \"...\")` must not be empty",
9567 ));
9568 }
9569 self_pk_column = Some(trimmed.to_owned());
9570 return Ok(());
9571 }
9572 Err(inner.error(
9573 "unknown `reverse_has` attribute (supported: \
9574 `name`, `child`, `child_fk_column`, \
9575 `self_pk_column`)",
9576 ))
9577 })?;
9578 let Some(name) = accessor_name else {
9579 return Err(syn::Error::new(
9580 span,
9581 "`reverse_has` requires `name = \"...\"`",
9582 ));
9583 };
9584 let Some(child) = child_ident else {
9585 return Err(syn::Error::new(
9586 span,
9587 "`reverse_has` requires `child = \"ChildModelType\"`",
9588 ));
9589 };
9590 let Some(child_fk_column) = child_fk_column else {
9591 return Err(syn::Error::new(
9592 span,
9593 "`reverse_has` requires `child_fk_column = \"<column>\"`",
9594 ));
9595 };
9596 if out.reverse_has_relations.iter().any(|r| r.name == name) {
9597 return Err(syn::Error::new(
9598 span,
9599 format!(
9600 "duplicate `reverse_has(name = \"{name}\")` — \
9601 pick a unique accessor name"
9602 ),
9603 ));
9604 }
9605 out.reverse_has_relations.push(ReverseHasAttr {
9606 name,
9607 child,
9608 child_fk_column,
9609 self_pk_column,
9610 });
9611 return Ok(());
9612 }
9613 if meta.path.is_ident("generic_has") {
9614 // `#[rustango(generic_has(name = "tags",
9615 // child = "Tag", ct_column = "content_type_id",
9616 // pk_column = "object_pk" [, self_pk_column = "id"]))]`
9617 // — issue #830, the reverse generic-FK (GFK) arm of the
9618 // relation-existence family.
9619 let span = meta.path.span();
9620 let mut accessor_name: Option<String> = None;
9621 let mut child_ident: Option<syn::Ident> = None;
9622 let mut ct_column: Option<String> = None;
9623 let mut pk_column: Option<String> = None;
9624 let mut self_pk_column: Option<String> = None;
9625 meta.parse_nested_meta(|inner| {
9626 if inner.path.is_ident("name") {
9627 let s: LitStr = inner.value()?.parse()?;
9628 let raw = s.value();
9629 if raw.trim().is_empty() {
9630 return Err(syn::Error::new(
9631 s.span(),
9632 "`generic_has(name = \"...\")` must not be empty",
9633 ));
9634 }
9635 accessor_name = Some(raw);
9636 return Ok(());
9637 }
9638 if inner.path.is_ident("child") {
9639 let s: LitStr = inner.value()?.parse()?;
9640 let trimmed = s.value().trim().to_owned();
9641 if trimmed.is_empty() {
9642 return Err(syn::Error::new(
9643 s.span(),
9644 "`generic_has(child = \"...\")` must not be empty",
9645 ));
9646 }
9647 child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9648 return Ok(());
9649 }
9650 if inner.path.is_ident("ct_column") {
9651 let s: LitStr = inner.value()?.parse()?;
9652 let trimmed = s.value().trim().to_owned();
9653 if trimmed.is_empty() {
9654 return Err(syn::Error::new(
9655 s.span(),
9656 "`generic_has(ct_column = \"...\")` must not be empty",
9657 ));
9658 }
9659 ct_column = Some(trimmed);
9660 return Ok(());
9661 }
9662 if inner.path.is_ident("pk_column") {
9663 let s: LitStr = inner.value()?.parse()?;
9664 let trimmed = s.value().trim().to_owned();
9665 if trimmed.is_empty() {
9666 return Err(syn::Error::new(
9667 s.span(),
9668 "`generic_has(pk_column = \"...\")` must not be empty",
9669 ));
9670 }
9671 pk_column = Some(trimmed);
9672 return Ok(());
9673 }
9674 if inner.path.is_ident("self_pk_column") {
9675 let s: LitStr = inner.value()?.parse()?;
9676 let trimmed = s.value().trim().to_owned();
9677 if trimmed.is_empty() {
9678 return Err(syn::Error::new(
9679 s.span(),
9680 "`generic_has(self_pk_column = \"...\")` must not be empty",
9681 ));
9682 }
9683 self_pk_column = Some(trimmed);
9684 return Ok(());
9685 }
9686 Err(inner.error(
9687 "unknown `generic_has` attribute (supported: \
9688 `name`, `child`, `ct_column`, `pk_column`, \
9689 `self_pk_column`)",
9690 ))
9691 })?;
9692 let Some(name) = accessor_name else {
9693 return Err(syn::Error::new(
9694 span,
9695 "`generic_has` requires `name = \"...\"`",
9696 ));
9697 };
9698 let Some(child) = child_ident else {
9699 return Err(syn::Error::new(
9700 span,
9701 "`generic_has` requires `child = \"ChildModelType\"`",
9702 ));
9703 };
9704 let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9705 let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9706 if out.generic_has_relations.iter().any(|r| r.name == name) {
9707 return Err(syn::Error::new(
9708 span,
9709 format!(
9710 "duplicate `generic_has(name = \"{name}\")` — \
9711 pick a unique accessor name"
9712 ),
9713 ));
9714 }
9715 out.generic_has_relations.push(GenericHasAttr {
9716 name,
9717 child,
9718 ct_column,
9719 pk_column,
9720 self_pk_column,
9721 });
9722 return Ok(());
9723 }
9724 if meta.path.is_ident("audit") {
9725 let mut audit = AuditAttrs::default();
9726 meta.parse_nested_meta(|inner| {
9727 if inner.path.is_ident("track") {
9728 let s: LitStr = inner.value()?.parse()?;
9729 audit.track =
9730 Some((split_field_list(&s.value()), s.span()));
9731 return Ok(());
9732 }
9733 Err(inner.error(
9734 "unknown audit attribute (supported: `track`)",
9735 ))
9736 })?;
9737 out.audit = Some(audit);
9738 return Ok(());
9739 }
9740 if meta.path.is_ident("permissions") {
9741 // Two forms accepted:
9742 // #[rustango(permissions)] — flag form, true
9743 // #[rustango(permissions = false)] — explicit opt-out
9744 // #[rustango(permissions = true)] — explicit opt-in
9745 if let Ok(v) = meta.value() {
9746 let lit: syn::LitBool = v.parse()?;
9747 out.permissions = lit.value;
9748 } else {
9749 out.permissions = true;
9750 }
9751 return Ok(());
9752 }
9753 if meta.path.is_ident("view") {
9754 // Issue #293 / T2.10. Two forms accepted, matching
9755 // the `permissions` flag pattern:
9756 // #[rustango(view)] — flag form, true
9757 // #[rustango(view = false)] — explicit opt-out
9758 // #[rustango(view = true)] — explicit opt-in
9759 if let Ok(v) = meta.value() {
9760 let lit: syn::LitBool = v.parse()?;
9761 out.is_view = lit.value;
9762 } else {
9763 out.is_view = true;
9764 }
9765 return Ok(());
9766 }
9767 if meta.path.is_ident("managed") {
9768 // Who owns the table. Issue #321.
9769 // #[rustango(managed = false)] — operator-managed table
9770 // #[rustango(managed = true)] — rustango-managed (the default)
9771 // Bare-flag form is intentionally not accepted: writing
9772 // `#[rustango(managed)]` reads as "yes please manage it"
9773 // which is already the default. The opt-out is the only
9774 // useful state, so it must be explicit.
9775 let v = meta.value()?;
9776 let lit: syn::LitBool = v.parse()?;
9777 out.managed = lit.value;
9778 return Ok(());
9779 }
9780 if meta.path.is_ident("verbose_name") {
9781 let s: LitStr = meta.value()?.parse()?;
9782 out.verbose_name = Some(s.value());
9783 return Ok(());
9784 }
9785 if meta.path.is_ident("verbose_name_plural") {
9786 let s: LitStr = meta.value()?.parse()?;
9787 out.verbose_name_plural = Some(s.value());
9788 return Ok(());
9789 }
9790 if meta.path.is_ident("db_table_comment") {
9791 // Free-form table-level comment attached to the DB
9792 // catalog.
9793 let s: LitStr = meta.value()?.parse()?;
9794 out.db_table_comment = Some(s.value());
9795 return Ok(());
9796 }
9797 if meta.path.is_ident("proxy") {
9798 // Declarative flag
9799 // marking the struct as a proxy of another model that
9800 // shares its DB table. Stored on `ModelSchema::proxy`
9801 // so future codegen can skip `CreateTable` emission
9802 // for proxies (parent owns the table) and route
9803 // per-instance method resolution to the proxy class.
9804 //
9805 // Accepts `proxy` (bare → true) and `proxy = true/false`.
9806 let value = if meta.input.peek(syn::Token![=]) {
9807 meta.value()?.parse::<syn::LitBool>()?.value
9808 } else {
9809 true
9810 };
9811 out.proxy = value;
9812 return Ok(());
9813 }
9814 if meta.path.is_ident("order_with_respect_to") {
9815 // `order_with_respect_to = "parent_fk"` — the model's
9816 // instances are intrinsically ordered relative to
9817 // their parent FK.
9818 //
9819 // rustango stores the FK field name on
9820 // `ModelSchema::order_with_respect_to`. Declarative-only
9821 // today: the migration writer + admin surfaces still
9822 // treat every model identically. Future codegen will
9823 // key off the metadata to auto-emit the `_order`
9824 // column and reorder helpers.
9825 //
9826 // Validated as a Rust-shape identifier so the macro
9827 // can reject typos at derive time.
9828 let s: LitStr = meta.value()?.parse()?;
9829 let raw = s.value();
9830 if raw.is_empty() {
9831 return Err(syn::Error::new(
9832 s.span(),
9833 "`order_with_respect_to` must be a non-empty FK field name",
9834 ));
9835 }
9836 let valid = raw
9837 .chars()
9838 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9839 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9840 if !valid {
9841 return Err(syn::Error::new(
9842 s.span(),
9843 format!(
9844 "`order_with_respect_to` must be a valid Rust \
9845 identifier (letters / digits / underscores, \
9846 not starting with a digit); got `{raw}`"
9847 ),
9848 ));
9849 }
9850 out.order_with_respect_to = Some(raw);
9851 return Ok(());
9852 }
9853 if meta.path.is_ident("required_db_features") {
9854 // Capability tokens the model needs (e.g.
9855 // `"json_extract"`, `"window_functions"`,
9856 // `"row_security"`). Comma-separated.
9857 // `manage check --deploy` walks every model and warns
9858 // when the active backend doesn't advertise the
9859 // capability.
9860 //
9861 // rustango ships a small registry of capability tokens
9862 // each dialect supports — see `Dialect::supports`.
9863 // Unknown tokens still parse (they end up on the
9864 // schema and show up in the warning) so projects can
9865 // declare aspirational capabilities and the check
9866 // verb will keep nagging until the dialect implements
9867 // them.
9868 let s: LitStr = meta.value()?.parse()?;
9869 let raw = s.value();
9870 let features: Vec<String> = raw
9871 .split(',')
9872 .map(str::trim)
9873 .filter(|s| !s.is_empty())
9874 .map(str::to_owned)
9875 .collect();
9876 if features.is_empty() {
9877 return Err(syn::Error::new(
9878 s.span(),
9879 "`required_db_features` must list at least one \
9880 comma-separated capability token",
9881 ));
9882 }
9883 out.required_db_features = features;
9884 return Ok(());
9885 }
9886 if meta.path.is_ident("required_db_vendor") {
9887 // The model is only meant to run against the named DB
9888 // backend. `manage check --deploy` flags a mismatch so
9889 // ops catches "I forgot to switch DATABASE_URL" at
9890 // deploy time rather than runtime.
9891 //
9892 // Restricted to the three backends rustango ships
9893 // dialects for so the check verb can compare reliably.
9894 let s: LitStr = meta.value()?.parse()?;
9895 let raw = s.value().to_ascii_lowercase();
9896 match raw.as_str() {
9897 "postgresql" | "postgres" | "pg" => {
9898 out.required_db_vendor = Some("postgres".to_owned());
9899 }
9900 "mysql" | "mariadb" => {
9901 out.required_db_vendor = Some("mysql".to_owned());
9902 }
9903 "sqlite" | "sqlite3" => {
9904 out.required_db_vendor = Some("sqlite".to_owned());
9905 }
9906 _ => {
9907 return Err(syn::Error::new(
9908 s.span(),
9909 format!(
9910 "unknown required_db_vendor `{raw}` — \
9911 expected `postgres` (aliases: `postgresql`, `pg`), \
9912 `mysql` (alias: `mariadb`), or `sqlite` \
9913 (alias: `sqlite3`)"
9914 ),
9915 ));
9916 }
9917 }
9918 return Ok(());
9919 }
9920 if meta.path.is_ident("base_manager_name") {
9921 // Name of the manager that `<instance>.<relation>_set`
9922 // uses when resolving reverse-relation managers.
9923 // Distinct from `default_manager_name` (what
9924 // `Self::objects()` returns). Stored on
9925 // `ModelSchema::base_manager_name`.
9926 //
9927 // Validated as a Rust identifier so it stays safe to
9928 // re-emit as code in future reverse-manager codegen.
9929 let s: LitStr = meta.value()?.parse()?;
9930 let raw = s.value();
9931 if raw.is_empty() {
9932 return Err(syn::Error::new(
9933 s.span(),
9934 "`base_manager_name` must be a non-empty string",
9935 ));
9936 }
9937 let valid = raw
9938 .chars()
9939 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9940 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9941 if !valid {
9942 return Err(syn::Error::new(
9943 s.span(),
9944 format!(
9945 "`base_manager_name` must be a valid Rust \
9946 identifier (letters / digits / underscores, \
9947 not starting with a digit); got `{raw}`"
9948 ),
9949 ));
9950 }
9951 out.base_manager_name = Some(raw);
9952 return Ok(());
9953 }
9954 if meta.path.is_ident("default_related_name") {
9955 // The name reverse-relation accessors use when callers
9956 // don't override `related_name = "..."` on the FK / M2M
9957 // field. Stored on `ModelSchema::default_related_name`
9958 // so future reverse-manager codegen / schema emit /
9959 // admin templates can pick the right accessor name
9960 // (today rustango doesn't auto-emit reverse managers;
9961 // the metadata is the foundation for that work).
9962 //
9963 // Enforced non-empty + ASCII identifier-shape so the
9964 // string is safe to use as a Rust ident later.
9965 let s: LitStr = meta.value()?.parse()?;
9966 let raw = s.value();
9967 if raw.is_empty() {
9968 return Err(syn::Error::new(
9969 s.span(),
9970 "`default_related_name` must be a non-empty string",
9971 ));
9972 }
9973 let valid = raw
9974 .chars()
9975 .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9976 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9977 if !valid {
9978 return Err(syn::Error::new(
9979 s.span(),
9980 format!(
9981 "`default_related_name` must be snake_case ASCII \
9982 (lowercase letters / digits / underscores, not \
9983 starting with a digit); got `{raw}`"
9984 ),
9985 ));
9986 }
9987 out.default_related_name = Some(raw);
9988 return Ok(());
9989 }
9990 if meta.path.is_ident("extra_permissions") {
9991 // Comma-separated `codename:label` pairs.
9992 let s: LitStr = meta.value()?.parse()?;
9993 let raw = s.value();
9994 let mut pairs = Vec::new();
9995 for entry in raw.split(',') {
9996 let entry = entry.trim();
9997 if entry.is_empty() {
9998 continue;
9999 }
10000 let (codename, label) = match entry.split_once(':') {
10001 Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
10002 None => (entry.to_owned(), entry.to_owned()),
10003 };
10004 if codename.is_empty() {
10005 return Err(meta.error(
10006 "`extra_permissions` entries must be `codename:label` pairs",
10007 ));
10008 }
10009 pairs.push((codename, label));
10010 }
10011 if pairs.is_empty() {
10012 return Err(meta
10013 .error("`extra_permissions = \"…\"` must list at least one pair"));
10014 }
10015 out.extra_permissions = pairs;
10016 return Ok(());
10017 }
10018 if meta.path.is_ident("default_permissions") {
10019 // Comma-separated subset of the CRUD action set.
10020 // Empty means all four (the framework default).
10021 let s: LitStr = meta.value()?.parse()?;
10022 let raw = s.value();
10023 let mut actions: Vec<String> = Vec::new();
10024 for entry in raw.split(',') {
10025 let action = entry.trim().to_ascii_lowercase();
10026 if action.is_empty() {
10027 continue;
10028 }
10029 match action.as_str() {
10030 "add" | "change" | "delete" | "view" => {}
10031 other => {
10032 return Err(syn::Error::new(
10033 s.span(),
10034 format!(
10035 "unknown default_permissions action `{other}` — \
10036 expected one of `add`, `change`, `delete`, `view`"
10037 ),
10038 ));
10039 }
10040 }
10041 if !actions.contains(&action) {
10042 actions.push(action);
10043 }
10044 }
10045 if actions.is_empty() {
10046 return Err(syn::Error::new(
10047 s.span(),
10048 "`default_permissions = \"…\"` must list at least one action; \
10049 use `permissions = false` on the container if you want NO \
10050 permissions seeded for this model.",
10051 ));
10052 }
10053 out.default_permissions = actions;
10054 return Ok(());
10055 }
10056 if meta.path.is_ident("get_latest_by") {
10057 // The `-` prefix selects descending order.
10058 let s: LitStr = meta.value()?.parse()?;
10059 let raw = s.value();
10060 let trimmed = raw.trim();
10061 if trimmed.is_empty() {
10062 return Err(meta.error("`get_latest_by` must name a column"));
10063 }
10064 let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
10065 (stripped.to_owned(), true)
10066 } else if let Some(stripped) = trimmed.strip_prefix('+') {
10067 (stripped.to_owned(), false)
10068 } else {
10069 (trimmed.to_owned(), false)
10070 };
10071 if col.is_empty() {
10072 return Err(meta.error("`get_latest_by` must name a column"));
10073 }
10074 out.get_latest_by = Some((col, desc, s.span()));
10075 return Ok(());
10076 }
10077 if meta.path.is_ident("unique_together") {
10078 // Composite UNIQUE index. Two syntaxes:
10079 //
10080 // #[rustango(unique_together = "org_id, user_id")] — auto-derived name
10081 // #[rustango(unique_together(columns = "org_id, user_id", name = "x"))] — explicit name
10082 //
10083 // Both produce `CREATE UNIQUE INDEX <name> ON <table>
10084 // (col1, col2)`, where <name> defaults to
10085 // `<table>_<col1>_<col2>_uq` when not supplied.
10086 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
10087 out.indexes.push(IndexAttr {
10088 span: meta.path.span(),
10089 name,
10090 columns,
10091 unique: true,
10092 method: "btree".to_owned(),
10093 where_clause: None,
10094 include: Vec::new(),
10095 });
10096 return Ok(());
10097 }
10098 if meta.path.is_ident("index_together") {
10099 // Composite (non-unique) index. Two syntaxes
10100 // mirroring `unique_together`.
10101 //
10102 // #[rustango(index_together = "created_at, status")]
10103 // #[rustango(index_together(columns = "created_at, status", name = "x"))]
10104 let (columns, name) = parse_together_attr(&meta, "index_together")?;
10105 out.indexes.push(IndexAttr {
10106 span: meta.path.span(),
10107 name,
10108 columns,
10109 unique: false,
10110 method: "btree".to_owned(),
10111 where_clause: None,
10112 include: Vec::new(),
10113 });
10114 return Ok(());
10115 }
10116 if meta.path.is_ident("unique_when") {
10117 // Partial unique index. Issue #265 / T1.3.
10118 //
10119 // #[rustango(unique_when(
10120 // columns = "email",
10121 // condition = "deleted_at IS NULL",
10122 // name = "unique_active_email"
10123 // ))]
10124 //
10125 // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
10126 // on PG / SQLite (both ship partial indexes natively).
10127 // MySQL falls back to a plain UNIQUE index — the
10128 // condition is lost; document the limitation in the
10129 // generated migration.
10130 let mut columns: Option<Vec<String>> = None;
10131 let mut condition: Option<String> = None;
10132 let mut name: Option<String> = None;
10133 let mut include: Vec<String> = Vec::new();
10134 meta.parse_nested_meta(|inner| {
10135 if inner.path.is_ident("columns") {
10136 let s: LitStr = inner.value()?.parse()?;
10137 columns = Some(split_field_list(&s.value()));
10138 return Ok(());
10139 }
10140 if inner.path.is_ident("condition") {
10141 let s: LitStr = inner.value()?.parse()?;
10142 condition = Some(s.value());
10143 return Ok(());
10144 }
10145 if inner.path.is_ident("name") {
10146 let s: LitStr = inner.value()?.parse()?;
10147 name = Some(s.value());
10148 return Ok(());
10149 }
10150 if inner.path.is_ident("include") {
10151 // PG 11+ covering-index columns. Non-key columns
10152 // travel with the index leaf for index-only
10153 // scans. Dropped on MySQL/SQLite by the writer.
10154 let s: LitStr = inner.value()?.parse()?;
10155 include = split_field_list(&s.value());
10156 return Ok(());
10157 }
10158 Err(inner.error(
10159 "unknown unique_when attribute (supported: \
10160 `columns = \"...\"`, `condition = \"...\"`, \
10161 `name = \"...\"`, `include = \"...\"`)",
10162 ))
10163 })?;
10164 let columns = columns.ok_or_else(|| {
10165 meta.error("`unique_when(...)` requires `columns = \"...\"`")
10166 })?;
10167 let condition = condition.ok_or_else(|| {
10168 meta.error("`unique_when(...)` requires `condition = \"...\"`")
10169 })?;
10170 if columns.is_empty() {
10171 return Err(meta.error("`unique_when(columns = \"\")` is empty"));
10172 }
10173 out.indexes.push(IndexAttr {
10174 span: meta.path.span(),
10175 name,
10176 columns,
10177 unique: true,
10178 method: "btree".to_owned(),
10179 where_clause: Some(condition),
10180 include,
10181 });
10182 return Ok(());
10183 }
10184 if meta.path.is_ident("index_when") {
10185 // Non-unique partial index. Sibling of `unique_when`
10186 // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
10187 //
10188 // #[rustango(index_when(
10189 // columns = "status, created_at",
10190 // condition = "deleted_at IS NULL",
10191 // name = "active_status_created_idx"
10192 // ))]
10193 //
10194 // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
10195 // on PG / SQLite (both ship partial indexes natively).
10196 // MySQL has no native partial-index support — the writer
10197 // emits a plain CREATE INDEX and the condition is lost;
10198 // operators wanting that selectivity on MySQL should
10199 // declare a covering index plus an application-level
10200 // filter.
10201 let mut columns: Option<Vec<String>> = None;
10202 let mut condition: Option<String> = None;
10203 let mut name: Option<String> = None;
10204 let mut method: String = "btree".to_owned();
10205 let mut include: Vec<String> = Vec::new();
10206 meta.parse_nested_meta(|inner| {
10207 if inner.path.is_ident("columns") {
10208 let s: LitStr = inner.value()?.parse()?;
10209 columns = Some(split_field_list(&s.value()));
10210 return Ok(());
10211 }
10212 if inner.path.is_ident("condition") {
10213 let s: LitStr = inner.value()?.parse()?;
10214 condition = Some(s.value());
10215 return Ok(());
10216 }
10217 if inner.path.is_ident("name") {
10218 let s: LitStr = inner.value()?.parse()?;
10219 name = Some(s.value());
10220 return Ok(());
10221 }
10222 if inner.path.is_ident("method") {
10223 let s: LitStr = inner.value()?.parse()?;
10224 method = s.value();
10225 return Ok(());
10226 }
10227 if inner.path.is_ident("include") {
10228 // PG 11+ covering-index columns; non-key
10229 // columns travel with the index leaf. Dropped
10230 // on MySQL/SQLite.
10231 let s: LitStr = inner.value()?.parse()?;
10232 include = split_field_list(&s.value());
10233 return Ok(());
10234 }
10235 Err(inner.error(
10236 "unknown index_when attribute (supported: \
10237 `columns = \"...\"`, `condition = \"...\"`, \
10238 `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
10239 `include = \"...\"`)",
10240 ))
10241 })?;
10242 let columns = columns
10243 .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
10244 let condition = condition.ok_or_else(|| {
10245 meta.error("`index_when(...)` requires `condition = \"...\"`")
10246 })?;
10247 if columns.is_empty() {
10248 return Err(meta.error("`index_when(columns = \"\")` is empty"));
10249 }
10250 out.indexes.push(IndexAttr {
10251 span: meta.path.span(),
10252 name,
10253 columns,
10254 unique: false,
10255 method,
10256 where_clause: Some(condition),
10257 include,
10258 });
10259 return Ok(());
10260 }
10261 if meta.path.is_ident("index") {
10262 // Container-level composite index. Two syntaxes:
10263 // #[rustango(index = "col1, col2")] — bare, non-unique btree
10264 // #[rustango(index("col1, col2"))] — call form (same result)
10265 // #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
10266 // The call form takes the column list as a leading string
10267 // literal, then optional `unique` / `name = "..."` /
10268 // `method = "..."` flags (a leading literal can't compose
10269 // under `parse_nested_meta`, so the paren body is parsed by
10270 // hand). `unique_together` / `index_together` remain
10271 // aliases for the same feature.
10272 let cols_lit: LitStr;
10273 let mut unique = false;
10274 let mut name: Option<String> = None;
10275 let mut method = "btree".to_owned();
10276 if meta.input.peek(syn::token::Paren) {
10277 let content;
10278 syn::parenthesized!(content in meta.input);
10279 cols_lit = content.parse()?;
10280 while content.peek(syn::Token![,]) {
10281 content.parse::<syn::Token![,]>()?;
10282 if content.is_empty() {
10283 break;
10284 }
10285 let flag: syn::Ident = content.parse()?;
10286 if flag == "unique" {
10287 unique = true;
10288 } else if flag == "name" {
10289 content.parse::<syn::Token![=]>()?;
10290 let s: LitStr = content.parse()?;
10291 name = Some(s.value());
10292 } else if flag == "method" {
10293 content.parse::<syn::Token![=]>()?;
10294 let s: LitStr = content.parse()?;
10295 let v = s.value();
10296 match v.as_str() {
10297 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
10298 | "bloom" => method = v,
10299 other => {
10300 return Err(syn::Error::new(
10301 s.span(),
10302 format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
10303 ));
10304 }
10305 }
10306 } else {
10307 return Err(syn::Error::new(
10308 flag.span(),
10309 "unknown index flag (supported: `unique`, `name`, `method`)",
10310 ));
10311 }
10312 }
10313 } else {
10314 cols_lit = meta.value()?.parse()?;
10315 }
10316 let columns = split_field_list(&cols_lit.value());
10317 out.indexes.push(IndexAttr {
10318 span: meta.path.span(),
10319 name,
10320 columns,
10321 unique,
10322 method,
10323 where_clause: None,
10324 include: Vec::new(),
10325 });
10326 return Ok(());
10327 }
10328 if meta.path.is_ident("check") {
10329 // #[rustango(check(name = "…", expr = "…"))]
10330 let mut name: Option<String> = None;
10331 let mut expr: Option<String> = None;
10332 meta.parse_nested_meta(|inner| {
10333 if inner.path.is_ident("name") {
10334 let s: LitStr = inner.value()?.parse()?;
10335 name = Some(s.value());
10336 return Ok(());
10337 }
10338 if inner.path.is_ident("expr") {
10339 let s: LitStr = inner.value()?.parse()?;
10340 expr = Some(s.value());
10341 return Ok(());
10342 }
10343 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
10344 })?;
10345 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
10346 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
10347 out.checks.push(CheckAttr { name, expr });
10348 return Ok(());
10349 }
10350 if meta.path.is_ident("exclude") {
10351 // #[rustango(exclude(name = "…", using = "gist",
10352 // elements = "col WITH op, col WITH op",
10353 // where = "…"))]
10354 let mut name: Option<String> = None;
10355 let mut using: Option<String> = None;
10356 let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
10357 let mut where_clause: Option<String> = None;
10358 meta.parse_nested_meta(|inner| {
10359 if inner.path.is_ident("name") {
10360 let s: LitStr = inner.value()?.parse()?;
10361 name = Some(s.value());
10362 return Ok(());
10363 }
10364 if inner.path.is_ident("using") {
10365 let s: LitStr = inner.value()?.parse()?;
10366 using = Some(s.value());
10367 return Ok(());
10368 }
10369 if inner.path.is_ident("elements") {
10370 let s: LitStr = inner.value()?.parse()?;
10371 elements_raw = Some((s.value(), s.span()));
10372 return Ok(());
10373 }
10374 if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
10375 let s: LitStr = inner.value()?.parse()?;
10376 where_clause = Some(s.value());
10377 return Ok(());
10378 }
10379 Err(inner.error(
10380 "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
10381 ))
10382 })?;
10383 let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
10384 let using = using.unwrap_or_else(|| "gist".to_owned());
10385 let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
10386 meta.error(
10387 "exclude requires `elements = \"col WITH op, col WITH op\"`",
10388 )
10389 })?;
10390 // Parse `col WITH op` pairs separated by commas.
10391 let mut elements: Vec<(String, String)> = Vec::new();
10392 for pair in elements_str.split(',') {
10393 let pair = pair.trim();
10394 if pair.is_empty() {
10395 continue;
10396 }
10397 let mut split = pair.splitn(2, |c: char| c.is_whitespace());
10398 let col = split.next().unwrap_or("").trim();
10399 let rest = split.next().unwrap_or("").trim();
10400 // `WITH op` — case-insensitive on `WITH`, then op.
10401 let rest_lc = rest.to_ascii_lowercase();
10402 let op = rest_lc
10403 .strip_prefix("with")
10404 .map(|r| r.trim_start())
10405 .filter(|r| !r.is_empty())
10406 .map(|_| {
10407 // Pull the original-case op from `rest` after the
10408 // `WITH ` token (5 chars).
10409 rest[4..].trim_start().to_owned()
10410 });
10411 let Some(op) = op else {
10412 return Err(syn::Error::new(
10413 elements_span,
10414 format!(
10415 "exclude elements: `{pair}` must be `<col> WITH <op>` \
10416 (e.g. `room_id WITH =` or `during WITH &&`)"
10417 ),
10418 ));
10419 };
10420 if col.is_empty() || op.is_empty() {
10421 return Err(syn::Error::new(
10422 elements_span,
10423 format!(
10424 "exclude elements: `{pair}` must be `<col> WITH <op>` \
10425 (both sides non-empty)"
10426 ),
10427 ));
10428 }
10429 elements.push((col.to_owned(), op));
10430 }
10431 if elements.is_empty() {
10432 return Err(syn::Error::new(
10433 elements_span,
10434 "exclude requires at least one `col WITH op` element",
10435 ));
10436 }
10437 out.excludes.push(ExcludeAttr {
10438 name,
10439 using,
10440 elements,
10441 where_clause,
10442 });
10443 return Ok(());
10444 }
10445 if meta.path.is_ident("generic_fk") {
10446 let mut gfk = GenericFkAttr {
10447 name: String::new(),
10448 ct_column: String::new(),
10449 pk_column: String::new(),
10450 };
10451 meta.parse_nested_meta(|inner| {
10452 if inner.path.is_ident("name") {
10453 let s: LitStr = inner.value()?.parse()?;
10454 gfk.name = s.value();
10455 return Ok(());
10456 }
10457 if inner.path.is_ident("ct_column") {
10458 let s: LitStr = inner.value()?.parse()?;
10459 gfk.ct_column = s.value();
10460 return Ok(());
10461 }
10462 if inner.path.is_ident("pk_column") {
10463 let s: LitStr = inner.value()?.parse()?;
10464 gfk.pk_column = s.value();
10465 return Ok(());
10466 }
10467 Err(inner.error(
10468 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10469 ))
10470 })?;
10471 if gfk.name.is_empty() {
10472 return Err(meta.error("generic_fk requires `name = \"...\"`"));
10473 }
10474 if gfk.ct_column.is_empty() {
10475 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10476 }
10477 if gfk.pk_column.is_empty() {
10478 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10479 }
10480 out.generic_fks.push(gfk);
10481 return Ok(());
10482 }
10483 if meta.path.is_ident("fk_composite") {
10484 let mut fk = CompositeFkAttr {
10485 name: String::new(),
10486 to: String::new(),
10487 from: Vec::new(),
10488 on: Vec::new(),
10489 };
10490 meta.parse_nested_meta(|inner| {
10491 if inner.path.is_ident("name") {
10492 let s: LitStr = inner.value()?.parse()?;
10493 fk.name = s.value();
10494 return Ok(());
10495 }
10496 if inner.path.is_ident("to") {
10497 let s: LitStr = inner.value()?.parse()?;
10498 fk.to = s.value();
10499 return Ok(());
10500 }
10501 // `on = ("col1", "col2", ...)` — parse a parenthesised
10502 // comma-list of string literals.
10503 if inner.path.is_ident("on") || inner.path.is_ident("from") {
10504 let value = inner.value()?;
10505 let content;
10506 syn::parenthesized!(content in value);
10507 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10508 content.parse_terminated(
10509 |p| p.parse::<syn::LitStr>(),
10510 syn::Token![,],
10511 )?;
10512 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10513 if inner.path.is_ident("on") {
10514 fk.on = cols;
10515 } else {
10516 fk.from = cols;
10517 }
10518 return Ok(());
10519 }
10520 Err(inner.error(
10521 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10522 ))
10523 })?;
10524 if fk.name.is_empty() {
10525 return Err(meta.error("fk_composite requires `name = \"...\"`"));
10526 }
10527 if fk.to.is_empty() {
10528 return Err(meta.error("fk_composite requires `to = \"...\"`"));
10529 }
10530 if fk.from.is_empty() || fk.on.is_empty() {
10531 return Err(meta.error(
10532 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10533 ));
10534 }
10535 if fk.from.len() != fk.on.len() {
10536 return Err(meta.error(format!(
10537 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10538 fk.from.len(),
10539 fk.on.len(),
10540 )));
10541 }
10542 out.composite_fks.push(fk);
10543 return Ok(());
10544 }
10545 if meta.path.is_ident("m2m") {
10546 let mut m2m = M2MAttr {
10547 name: String::new(),
10548 to: String::new(),
10549 through: String::new(),
10550 src: String::new(),
10551 dst: String::new(),
10552 auto_create: true,
10553 };
10554 meta.parse_nested_meta(|inner| {
10555 if inner.path.is_ident("name") {
10556 let s: LitStr = inner.value()?.parse()?;
10557 m2m.name = s.value();
10558 return Ok(());
10559 }
10560 if inner.path.is_ident("to") {
10561 let s: LitStr = inner.value()?.parse()?;
10562 m2m.to = s.value();
10563 return Ok(());
10564 }
10565 if inner.path.is_ident("through") {
10566 let s: LitStr = inner.value()?.parse()?;
10567 m2m.through = s.value();
10568 return Ok(());
10569 }
10570 if inner.path.is_ident("src") {
10571 let s: LitStr = inner.value()?.parse()?;
10572 m2m.src = s.value();
10573 return Ok(());
10574 }
10575 if inner.path.is_ident("dst") {
10576 let s: LitStr = inner.value()?.parse()?;
10577 m2m.dst = s.value();
10578 return Ok(());
10579 }
10580 if inner.path.is_ident("auto_create") {
10581 let lit: syn::LitBool = inner.value()?.parse()?;
10582 m2m.auto_create = lit.value;
10583 return Ok(());
10584 }
10585 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10586 })?;
10587 if m2m.name.is_empty() {
10588 return Err(meta.error("m2m requires `name = \"...\"`"));
10589 }
10590 if m2m.to.is_empty() {
10591 return Err(meta.error("m2m requires `to = \"...\"`"));
10592 }
10593 if m2m.through.is_empty() {
10594 return Err(meta.error("m2m requires `through = \"...\"`"));
10595 }
10596 if m2m.src.is_empty() {
10597 return Err(meta.error("m2m requires `src = \"...\"`"));
10598 }
10599 if m2m.dst.is_empty() {
10600 return Err(meta.error("m2m requires `dst = \"...\"`"));
10601 }
10602 out.m2m.push(m2m);
10603 return Ok(());
10604 }
10605 if meta.path.is_ident("generic_m2m") {
10606 let mut gm = GenericM2MAttr {
10607 name: String::new(),
10608 through: String::new(),
10609 pk_column: String::new(),
10610 ct_column: String::new(),
10611 related_column: String::new(),
10612 };
10613 meta.parse_nested_meta(|inner| {
10614 let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10615 let s: LitStr = inner.value()?.parse()?;
10616 Ok(s.value())
10617 };
10618 if inner.path.is_ident("name") {
10619 gm.name = field(&inner)?;
10620 return Ok(());
10621 }
10622 if inner.path.is_ident("through") {
10623 gm.through = field(&inner)?;
10624 return Ok(());
10625 }
10626 if inner.path.is_ident("pk_column") {
10627 gm.pk_column = field(&inner)?;
10628 return Ok(());
10629 }
10630 if inner.path.is_ident("ct_column") {
10631 gm.ct_column = field(&inner)?;
10632 return Ok(());
10633 }
10634 if inner.path.is_ident("related_column") {
10635 gm.related_column = field(&inner)?;
10636 return Ok(());
10637 }
10638 Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10639 })?;
10640 for (val, label) in [
10641 (&gm.name, "name"),
10642 (&gm.through, "through"),
10643 (&gm.pk_column, "pk_column"),
10644 (&gm.ct_column, "ct_column"),
10645 (&gm.related_column, "related_column"),
10646 ] {
10647 if val.is_empty() {
10648 return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10649 }
10650 }
10651 out.generic_m2m.push(gm);
10652 return Ok(());
10653 }
10654 Err(meta.error("unknown rustango container attribute"))
10655 })?;
10656 }
10657 Ok(out)
10658}
10659
10660/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10661/// owned field names, trimming whitespace and skipping empty entries.
10662/// Field-name validation against the model is done by the caller.
10663fn split_field_list(raw: &str) -> Vec<String> {
10664 raw.split(',')
10665 .map(str::trim)
10666 .filter(|s| !s.is_empty())
10667 .map(str::to_owned)
10668 .collect()
10669}
10670
10671/// Shared parser for `unique_together` and `index_together` container
10672/// attrs. Accepts both shapes:
10673///
10674/// * `attr = "col1, col2"` — auto-derived index name.
10675/// * `attr(columns = "col1, col2", name = "...")` — explicit name.
10676///
10677/// Returns `(columns, name)`.
10678fn parse_together_attr(
10679 meta: &syn::meta::ParseNestedMeta<'_>,
10680 attr: &str,
10681) -> syn::Result<(Vec<String>, Option<String>)> {
10682 // Disambiguate by whether the next token is `=` (key-value) or
10683 // `(` (parenthesized).
10684 if meta.input.peek(syn::Token![=]) {
10685 let cols_lit: LitStr = meta.value()?.parse()?;
10686 let columns = split_field_list(&cols_lit.value());
10687 check_together_columns(meta, attr, &columns)?;
10688 return Ok((columns, None));
10689 }
10690 let mut columns: Option<Vec<String>> = None;
10691 let mut name: Option<String> = None;
10692 meta.parse_nested_meta(|inner| {
10693 if inner.path.is_ident("columns") {
10694 let s: LitStr = inner.value()?.parse()?;
10695 columns = Some(split_field_list(&s.value()));
10696 return Ok(());
10697 }
10698 if inner.path.is_ident("name") {
10699 let s: LitStr = inner.value()?.parse()?;
10700 name = Some(s.value());
10701 return Ok(());
10702 }
10703 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10704 })?;
10705 let columns = columns.ok_or_else(|| {
10706 meta.error(format!(
10707 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10708 ))
10709 })?;
10710 check_together_columns(meta, attr, &columns)?;
10711 Ok((columns, name))
10712}
10713
10714fn check_together_columns(
10715 meta: &syn::meta::ParseNestedMeta<'_>,
10716 attr: &str,
10717 columns: &[String],
10718) -> syn::Result<()> {
10719 if columns.len() < 2 {
10720 let single = if attr == "unique_together" {
10721 "#[rustango(unique)] on the field"
10722 } else {
10723 "#[rustango(index)] on the field"
10724 };
10725 return Err(meta.error(format!(
10726 "{attr} expects two or more columns; for a single-column equivalent use {single}",
10727 )));
10728 }
10729 Ok(())
10730}
10731
10732/// Parse the fieldsets DSL: pipe-separated sections, optional
10733/// `"Title:"` prefix on each, comma-separated field names after.
10734/// Examples:
10735/// * `"name, office"` → one untitled section with two fields
10736/// * `"Identity: name, office | Metadata: created_at"` → two titled
10737/// sections
10738///
10739/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10740fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10741 raw.split('|')
10742 .map(str::trim)
10743 .filter(|s| !s.is_empty())
10744 .map(|section| {
10745 // Split off an optional `Title:` prefix (first colon).
10746 let (title, rest) = match section.split_once(':') {
10747 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10748 _ => (String::new(), section),
10749 };
10750 let fields = split_field_list(rest);
10751 (title, fields)
10752 })
10753 .collect()
10754}
10755
10756/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10757/// comma-separated entry maps a target field to one or more source
10758/// fields joined with `+`. Whitespace around tokens is trimmed.
10759/// Entries missing `:` or with empty target/source lists are dropped.
10760fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10761 raw.split(',')
10762 .map(str::trim)
10763 .filter(|s| !s.is_empty())
10764 .filter_map(|entry| {
10765 let (target, sources_raw) = entry.split_once(':')?;
10766 let target = target.trim().to_owned();
10767 if target.is_empty() {
10768 return None;
10769 }
10770 let sources: Vec<String> = sources_raw
10771 .split('+')
10772 .map(|s| s.trim().to_owned())
10773 .filter(|s| !s.is_empty())
10774 .collect();
10775 if sources.is_empty() {
10776 return None;
10777 }
10778 Some((target, sources))
10779 })
10780 .collect()
10781}
10782
10783/// Parse `formfield_overrides` — `"field:widget,field2:widget2"`
10784/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10785/// and empty halves drop silently — the macro layer only enforces shape,
10786/// not field-name vs. widget-name validity (those checks happen at
10787/// `AdminConfig` consumption time). Issue #359.
10788fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10789 raw.split(',')
10790 .map(str::trim)
10791 .filter(|s| !s.is_empty())
10792 .filter_map(|entry| {
10793 let (field, widget) = entry.split_once(':')?;
10794 let field = field.trim().to_owned();
10795 let widget = widget.trim().to_owned();
10796 if field.is_empty() || widget.is_empty() {
10797 return None;
10798 }
10799 Some((field, widget))
10800 })
10801 .collect()
10802}
10803
10804/// Parse an ordering list — `"name"` is ASC, `"-name"` is DESC.
10805/// Returns `(field_name, desc)` pairs in the same order as the input.
10806fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10807 raw.split(',')
10808 .map(str::trim)
10809 .filter(|s| !s.is_empty())
10810 .map(|spec| {
10811 spec.strip_prefix('-')
10812 .map_or((spec.to_owned(), false), |rest| {
10813 (rest.trim().to_owned(), true)
10814 })
10815 })
10816 .collect()
10817}
10818
10819struct FieldAttrs {
10820 column: Option<String>,
10821 primary_key: bool,
10822 fk: Option<String>,
10823 o2o: Option<String>,
10824 on: Option<String>,
10825 /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10826 /// "set_default" | "no_action")]` — what happens to this row when
10827 /// the row it points at is deleted.
10828 /// Only meaningful on a `ForeignKey<T>` field or with `fk` / `o2o`
10829 /// set; the macro errors at compile time if applied to a
10830 /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10831 /// DDL writer renders `ON DELETE <action>` after the constraint
10832 /// clause when this is `Some`; `None` falls back to the database
10833 /// default (NO ACTION on every backend rustango supports).
10834 on_delete: Option<String>,
10835 /// `#[rustango(related_name = "...")]` — per-FK
10836 /// reverse-accessor override. When set, the derive emits
10837 /// `Parent::<related_name>[_pool]` instead of the container-level
10838 /// `default_related_name` or the `<child_snake>_set[_pool]`
10839 /// fallback. Only meaningful when `fk` / `o2o` is also set;
10840 /// silently ignored on non-FK fields. Follow-up to #816.
10841 /// `"+"` emits none, for a parent defined in another crate.
10842 related_name: Option<String>,
10843 max_length: Option<u32>,
10844 /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10845 /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10846 /// unconstrained `vector` column.
10847 vector_dims: Option<u32>,
10848 /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10849 /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10850 /// unconstrained `geometry(Point)` column (SRID 0).
10851 geometry_srid: Option<u32>,
10852 min: Option<i64>,
10853 max: Option<i64>,
10854 default: Option<String>,
10855 /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10856 /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10857 /// "gen_random_uuid()"`. The Rust field type must be
10858 /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10859 /// INSERTs so the DB DEFAULT fires.
10860 auto_uuid: bool,
10861 /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10862 /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10863 /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10864 /// the field is `Auto::Unset`, then bound as a normal parameter
10865 /// rather than relying on a per-dialect DB function. Issue #823
10866 /// (Eloquent `HasUuids`).
10867 ///
10868 /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10869 /// Composes with every backend (PG / MySQL / SQLite) — no
10870 /// `gen_random_uuid()` requirement on the database.
10871 default_uuid_v7: bool,
10872 /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10873 /// Server-set on insert, immutable from app code afterwards.
10874 /// Implies `auto + default = "now()"`. Field type must be
10875 /// `DateTime<Utc>`.
10876 auto_now_add: bool,
10877 /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10878 /// every insert AND every update. Implies `auto + default =
10879 /// "now()"`; the macro additionally rewrites `update_on` /
10880 /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10881 /// field value.
10882 auto_now: bool,
10883 /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10884 /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10885 /// `soft_delete_on(executor)` and `restore_on(executor)`
10886 /// methods on the model.
10887 soft_delete: bool,
10888 /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10889 /// the column in the generated DDL.
10890 unique: bool,
10891 /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10892 /// generates a `CREATE INDEX` for this column. `unique` here means
10893 /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10894 index: bool,
10895 index_unique: bool,
10896 index_name: Option<String>,
10897 /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10898 /// `"btree"`. Issue #34.
10899 index_method: String,
10900 /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10901 /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10902 /// the macro skips this column from every INSERT and UPDATE
10903 /// path, so the database always recomputes the value from
10904 /// `EXPR`. Backlog item #35.
10905 generated_as: Option<String>,
10906 /// `#[rustango(help_text = "…")]` — help text
10907 /// rendered below the admin form's input. Threaded into
10908 /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10909 /// layers can read it.
10910 help_text: Option<String>,
10911 /// `#[rustango(choices = "value:Label, value:Label")]` — the
10912 /// enumerated allowed values. Threaded into `FieldSchema::choices`
10913 /// as a `&'static [(&'static str, &'static str)]` slice. When
10914 /// present, the admin form renders a `<select>` instead of `<input>`
10915 /// and the validator rejects values not in the list. Only meaningful
10916 /// for `FieldType::String`; the macro errors at compile time if
10917 /// applied to a non-string field.
10918 choices: Option<Vec<(String, String)>>,
10919 /// `#[rustango(db_comment = "…")]` — DB-side column
10920 /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10921 /// the comment in CREATE TABLE; Postgres emits a separate
10922 /// `COMMENT ON COLUMN` statement after the table is created;
10923 /// SQLite silently drops the value (no native column comments).
10924 db_comment: Option<String>,
10925 /// `#[rustango(verbose_name = "…")]` — human-readable
10926 /// label for the field. Threaded into `FieldSchema::verbose_name`
10927 /// so admin column headers, form labels, and other display
10928 /// surfaces can prefer the friendly caption over the Rust
10929 /// identifier. `None` means renderers fall back to the field name.
10930 verbose_name: Option<String>,
10931 /// `#[rustango(editable = false)]` — opt out of
10932 /// auto-generated form rendering. Defaults to `true` so existing
10933 /// fields keep their current admin / form behavior; setting
10934 /// `false` removes the field from the admin change-form entirely
10935 /// (the value is still visible on detail / list views, just not
10936 /// editable).
10937 editable: bool,
10938 /// `#[rustango(blank)]` / `#[rustango(blank = true)]` —
10939 /// "form may submit empty even when DB is NOT NULL". Threaded into
10940 /// `FieldSchema::blank`. Defaults to `false`.
10941 blank: bool,
10942 /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10943 /// case-insensitive text column. Threaded into
10944 /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10945 /// fields; the macro errors at derive time if applied elsewhere.
10946 case_insensitive: bool,
10947 /// `#[rustango(validators = "email,url")]` — the
10948 /// model-level validator chain. Comma-separated names that
10949 /// dispatch to the `validators::*` family in `validate_value`.
10950 /// Empty by default; fires on every typed INSERT/UPDATE.
10951 validators: Vec<String>,
10952}
10953
10954fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10955 let mut out = FieldAttrs {
10956 column: None,
10957 primary_key: false,
10958 fk: None,
10959 o2o: None,
10960 on: None,
10961 on_delete: None,
10962 related_name: None,
10963 max_length: None,
10964 vector_dims: None,
10965 geometry_srid: None,
10966 min: None,
10967 max: None,
10968 default: None,
10969 auto_uuid: false,
10970 default_uuid_v7: false,
10971 auto_now_add: false,
10972 auto_now: false,
10973 soft_delete: false,
10974 unique: false,
10975 index: false,
10976 index_unique: false,
10977 index_name: None,
10978 index_method: "btree".to_owned(),
10979 generated_as: None,
10980 help_text: None,
10981 choices: None,
10982 db_comment: None,
10983 verbose_name: None,
10984 editable: true,
10985 blank: false,
10986 case_insensitive: false,
10987 validators: Vec::new(),
10988 };
10989 for attr in &field.attrs {
10990 if !attr.path().is_ident("rustango") {
10991 continue;
10992 }
10993 attr.parse_nested_meta(|meta| {
10994 if meta.path.is_ident("column") {
10995 let s: LitStr = meta.value()?.parse()?;
10996 let name = s.value();
10997 validate_sql_identifier(&name, "column", s.span())?;
10998 out.column = Some(name);
10999 return Ok(());
11000 }
11001 if meta.path.is_ident("primary_key") {
11002 out.primary_key = true;
11003 return Ok(());
11004 }
11005 if meta.path.is_ident("fk") {
11006 let s: LitStr = meta.value()?.parse()?;
11007 out.fk = Some(s.value());
11008 return Ok(());
11009 }
11010 if meta.path.is_ident("o2o") {
11011 let s: LitStr = meta.value()?.parse()?;
11012 out.o2o = Some(s.value());
11013 return Ok(());
11014 }
11015 if meta.path.is_ident("on") {
11016 let s: LitStr = meta.value()?.parse()?;
11017 out.on = Some(s.value());
11018 return Ok(());
11019 }
11020 if meta.path.is_ident("on_delete") {
11021 let s: LitStr = meta.value()?.parse()?;
11022 let raw = s.value();
11023 let normalized = raw.trim().to_ascii_lowercase();
11024 // Validate at parse time so the user gets a clear span
11025 // rather than a downstream compile error in the emit.
11026 match normalized.as_str() {
11027 "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
11028 _ => {
11029 return Err(syn::Error::new(
11030 s.span(),
11031 format!(
11032 "unknown on_delete action `{raw}`; expected one of \
11033 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
11034 ),
11035 ));
11036 }
11037 }
11038 out.on_delete = Some(normalized);
11039 return Ok(());
11040 }
11041 if meta.path.is_ident("related_name") {
11042 let s: LitStr = meta.value()?.parse()?;
11043 let raw = s.value();
11044 if raw.trim().is_empty() {
11045 return Err(syn::Error::new(
11046 s.span(),
11047 "`related_name` must be a non-empty identifier",
11048 ));
11049 }
11050 // Validate as a Rust-ident shape — the value becomes
11051 // a method name on the parent type. Same rule as
11052 // `default_related_name` so the two surfaces match.
11053 // `"+"` emits no accessor at all.
11054 let snake = raw
11055 .chars()
11056 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
11057 && !raw.starts_with(char::is_numeric);
11058 if !snake && raw != NO_REVERSE_ACCESSOR {
11059 return Err(syn::Error::new(
11060 s.span(),
11061 "`related_name` must be snake_case ASCII (lowercase letters, \
11062 digits, underscores; no leading digit), or \"+\" for none",
11063 ));
11064 }
11065 out.related_name = Some(raw);
11066 return Ok(());
11067 }
11068 if meta.path.is_ident("max_length") {
11069 let lit: syn::LitInt = meta.value()?.parse()?;
11070 out.max_length = Some(lit.base10_parse::<u32>()?);
11071 return Ok(());
11072 }
11073 // `#[rustango(vector(dims = N))]` — pgvector column
11074 // dimension (#824). Nested-meta form so it reads like the
11075 // other typed-column attrs.
11076 if meta.path.is_ident("vector") {
11077 meta.parse_nested_meta(|inner| {
11078 if inner.path.is_ident("dims") {
11079 let lit: syn::LitInt = inner.value()?.parse()?;
11080 out.vector_dims = Some(lit.base10_parse::<u32>()?);
11081 return Ok(());
11082 }
11083 Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
11084 })?;
11085 return Ok(());
11086 }
11087 // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
11088 // (#443). Nested-meta form, mirroring `vector(dims = N)`.
11089 if meta.path.is_ident("geometry") {
11090 meta.parse_nested_meta(|inner| {
11091 if inner.path.is_ident("srid") {
11092 let lit: syn::LitInt = inner.value()?.parse()?;
11093 out.geometry_srid = Some(lit.base10_parse::<u32>()?);
11094 return Ok(());
11095 }
11096 Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
11097 })?;
11098 return Ok(());
11099 }
11100 if meta.path.is_ident("min") {
11101 out.min = Some(parse_signed_i64(&meta)?);
11102 return Ok(());
11103 }
11104 if meta.path.is_ident("max") {
11105 out.max = Some(parse_signed_i64(&meta)?);
11106 return Ok(());
11107 }
11108 if meta.path.is_ident("default") {
11109 let s: LitStr = meta.value()?.parse()?;
11110 out.default = Some(s.value());
11111 return Ok(());
11112 }
11113 if meta.path.is_ident("generated_as") {
11114 let s: LitStr = meta.value()?.parse()?;
11115 out.generated_as = Some(s.value());
11116 return Ok(());
11117 }
11118 if meta.path.is_ident("help_text") {
11119 let s: LitStr = meta.value()?.parse()?;
11120 out.help_text = Some(s.value());
11121 return Ok(());
11122 }
11123 if meta.path.is_ident("choices") {
11124 let s: LitStr = meta.value()?.parse()?;
11125 let raw = s.value();
11126 let mut pairs: Vec<(String, String)> = Vec::new();
11127 for chunk in raw.split(',') {
11128 let chunk = chunk.trim();
11129 if chunk.is_empty() {
11130 continue;
11131 }
11132 let (value, label) = match chunk.split_once(':') {
11133 Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
11134 None => (chunk.to_owned(), chunk.to_owned()),
11135 };
11136 if value.is_empty() {
11137 return Err(syn::Error::new(
11138 s.span(),
11139 "`choices` entry has empty value before `:`",
11140 ));
11141 }
11142 pairs.push((value, label));
11143 }
11144 if pairs.is_empty() {
11145 return Err(syn::Error::new(
11146 s.span(),
11147 "`choices = \"…\"` must contain at least one value",
11148 ));
11149 }
11150 out.choices = Some(pairs);
11151 return Ok(());
11152 }
11153 if meta.path.is_ident("db_comment") {
11154 let s: LitStr = meta.value()?.parse()?;
11155 out.db_comment = Some(s.value());
11156 return Ok(());
11157 }
11158 if meta.path.is_ident("verbose_name") {
11159 let s: LitStr = meta.value()?.parse()?;
11160 out.verbose_name = Some(s.value());
11161 return Ok(());
11162 }
11163 if meta.path.is_ident("editable") {
11164 // Two forms accepted:
11165 // #[rustango(editable = false)] / true — explicit
11166 // #[rustango(editable)] — flag form (= true, the
11167 // default, so harmless; included for symmetry)
11168 if let Ok(v) = meta.value() {
11169 let lit: syn::LitBool = v.parse()?;
11170 out.editable = lit.value;
11171 } else {
11172 out.editable = true;
11173 }
11174 return Ok(());
11175 }
11176 if meta.path.is_ident("blank") {
11177 // Two forms accepted:
11178 // #[rustango(blank)] — flag form, true
11179 // #[rustango(blank = false)] / true — explicit
11180 if let Ok(v) = meta.value() {
11181 let lit: syn::LitBool = v.parse()?;
11182 out.blank = lit.value;
11183 } else {
11184 out.blank = true;
11185 }
11186 return Ok(());
11187 }
11188 if meta.path.is_ident("citext") {
11189 // Case-insensitive text column (#344). Two forms:
11190 // #[rustango(citext)] — flag form, true
11191 // #[rustango(citext = true)] — explicit
11192 // #[rustango(citext = false)] — explicit opt-out
11193 // String-only validation lives in the field-type
11194 // emitter (the FieldType discriminant is computed in
11195 // `detect_type`); the macro records the flag and the
11196 // DDL writer emits dialect-specific COLLATE / CITEXT.
11197 if let Ok(v) = meta.value() {
11198 let lit: syn::LitBool = v.parse()?;
11199 out.case_insensitive = lit.value;
11200 } else {
11201 out.case_insensitive = true;
11202 }
11203 return Ok(());
11204 }
11205 if meta.path.is_ident("validators") {
11206 let s: LitStr = meta.value()?.parse()?;
11207 let raw = s.value();
11208 out.validators = raw
11209 .split(',')
11210 .map(str::trim)
11211 .filter(|s| !s.is_empty())
11212 .map(str::to_owned)
11213 .collect();
11214 if out.validators.is_empty() {
11215 return Err(syn::Error::new(
11216 s.span(),
11217 "`validators = \"…\"` must list at least one name",
11218 ));
11219 }
11220 return Ok(());
11221 }
11222 if meta.path.is_ident("auto_uuid") {
11223 out.auto_uuid = true;
11224 // Implied: PK + auto + DEFAULT gen_random_uuid().
11225 // Each is also explicitly settable; the explicit
11226 // value wins if conflicting.
11227 out.primary_key = true;
11228 if out.default.is_none() {
11229 out.default = Some("gen_random_uuid()".into());
11230 }
11231 return Ok(());
11232 }
11233 if meta.path.is_ident("default_uuid_v7") {
11234 // Backend-neutral counterpart of `auto_uuid` — issue #823.
11235 // No SQL DEFAULT (the macro fills the value Rust-side
11236 // before binding); just mark the field as PK + Auto
11237 // so the insert path is the `Auto::Unset → generate`
11238 // branch.
11239 out.default_uuid_v7 = true;
11240 out.primary_key = true;
11241 return Ok(());
11242 }
11243 if meta.path.is_ident("auto_now_add") {
11244 out.auto_now_add = true;
11245 if out.default.is_none() {
11246 out.default = Some("now()".into());
11247 }
11248 return Ok(());
11249 }
11250 if meta.path.is_ident("auto_now") {
11251 out.auto_now = true;
11252 if out.default.is_none() {
11253 out.default = Some("now()".into());
11254 }
11255 return Ok(());
11256 }
11257 if meta.path.is_ident("soft_delete") {
11258 out.soft_delete = true;
11259 return Ok(());
11260 }
11261 if meta.path.is_ident("unique") {
11262 out.unique = true;
11263 return Ok(());
11264 }
11265 if meta.path.is_ident("index") {
11266 out.index = true;
11267 // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
11268 if meta.input.peek(syn::token::Paren) {
11269 meta.parse_nested_meta(|inner| {
11270 if inner.path.is_ident("unique") {
11271 out.index_unique = true;
11272 return Ok(());
11273 }
11274 if inner.path.is_ident("name") {
11275 let s: LitStr = inner.value()?.parse()?;
11276 out.index_name = Some(s.value());
11277 return Ok(());
11278 }
11279 if inner.path.is_ident("method") {
11280 let s: LitStr = inner.value()?.parse()?;
11281 let v = s.value();
11282 match v.as_str() {
11283 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
11284 out.index_method = v;
11285 }
11286 other => {
11287 return Err(inner.error(format!(
11288 "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
11289 )));
11290 }
11291 }
11292 return Ok(());
11293 }
11294 Err(inner.error(
11295 "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
11296 ))
11297 })?;
11298 }
11299 return Ok(());
11300 }
11301 Err(meta.error("unknown rustango field attribute"))
11302 })?;
11303 }
11304 Ok(out)
11305}
11306
11307/// Parse a signed integer literal, accepting optional leading `-`.
11308fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
11309 let expr: syn::Expr = meta.value()?.parse()?;
11310 match expr {
11311 syn::Expr::Lit(syn::ExprLit {
11312 lit: syn::Lit::Int(lit),
11313 ..
11314 }) => lit.base10_parse::<i64>(),
11315 syn::Expr::Unary(syn::ExprUnary {
11316 op: syn::UnOp::Neg(_),
11317 expr,
11318 ..
11319 }) => {
11320 if let syn::Expr::Lit(syn::ExprLit {
11321 lit: syn::Lit::Int(lit),
11322 ..
11323 }) = *expr
11324 {
11325 let v: i64 = lit.base10_parse()?;
11326 Ok(-v)
11327 } else {
11328 Err(syn::Error::new_spanned(expr, "expected integer literal"))
11329 }
11330 }
11331 other => Err(syn::Error::new_spanned(
11332 other,
11333 "expected integer literal (signed)",
11334 )),
11335 }
11336}
11337
11338struct FieldInfo<'a> {
11339 ident: &'a syn::Ident,
11340 column: String,
11341 primary_key: bool,
11342 /// `true` when the Rust type was `Auto<T>` — the INSERT path will
11343 /// skip this column when `Auto::Unset` and emit it under
11344 /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
11345 auto: bool,
11346 /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
11347 /// the `Column::Value` associated type for typed-column tokens.
11348 value_ty: &'a Type,
11349 /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
11350 field_type_tokens: TokenStream2,
11351 schema: TokenStream2,
11352 from_row_init: TokenStream2,
11353 /// Variant of [`Self::from_row_init`] that reads the column via
11354 /// `format!("{prefix}__{col}")` so a model can be decoded out of
11355 /// the aliased columns of a JOINed row. Drives slice 9.0d's
11356 /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
11357 /// helper that `select_related` calls when stitching loaded FKs.
11358 from_aliased_row_init: TokenStream2,
11359 /// MySQL twins of the two inits above. They differ only for a UUID
11360 /// column, which MySQL keeps as `CHAR(36)` text (#1733).
11361 from_row_init_my: TokenStream2,
11362 from_aliased_row_init_my: TokenStream2,
11363 /// A bare or optional `Uuid` field.
11364 uuid_column: bool,
11365 /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
11366 /// relation helper emit (`Author::<child>_set`) needs to know `T`
11367 /// to point the generated method at the right child model.
11368 fk_inner: Option<Type>,
11369 /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
11370 /// `kind` (since ForeignKey detection sets `kind` to K's
11371 /// underlying type) but stored separately for clarity at the
11372 /// `FkRelation` construction site, which only sees the FK's
11373 /// surface fields.
11374 fk_pk_kind: DetectedKind,
11375 /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
11376 /// the bare `ForeignKey<T, K>`. Routes the load_related and
11377 /// fk_pk_access emitters to wrap assignments / accessors in
11378 /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
11379 /// FK column compiles end-to-end. The DDL writer reads this off
11380 /// the field schema (`nullable` flag); the macro just needs to
11381 /// keep the Rust-side codegen consistent.
11382 nullable: bool,
11383 /// `true` when this column was marked `#[rustango(auto_now)]` —
11384 /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
11385 /// column instead of the user-supplied value, so `updated_at`
11386 /// always reflects the latest write without the caller having
11387 /// to remember to set it.
11388 auto_now: bool,
11389 /// `true` when this column was marked `#[rustango(auto_now_add)]`
11390 /// — the column is server-set on INSERT (DB DEFAULT) and
11391 /// **immutable** afterwards. `update_on` / `save_on` skip the
11392 /// column entirely so a stale `created_at` value in memory never
11393 /// rewrites the persisted timestamp.
11394 auto_now_add: bool,
11395 /// `true` when this column was marked `#[rustango(soft_delete)]`.
11396 /// Triggers emission of `soft_delete_on(executor)` and
11397 /// `restore_on(executor)` on the model's inherent impl. There is
11398 /// at most one such column per model — emission asserts this.
11399 soft_delete: bool,
11400 /// `Some` when this column was marked
11401 /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
11402 /// every INSERT and UPDATE path; the database recomputes the
11403 /// value from `EXPR`. Backlog item #35.
11404 generated_as: Option<String>,
11405 /// `true` when this column was marked
11406 /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
11407 /// emit an `insert_push` that auto-fills an `Auto::Unset` value
11408 /// with `Uuid::now_v7()` before binding, so the PK is generated
11409 /// Rust-side and the column is always present in the INSERT
11410 /// statement (no DB DEFAULT requirement). Issue #823.
11411 default_uuid_v7: bool,
11412 /// `Some` when this FK field carried `#[rustango(related_name =
11413 /// "...")]`. Threaded into [`FkRelation::related_name`] and
11414 /// consumed by [`reverse_helper_tokens`] to override the default
11415 /// `<child_snake>_set` accessor name. Follow-up to #816.
11416 related_name: Option<String>,
11417}
11418
11419/// Reject table names that won't survive SQL identifier
11420/// derivation downstream. Postgres' regular-identifier rule
11421/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
11422/// through the framework's unquoted FK / index / constraint name
11423/// emission without surprises. We also disallow leading-digit and
11424/// the empty string for clarity.
11425///
11426/// Reserved-word collisions (`select`, `from`, …) aren't flagged
11427/// here — those produce a runtime error from the SQL parser,
11428/// which is loud enough; statically enumerating reserved words
11429/// across the three supported dialects is more friction than help.
11430///
11431/// Backlog item #65.
11432fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11433 validate_sql_identifier(name, "table", span)
11434}
11435
11436/// Reject SQL identifiers that compile but break downstream SQL
11437/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11438/// `kind` is "table" / "column" — used for the error message so users
11439/// see which attribute caused the failure.
11440fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11441 if name.is_empty() {
11442 return Err(syn::Error::new(
11443 span,
11444 format!("`{kind} = \"\"` is not a valid SQL identifier"),
11445 ));
11446 }
11447 let mut chars = name.chars();
11448 let first = chars.next().unwrap();
11449 if !(first.is_ascii_alphabetic() || first == '_') {
11450 return Err(syn::Error::new(
11451 span,
11452 format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11453 ));
11454 }
11455 for c in chars {
11456 if !(c.is_ascii_alphanumeric() || c == '_') {
11457 return Err(syn::Error::new(
11458 span,
11459 format!(
11460 "{kind} name `{name}` contains invalid character {c:?} — \
11461 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11462 Hyphens in particular break FK / index name derivation \
11463 downstream; use underscores instead (e.g. `{}`)",
11464 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11465 ),
11466 ));
11467 }
11468 }
11469 Ok(())
11470}
11471
11472fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11473 let root = rustango_root();
11474 let attrs = parse_field_attrs(field)?;
11475 let ident = field
11476 .ident
11477 .as_ref()
11478 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11479 let name = ident_name(ident);
11480 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11481 let primary_key = attrs.primary_key;
11482 let DetectedType {
11483 kind,
11484 nullable,
11485 auto: detected_auto,
11486 fk_inner,
11487 } = detect_type(&field.ty)?;
11488 check_bound_compatibility(field, &attrs, kind)?;
11489 let auto = detected_auto;
11490 // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11491 // INSERT path: the user must wrap the field in `Auto<T>`, which
11492 // marks the column as DB-default-supplied. The mixin attrs then
11493 // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11494 // for `auto_now`, force the value on UPDATE too.
11495 if attrs.auto_uuid {
11496 if kind != DetectedKind::Uuid {
11497 return Err(syn::Error::new_spanned(
11498 field,
11499 "`#[rustango(auto_uuid)]` requires the field type to be \
11500 `Auto<uuid::Uuid>`",
11501 ));
11502 }
11503 if !detected_auto {
11504 return Err(syn::Error::new_spanned(
11505 field,
11506 "`#[rustango(auto_uuid)]` requires the field type to be \
11507 wrapped in `Auto<...>` so the macro skips the column on \
11508 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11509 ));
11510 }
11511 }
11512 if attrs.default_uuid_v7 {
11513 if kind != DetectedKind::Uuid {
11514 return Err(syn::Error::new_spanned(
11515 field,
11516 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11517 `Auto<uuid::Uuid>`",
11518 ));
11519 }
11520 if !detected_auto {
11521 return Err(syn::Error::new_spanned(
11522 field,
11523 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11524 wrapped in `Auto<...>` so the macro can detect the \
11525 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11526 ));
11527 }
11528 if attrs.auto_uuid {
11529 return Err(syn::Error::new_spanned(
11530 field,
11531 "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11532 `#[rustango(auto_uuid)]` — the former generates the UUID \
11533 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11534 Pick one.",
11535 ));
11536 }
11537 }
11538 if attrs.auto_now_add || attrs.auto_now {
11539 if kind != DetectedKind::DateTime {
11540 return Err(syn::Error::new_spanned(
11541 field,
11542 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11543 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11544 ));
11545 }
11546 if !detected_auto {
11547 return Err(syn::Error::new_spanned(
11548 field,
11549 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11550 the field type to be wrapped in `Auto<...>` so the macro skips \
11551 the column on INSERT and the DB DEFAULT (`now()`) fires",
11552 ));
11553 }
11554 }
11555 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11556 return Err(syn::Error::new_spanned(
11557 field,
11558 "`#[rustango(soft_delete)]` requires the field type to be \
11559 `Option<chrono::DateTime<chrono::Utc>>`",
11560 ));
11561 }
11562 let is_mixin_auto =
11563 attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11564 if detected_auto && !primary_key && !is_mixin_auto {
11565 return Err(syn::Error::new_spanned(
11566 field,
11567 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11568 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11569 `auto_now`",
11570 ));
11571 }
11572 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11573 return Err(syn::Error::new_spanned(
11574 field,
11575 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11576 SERIAL / BIGSERIAL already supplies a default sequence.",
11577 ));
11578 }
11579 if fk_inner.is_some() && primary_key {
11580 return Err(syn::Error::new_spanned(
11581 field,
11582 "`ForeignKey<T>` is not allowed on a primary-key field — \
11583 a row's PK is its own identity, not a reference to a parent.",
11584 ));
11585 }
11586 if attrs.generated_as.is_some() {
11587 if primary_key {
11588 return Err(syn::Error::new_spanned(
11589 field,
11590 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11591 primary-key field — a PK must be writable so the row \
11592 has an identity at INSERT time.",
11593 ));
11594 }
11595 if attrs.default.is_some() {
11596 return Err(syn::Error::new_spanned(
11597 field,
11598 "`#[rustango(generated_as = \"…\")]` cannot combine with \
11599 `default = \"…\"` — Postgres rejects DEFAULT on \
11600 generated columns. The expression IS the default.",
11601 ));
11602 }
11603 if detected_auto {
11604 return Err(syn::Error::new_spanned(
11605 field,
11606 "`#[rustango(generated_as = \"…\")]` is not allowed on \
11607 an `Auto<T>` field — generated columns are computed \
11608 by the DB, not server-assigned via a sequence. Use a \
11609 plain Rust type (e.g. `f64`).",
11610 ));
11611 }
11612 if fk_inner.is_some() {
11613 return Err(syn::Error::new_spanned(
11614 field,
11615 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11616 ForeignKey field.",
11617 ));
11618 }
11619 }
11620 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11621 let column_lit = column.as_str();
11622 // pgvector (#824): the `vector(dims = N)` attribute supplies the
11623 // dimension that the bare `Vector` Rust type can't carry, so emit
11624 // `FieldType::Vector(N)` here rather than the `variant_tokens`
11625 // fallback of `Vector(0)`.
11626 let field_type_tokens = if kind == DetectedKind::Vector {
11627 let root = rustango_root();
11628 let dims = attrs.vector_dims.unwrap_or(0);
11629 quote!(#root::core::FieldType::Vector(#dims))
11630 } else if kind == DetectedKind::Geometry {
11631 // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11632 // SRID that the bare `Point` Rust type can't carry, so emit
11633 // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11634 let root = rustango_root();
11635 let srid = attrs.geometry_srid.unwrap_or(0);
11636 quote!(#root::core::FieldType::Geometry(#srid))
11637 } else {
11638 kind.variant_tokens()
11639 };
11640 let max_length = optional_u32(attrs.max_length);
11641 let min = optional_i64(attrs.min);
11642 let max = optional_i64(attrs.max);
11643 let default = optional_str(attrs.default.as_deref());
11644
11645 let unique = attrs.unique;
11646 let generated_as = optional_str(attrs.generated_as.as_deref());
11647 let help_text = optional_str(attrs.help_text.as_deref());
11648 let choices = optional_choices(attrs.choices.as_deref());
11649 let db_comment = optional_str(attrs.db_comment.as_deref());
11650 let verbose_name = optional_str(attrs.verbose_name.as_deref());
11651 let editable = attrs.editable;
11652 let blank = attrs.blank;
11653 let case_insensitive = attrs.case_insensitive;
11654 let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11655 if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() && fk_inner.is_none()
11656 {
11657 return Err(syn::Error::new_spanned(
11658 field,
11659 "`#[rustango(on_delete = \"…\")]` requires a `ForeignKey<T>` field \
11660 or `fk = \"<table>\"` / `o2o = \"<table>\"` on the same field — it \
11661 has no meaning on a non-FK column.",
11662 ));
11663 }
11664 if attrs.on_delete.as_deref() == Some("set_null") && !nullable {
11665 return Err(syn::Error::new_spanned(
11666 field,
11667 "`on_delete = \"set_null\"` needs a nullable column: use \
11668 `Option<ForeignKey<T>>` or `Option<…>` (spelled out; a type \
11669 alias for `Option` is not seen).",
11670 ));
11671 }
11672 let fk_on_delete = match attrs.on_delete.as_deref() {
11673 None => quote!(::core::option::Option::None),
11674 Some(action) => {
11675 let variant = match action {
11676 "cascade" => quote!(Cascade),
11677 "restrict" => quote!(Restrict),
11678 "set_null" => quote!(SetNull),
11679 "set_default" => quote!(SetDefault),
11680 "no_action" => quote!(NoAction),
11681 // parse_field_attrs already validated this — guard against future drift.
11682 other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11683 };
11684 quote!(::core::option::Option::Some(
11685 #root::core::OnDeleteAction::#variant
11686 ))
11687 }
11688 };
11689 let auto_now_flag = attrs.auto_now;
11690 let schema = quote! {{
11691 #root::core::FieldSchema::from_parts(#root::core::FieldSchemaParts {
11692 name: #name,
11693 column: #column_lit,
11694 ty: #field_type_tokens,
11695 nullable: #nullable,
11696 primary_key: #primary_key,
11697 relation: #relation,
11698 max_length: #max_length,
11699 min: #min,
11700 max: #max,
11701 default: #default,
11702 auto: #auto,
11703 auto_now: #auto_now_flag,
11704 unique: #unique,
11705 generated_as: #generated_as,
11706 help_text: #help_text,
11707 choices: #choices,
11708 db_comment: #db_comment,
11709 verbose_name: #verbose_name,
11710 editable: #editable,
11711 blank: #blank,
11712 case_insensitive: #case_insensitive,
11713 fk_on_delete: #fk_on_delete,
11714 validators: &[ #(#validators_lits),* ],
11715 })
11716 }};
11717
11718 let from_row_init = quote! {
11719 #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11720 };
11721 let from_aliased_row_init = quote! {
11722 #ident: #root::sql::sqlx::Row::try_get(
11723 row,
11724 #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11725 )?
11726 };
11727 // `Auto<Uuid>` and `ForeignKey<_, Uuid>` decode through their own
11728 // MySQL impls; a bare or optional `Uuid` needs `FlatScalar`'s cell.
11729 let uuid_column = kind == DetectedKind::Uuid && !detected_auto && fk_inner.is_none();
11730 let (from_row_init_my, from_aliased_row_init_my) = if uuid_column {
11731 (
11732 quote! { #ident: #root::sql::try_get_flat_my(row, #column_lit)? },
11733 quote! {
11734 #ident: #root::sql::try_get_flat_my(
11735 row,
11736 #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11737 )?
11738 },
11739 )
11740 } else {
11741 (from_row_init.clone(), from_aliased_row_init.clone())
11742 };
11743
11744 Ok(FieldInfo {
11745 ident,
11746 column,
11747 primary_key,
11748 auto,
11749 value_ty: &field.ty,
11750 field_type_tokens,
11751 schema,
11752 from_row_init,
11753 from_aliased_row_init,
11754 from_row_init_my,
11755 from_aliased_row_init_my,
11756 uuid_column,
11757 fk_inner: fk_inner.cloned(),
11758 fk_pk_kind: kind,
11759 nullable,
11760 auto_now: attrs.auto_now,
11761 auto_now_add: attrs.auto_now_add,
11762 soft_delete: attrs.soft_delete,
11763 generated_as: attrs.generated_as.clone(),
11764 default_uuid_v7: attrs.default_uuid_v7,
11765 related_name: attrs.related_name.clone(),
11766 })
11767}
11768
11769fn check_bound_compatibility(
11770 field: &syn::Field,
11771 attrs: &FieldAttrs,
11772 kind: DetectedKind,
11773) -> syn::Result<()> {
11774 if attrs.max_length.is_some() && kind != DetectedKind::String {
11775 return Err(syn::Error::new_spanned(
11776 field,
11777 "`max_length` is only valid on `String` fields (or `Option<String>`)",
11778 ));
11779 }
11780 // These were silently dropped on the wrong type (#1937).
11781 if attrs.case_insensitive && kind != DetectedKind::String {
11782 return Err(syn::Error::new_spanned(
11783 field,
11784 "`citext` is only valid on `String` fields (or `Option<String>`)",
11785 ));
11786 }
11787 if attrs.vector_dims.is_some() && kind != DetectedKind::Vector {
11788 return Err(syn::Error::new_spanned(
11789 field,
11790 "`vector(dims = …)` is only valid on a pgvector `Vector` field",
11791 ));
11792 }
11793 if attrs.geometry_srid.is_some() && kind != DetectedKind::Geometry {
11794 return Err(syn::Error::new_spanned(
11795 field,
11796 "`geometry(srid = …)` is only valid on a PostGIS `Point` field",
11797 ));
11798 }
11799 if attrs.choices.is_some() && kind != DetectedKind::String {
11800 return Err(syn::Error::new_spanned(
11801 field,
11802 "`choices` is only valid on `String` fields (or `Option<String>`) — \
11803 integer-valued enumerations should be modeled with a Rust enum and \
11804 custom (de)serializer for now",
11805 ));
11806 }
11807 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11808 return Err(syn::Error::new_spanned(
11809 field,
11810 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11811 ));
11812 }
11813 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11814 if min > max {
11815 return Err(syn::Error::new_spanned(
11816 field,
11817 format!("`min` ({min}) is greater than `max` ({max})"),
11818 ));
11819 }
11820 }
11821 Ok(())
11822}
11823
11824/// A `&'static [ty]` of `const fn` results. The slice needs its own
11825/// `const`: a `&[..]` assigned in a statement is not extended to `'static`.
11826fn const_slice(ty: &TokenStream2, items: impl Iterator<Item = TokenStream2>) -> TokenStream2 {
11827 quote!({
11828 const ITEMS: &[#ty] = &[ #(#items),* ];
11829 ITEMS
11830 })
11831}
11832
11833fn optional_u32(value: Option<u32>) -> TokenStream2 {
11834 if let Some(v) = value {
11835 quote!(::core::option::Option::Some(#v))
11836 } else {
11837 quote!(::core::option::Option::None)
11838 }
11839}
11840
11841fn optional_i64(value: Option<i64>) -> TokenStream2 {
11842 if let Some(v) = value {
11843 quote!(::core::option::Option::Some(#v))
11844 } else {
11845 quote!(::core::option::Option::None)
11846 }
11847}
11848
11849fn optional_str(value: Option<&str>) -> TokenStream2 {
11850 if let Some(v) = value {
11851 quote!(::core::option::Option::Some(#v))
11852 } else {
11853 quote!(::core::option::Option::None)
11854 }
11855}
11856
11857fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11858 let Some(pairs) = pairs else {
11859 return quote!(::core::option::Option::None);
11860 };
11861 let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11862 quote!(::core::option::Option::Some(&[#(#entries),*]))
11863}
11864
11865fn relation_tokens(
11866 field: &syn::Field,
11867 attrs: &FieldAttrs,
11868 fk_inner: Option<&syn::Type>,
11869 table: &str,
11870) -> syn::Result<TokenStream2> {
11871 let root = rustango_root();
11872 if let Some(inner) = fk_inner {
11873 if attrs.fk.is_some() || attrs.o2o.is_some() {
11874 return Err(syn::Error::new_spanned(
11875 field,
11876 "`ForeignKey<T>` already declares the FK target via the type parameter — \
11877 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11878 ));
11879 }
11880 // No `on`: the target's PK column, which may be renamed.
11881 let on = match attrs.on.as_deref() {
11882 Some(on) => quote!(#on),
11883 None => quote! {
11884 match <#inner as #root::core::Model>::SCHEMA.pk_column() {
11885 ::core::option::Option::Some(c) => c,
11886 ::core::option::Option::None => "id",
11887 }
11888 },
11889 };
11890 return Ok(quote! {
11891 ::core::option::Option::Some(#root::core::Relation::fk(
11892 <#inner as #root::core::Model>::SCHEMA.table,
11893 #on,
11894 ))
11895 });
11896 }
11897 match (&attrs.fk, &attrs.o2o) {
11898 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11899 field,
11900 "`fk` and `o2o` are mutually exclusive",
11901 )),
11902 (Some(to), None) => {
11903 let on = attrs.on.as_deref().unwrap_or("id");
11904 // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11905 // the model's own table. Threaded as a literal string at
11906 // macro-expansion time to sidestep the const-eval cycle
11907 // that `Self::SCHEMA.table` would create when referenced
11908 // inside Self::SCHEMA's own initializer.
11909 let resolved = if to == "self" { table } else { to };
11910 Ok(quote! {
11911 ::core::option::Option::Some(#root::core::Relation::fk(#resolved, #on))
11912 })
11913 }
11914 (None, Some(to)) => {
11915 let on = attrs.on.as_deref().unwrap_or("id");
11916 let resolved = if to == "self" { table } else { to };
11917 Ok(quote! {
11918 ::core::option::Option::Some(#root::core::Relation::o2o(#resolved, #on))
11919 })
11920 }
11921 (None, None) => {
11922 if attrs.on.is_some() {
11923 return Err(syn::Error::new_spanned(
11924 field,
11925 "`on` requires `fk` or `o2o`",
11926 ));
11927 }
11928 Ok(quote!(::core::option::Option::None))
11929 }
11930 }
11931}
11932
11933/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11934/// about kinds without depending on `rustango-core` (which would require a
11935/// proc-macro/normal split it doesn't have today).
11936#[derive(Clone, Copy, PartialEq, Eq)]
11937enum DetectedKind {
11938 I16,
11939 I32,
11940 I64,
11941 F32,
11942 F64,
11943 Bool,
11944 String,
11945 DateTime,
11946 Date,
11947 Time,
11948 Uuid,
11949 Json,
11950 Decimal,
11951 Binary,
11952 /// `Array<String>` → PG `text[]` (#341).
11953 ArrayText,
11954 /// `Array<i32>` → PG `integer[]` (#341).
11955 ArrayInt,
11956 /// `Array<i64>` → PG `bigint[]` (#341).
11957 ArrayBigInt,
11958 /// `Range<i32>` → PG `int4range` (#343).
11959 RangeInt,
11960 /// `Range<i64>` → PG `int8range` (#343).
11961 RangeBigInt,
11962 /// `Range<Decimal>` → PG `numrange` (#343).
11963 RangeNumeric,
11964 /// `Range<NaiveDate>` → PG `daterange` (#343).
11965 RangeDate,
11966 /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11967 RangeDateTime,
11968 /// `HStore` → PG `hstore` (#342).
11969 HStore,
11970 /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11971 /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11972 /// in at the `FieldType` emission site (not carried on this enum).
11973 Vector,
11974 /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11975 /// from the `#[rustango(geometry(srid = N))]` field attribute,
11976 /// threaded in at the `FieldType` emission site.
11977 Geometry,
11978}
11979
11980impl DetectedKind {
11981 fn variant_tokens(self) -> TokenStream2 {
11982 let root = rustango_root();
11983 match self {
11984 Self::I16 => quote!(#root::core::FieldType::I16),
11985 Self::I32 => quote!(#root::core::FieldType::I32),
11986 Self::I64 => quote!(#root::core::FieldType::I64),
11987 Self::F32 => quote!(#root::core::FieldType::F32),
11988 Self::F64 => quote!(#root::core::FieldType::F64),
11989 Self::Bool => quote!(#root::core::FieldType::Bool),
11990 Self::String => quote!(#root::core::FieldType::String),
11991 Self::DateTime => quote!(#root::core::FieldType::DateTime),
11992 Self::Date => quote!(#root::core::FieldType::Date),
11993 Self::Time => quote!(#root::core::FieldType::Time),
11994 Self::Uuid => quote!(#root::core::FieldType::Uuid),
11995 Self::Json => quote!(#root::core::FieldType::Json),
11996 Self::Decimal => quote!(#root::core::FieldType::Decimal),
11997 Self::Binary => quote!(#root::core::FieldType::Binary),
11998 Self::ArrayText => {
11999 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
12000 }
12001 Self::ArrayInt => {
12002 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
12003 }
12004 Self::ArrayBigInt => {
12005 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
12006 }
12007 Self::RangeInt => {
12008 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
12009 }
12010 Self::RangeBigInt => {
12011 quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
12012 }
12013 Self::RangeNumeric => {
12014 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
12015 }
12016 Self::RangeDate => {
12017 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
12018 }
12019 Self::RangeDateTime => {
12020 quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
12021 }
12022 Self::HStore => quote!(#root::core::FieldType::HStore),
12023 // Dimension comes from the `vector(dims = N)` attribute,
12024 // applied at the emission site; `0` here is just a fallback.
12025 Self::Vector => quote!(#root::core::FieldType::Vector(0)),
12026 // SRID comes from the `geometry(srid = N)` attribute, applied
12027 // at the emission site; `0` here is just a fallback.
12028 Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
12029 }
12030 }
12031
12032 fn is_integer(self) -> bool {
12033 matches!(self, Self::I16 | Self::I32 | Self::I64)
12034 }
12035}
12036
12037/// Result of walking a field's Rust type. `kind` is the underlying
12038/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
12039/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
12040/// `Some(<T>)` when the field was `ForeignKey<T>` (or
12041/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
12042#[derive(Clone, Copy)]
12043struct DetectedType<'a> {
12044 kind: DetectedKind,
12045 nullable: bool,
12046 auto: bool,
12047 fk_inner: Option<&'a syn::Type>,
12048}
12049
12050/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
12051/// non-`Auto` types — the caller should already have routed Auto-only
12052/// codegen through this helper, so a `None` indicates a macro-internal
12053/// invariant break.
12054fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
12055 let Type::Path(TypePath { path, qself: None }) = ty else {
12056 return None;
12057 };
12058 let last = path.segments.last()?;
12059 if last.ident != "Auto" {
12060 return None;
12061 }
12062 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
12063 return None;
12064 };
12065 args.args.iter().find_map(|a| match a {
12066 syn::GenericArgument::Type(t) => Some(t),
12067 _ => None,
12068 })
12069}
12070
12071fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
12072 let Type::Path(TypePath { path, qself: None }) = ty else {
12073 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
12074 };
12075 let last = path
12076 .segments
12077 .last()
12078 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
12079
12080 if last.ident == "Option" {
12081 let inner = generic_inner(ty, &last.arguments, "Option")?;
12082 let inner_det = detect_type(inner)?;
12083 if inner_det.nullable {
12084 return Err(syn::Error::new_spanned(
12085 ty,
12086 "nested Option is not supported",
12087 ));
12088 }
12089 if inner_det.auto {
12090 return Err(syn::Error::new_spanned(
12091 ty,
12092 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
12093 ));
12094 }
12095 return Ok(DetectedType {
12096 nullable: true,
12097 ..inner_det
12098 });
12099 }
12100
12101 if last.ident == "Auto" {
12102 let inner = generic_inner(ty, &last.arguments, "Auto")?;
12103 let inner_det = detect_type(inner)?;
12104 if inner_det.auto {
12105 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
12106 }
12107 if inner_det.nullable {
12108 return Err(syn::Error::new_spanned(
12109 ty,
12110 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
12111 ));
12112 }
12113 if inner_det.fk_inner.is_some() {
12114 return Err(syn::Error::new_spanned(
12115 ty,
12116 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
12117 ));
12118 }
12119 if !matches!(
12120 inner_det.kind,
12121 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
12122 ) {
12123 return Err(syn::Error::new_spanned(
12124 ty,
12125 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
12126 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
12127 (DEFAULT now())",
12128 ));
12129 }
12130 return Ok(DetectedType {
12131 auto: true,
12132 ..inner_det
12133 });
12134 }
12135
12136 if last.ident == "ForeignKey" {
12137 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
12138 // Resolve the FK column's underlying SQL type from `K`. When the
12139 // user wrote `ForeignKey<T>` without a key parameter, the type
12140 // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
12141 // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
12142 // recurse into K so the column DDL emits the right SQL type
12143 // (VARCHAR for String, UUID for Uuid, …) and the load_related
12144 // emitter knows which `SqlValue` variant to match.
12145 let kind = match key_ty {
12146 Some(k) => detect_type(k)?.kind,
12147 None => DetectedKind::I64,
12148 };
12149 return Ok(DetectedType {
12150 kind,
12151 nullable: false,
12152 auto: false,
12153 fk_inner: Some(inner),
12154 });
12155 }
12156
12157 let kind = match last.ident.to_string().as_str() {
12158 "i16" => DetectedKind::I16,
12159 "i32" => DetectedKind::I32,
12160 "i64" => DetectedKind::I64,
12161 "f32" => DetectedKind::F32,
12162 "f64" => DetectedKind::F64,
12163 "bool" => DetectedKind::Bool,
12164 "String" => DetectedKind::String,
12165 "DateTime" => DetectedKind::DateTime,
12166 "NaiveDate" => DetectedKind::Date,
12167 "NaiveTime" => DetectedKind::Time,
12168 "Uuid" => DetectedKind::Uuid,
12169 "Value" => DetectedKind::Json,
12170 "Decimal" => DetectedKind::Decimal,
12171 // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
12172 // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
12173 // — that would emit Binary DDL and decode-fail at runtime.
12174 "Vec" => {
12175 let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
12176 if let Type::Path(TypePath { path, qself: None }) = inner {
12177 if let Some(seg) = path.segments.last() {
12178 if seg.ident == "u8" && seg.arguments.is_empty() {
12179 return Ok(DetectedType {
12180 kind: DetectedKind::Binary,
12181 nullable: false,
12182 auto: false,
12183 fk_inner: None,
12184 });
12185 }
12186 }
12187 }
12188 return Err(syn::Error::new_spanned(
12189 ty,
12190 "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
12191 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
12192 ));
12193 }
12194 // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
12195 // `integer[]` / `bigint[]` (#341).
12196 "Array" => {
12197 let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
12198 let elem = match inner {
12199 Type::Path(TypePath { path, qself: None }) => {
12200 path.segments.last().map(|s| s.ident.to_string())
12201 }
12202 _ => None,
12203 };
12204 let kind = match elem.as_deref() {
12205 Some("String") => DetectedKind::ArrayText,
12206 Some("i32") => DetectedKind::ArrayInt,
12207 Some("i64") => DetectedKind::ArrayBigInt,
12208 _ => {
12209 return Err(syn::Error::new_spanned(
12210 ty,
12211 "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
12212 `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
12213 ));
12214 }
12215 };
12216 return Ok(DetectedType {
12217 kind,
12218 nullable: false,
12219 auto: false,
12220 fk_inner: None,
12221 });
12222 }
12223 // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
12224 // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
12225 // `int8range` / `numrange` / `daterange` / `tstzrange` (#343).
12226 "Range" => {
12227 let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
12228 let elem = match inner {
12229 Type::Path(TypePath { path, qself: None }) => {
12230 path.segments.last().map(|s| s.ident.to_string())
12231 }
12232 _ => None,
12233 };
12234 let kind = match elem.as_deref() {
12235 Some("i32") => DetectedKind::RangeInt,
12236 Some("i64") => DetectedKind::RangeBigInt,
12237 Some("Decimal") => DetectedKind::RangeNumeric,
12238 Some("NaiveDate") => DetectedKind::RangeDate,
12239 Some("DateTime") => DetectedKind::RangeDateTime,
12240 _ => {
12241 return Err(syn::Error::new_spanned(
12242 ty,
12243 "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
12244 `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
12245 `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
12246 (→ tstzrange) are supported (#343)",
12247 ));
12248 }
12249 };
12250 return Ok(DetectedType {
12251 kind,
12252 nullable: false,
12253 auto: false,
12254 fk_inner: None,
12255 });
12256 }
12257 // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
12258 // (the `CastValue` impl bridges logical↔stored); the field's own
12259 // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
12260 // schema just needs `FieldType::String`.
12261 "Cast" => {
12262 return Ok(DetectedType {
12263 kind: DetectedKind::String,
12264 nullable: false,
12265 auto: false,
12266 fk_inner: None,
12267 });
12268 }
12269 // `HStore` → PG `hstore` (#342). No generic
12270 // parameter — always a string→string map.
12271 "HStore" => {
12272 return Ok(DetectedType {
12273 kind: DetectedKind::HStore,
12274 nullable: false,
12275 auto: false,
12276 fk_inner: None,
12277 });
12278 }
12279 // `Vector` → pgvector `vector(N)` (#824). The dimension is
12280 // supplied by `#[rustango(vector(dims = N))]`, not the type.
12281 "Vector" => {
12282 return Ok(DetectedType {
12283 kind: DetectedKind::Vector,
12284 nullable: false,
12285 auto: false,
12286 fk_inner: None,
12287 });
12288 }
12289 // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
12290 // supplied by `#[rustango(geometry(srid = N))]`, not the type.
12291 "Point" => {
12292 return Ok(DetectedType {
12293 kind: DetectedKind::Geometry,
12294 nullable: false,
12295 auto: false,
12296 fk_inner: None,
12297 });
12298 }
12299 other => {
12300 return Err(syn::Error::new_spanned(
12301 ty,
12302 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)"),
12303 ));
12304 }
12305 };
12306 Ok(DetectedType {
12307 kind,
12308 nullable: false,
12309 auto: false,
12310 fk_inner: None,
12311 })
12312}
12313
12314fn generic_inner<'a>(
12315 ty: &'a Type,
12316 arguments: &'a PathArguments,
12317 wrapper: &str,
12318) -> syn::Result<&'a Type> {
12319 let PathArguments::AngleBracketed(args) = arguments else {
12320 return Err(syn::Error::new_spanned(
12321 ty,
12322 format!("{wrapper} requires a generic argument"),
12323 ));
12324 };
12325 args.args
12326 .iter()
12327 .find_map(|a| match a {
12328 GenericArgument::Type(t) => Some(t),
12329 _ => None,
12330 })
12331 .ok_or_else(|| {
12332 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12333 })
12334}
12335
12336/// Like [`generic_inner`] but pulls *two* type args — the first is
12337/// required, the second is optional. Used by the `ForeignKey<T, K>`
12338/// detection where K defaults to `i64` when omitted.
12339fn generic_pair<'a>(
12340 ty: &'a Type,
12341 arguments: &'a PathArguments,
12342 wrapper: &str,
12343) -> syn::Result<(&'a Type, Option<&'a Type>)> {
12344 let PathArguments::AngleBracketed(args) = arguments else {
12345 return Err(syn::Error::new_spanned(
12346 ty,
12347 format!("{wrapper} requires a generic argument"),
12348 ));
12349 };
12350 let mut types = args.args.iter().filter_map(|a| match a {
12351 GenericArgument::Type(t) => Some(t),
12352 _ => None,
12353 });
12354 let first = types.next().ok_or_else(|| {
12355 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12356 })?;
12357 let second = types.next();
12358 Ok((first, second))
12359}
12360
12361fn to_snake_case(s: &str) -> String {
12362 let mut out = String::with_capacity(s.len() + 4);
12363 for (i, ch) in s.chars().enumerate() {
12364 if ch.is_ascii_uppercase() {
12365 if i > 0 {
12366 out.push('_');
12367 }
12368 out.push(ch.to_ascii_lowercase());
12369 } else {
12370 out.push(ch);
12371 }
12372 }
12373 out
12374}
12375
12376// ============================================================
12377// #[derive(Form)] — slice 8.4B
12378// ============================================================
12379
12380/// Per-field `#[form(...)]` attributes recognised by the derive.
12381#[derive(Default)]
12382struct FormFieldAttrs {
12383 min: Option<i64>,
12384 max: Option<i64>,
12385 min_length: Option<u32>,
12386 max_length: Option<u32>,
12387 /// `#[form(clean = "fn_name")]` — per-field clean hook.
12388 /// The named static method on the form struct is called after the
12389 /// field's typed parse + length/range checks; it gets the parsed
12390 /// value by reference and returns `Result<<FieldType>, String>`.
12391 /// On Ok, the returned value replaces the parsed one; on Err, the
12392 /// message is attached to the field error list. Issue #372.
12393 clean: Option<syn::Ident>,
12394}
12395
12396/// Container-level `#[form(...)]` attributes. Currently only the
12397/// cross-field `validate` hook (issue #373).
12398#[derive(Default)]
12399struct FormContainerAttrs {
12400 /// `#[form(validate = "fn_name")]` — whole-form clean hook.
12401 /// After every per-field parse succeeds, the named method on the
12402 /// form struct is called with `&self` and may return
12403 /// `Result<(), FormErrors>`. Errors merge into the field error
12404 /// list. Issue #373.
12405 validate: Option<syn::Ident>,
12406}
12407
12408/// Detected shape of a form field's Rust type.
12409#[derive(Clone, Copy)]
12410enum FormFieldKind {
12411 String,
12412 I16,
12413 I32,
12414 I64,
12415 F32,
12416 F64,
12417 Bool,
12418}
12419
12420impl FormFieldKind {
12421 fn parse_method(self) -> &'static str {
12422 match self {
12423 Self::I16 => "i16",
12424 Self::I32 => "i32",
12425 Self::I64 => "i64",
12426 Self::F32 => "f32",
12427 Self::F64 => "f64",
12428 // String + Bool don't go through `str::parse`; the codegen
12429 // handles them inline.
12430 Self::String | Self::Bool => "",
12431 }
12432 }
12433}
12434
12435/// A generated `derive(Form)` local; mixed-site so a same-named field can't shadow it.
12436fn form_local(name: &str) -> syn::Ident {
12437 syn::Ident::new(name, proc_macro2::Span::mixed_site())
12438}
12439
12440fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
12441 let root = rustango_root();
12442 let (errs, cleaned, data_param) = (
12443 form_local("__errors"),
12444 form_local("__cleaned"),
12445 form_local("data"),
12446 );
12447 let struct_name = &input.ident;
12448
12449 let Data::Struct(data) = &input.data else {
12450 return Err(syn::Error::new_spanned(
12451 struct_name,
12452 "Form can only be derived on structs",
12453 ));
12454 };
12455 let Fields::Named(named) = &data.fields else {
12456 return Err(syn::Error::new_spanned(
12457 struct_name,
12458 "Form requires a struct with named fields",
12459 ));
12460 };
12461
12462 // #373 — container-level `#[form(validate = "fn")]` hook.
12463 let container = parse_form_container_attrs(input)?;
12464 let post_field_clean: Vec<TokenStream2> = Vec::new();
12465 let _ = post_field_clean;
12466
12467 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12468 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12469
12470 for field in &named.named {
12471 let ident = field
12472 .ident
12473 .as_ref()
12474 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12475 let attrs = parse_form_field_attrs(field)?;
12476 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12477 let is_string = matches!(kind, FormFieldKind::String);
12478 let is_number = !is_string && !matches!(kind, FormFieldKind::Bool);
12479 if !is_string && (attrs.min_length.is_some() || attrs.max_length.is_some()) {
12480 return Err(syn::Error::new(
12481 field.span(),
12482 "`min_length` / `max_length` need a String field",
12483 ));
12484 }
12485 if !is_number && (attrs.min.is_some() || attrs.max.is_some()) {
12486 return Err(syn::Error::new(
12487 field.span(),
12488 "`min` / `max` need a numeric field",
12489 ));
12490 }
12491
12492 let name_lit = ident_name(ident);
12493 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12494 // #372 — append the per-field `clean_<field>` call right after
12495 // the parse block when the attribute is set. The clean fn
12496 // takes &T and returns Result<T, String>; on Err we attach
12497 // the message to the field error list without aborting, so
12498 // the caller sees every field error at once.
12499 let clean_block = if let Some(clean_fn) = &attrs.clean {
12500 quote! {
12501 if #errs.fields().get(#name_lit).is_none() {
12502 match Self::#clean_fn(&#ident) {
12503 ::core::result::Result::Ok(#cleaned) => { #ident = #cleaned; }
12504 ::core::result::Result::Err(__msg) => {
12505 #errs.add(#name_lit, __msg);
12506 }
12507 }
12508 }
12509 }
12510 } else {
12511 quote! {}
12512 };
12513 field_blocks.push(quote! {
12514 #parse_block
12515 #clean_block
12516 });
12517 field_idents.push(ident);
12518 }
12519
12520 // #373 — after every per-field parse + clean succeeds, call the
12521 // cross-field validator if declared. Errors merge into the
12522 // outgoing FormErrors via the existing `FormErrors::merge` helper
12523 // (same primitive the serializer cross-field hook uses).
12524 let cross_field_call = if let Some(validate_fn) = &container.validate {
12525 quote! {
12526 if #errs.is_empty() {
12527 let __candidate = Self { #( #field_idents ),* };
12528 if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12529 #errs.merge(__other);
12530 }
12531 if !#errs.is_empty() {
12532 return ::core::result::Result::Err(#errs);
12533 }
12534 return ::core::result::Result::Ok(__candidate);
12535 }
12536 }
12537 } else {
12538 quote! {}
12539 };
12540
12541 Ok(quote! {
12542 impl #root::forms::Form for #struct_name {
12543 fn parse(
12544 #data_param: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12545 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12546 let mut #errs = #root::forms::FormErrors::default();
12547 #( #field_blocks )*
12548 #cross_field_call
12549 if !#errs.is_empty() {
12550 return ::core::result::Result::Err(#errs);
12551 }
12552 ::core::result::Result::Ok(Self {
12553 #( #field_idents ),*
12554 })
12555 }
12556 }
12557 })
12558}
12559
12560fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12561 let mut out = FormContainerAttrs::default();
12562 for attr in &input.attrs {
12563 if !attr.path().is_ident("form") {
12564 continue;
12565 }
12566 attr.parse_nested_meta(|meta| {
12567 if meta.path.is_ident("validate") {
12568 let s: LitStr = meta.value()?.parse()?;
12569 out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12570 return Ok(());
12571 }
12572 Err(meta.error("unknown form container attribute (supported: `validate`)"))
12573 })?;
12574 }
12575 Ok(out)
12576}
12577
12578fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12579 let mut out = FormFieldAttrs::default();
12580 for attr in &field.attrs {
12581 if !attr.path().is_ident("form") {
12582 continue;
12583 }
12584 attr.parse_nested_meta(|meta| {
12585 if meta.path.is_ident("min") {
12586 let lit: syn::LitInt = meta.value()?.parse()?;
12587 out.min = Some(lit.base10_parse::<i64>()?);
12588 return Ok(());
12589 }
12590 if meta.path.is_ident("max") {
12591 let lit: syn::LitInt = meta.value()?.parse()?;
12592 out.max = Some(lit.base10_parse::<i64>()?);
12593 return Ok(());
12594 }
12595 if meta.path.is_ident("min_length") {
12596 let lit: syn::LitInt = meta.value()?.parse()?;
12597 out.min_length = Some(lit.base10_parse::<u32>()?);
12598 return Ok(());
12599 }
12600 if meta.path.is_ident("max_length") {
12601 let lit: syn::LitInt = meta.value()?.parse()?;
12602 out.max_length = Some(lit.base10_parse::<u32>()?);
12603 return Ok(());
12604 }
12605 if meta.path.is_ident("clean") {
12606 let s: LitStr = meta.value()?.parse()?;
12607 out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12608 return Ok(());
12609 }
12610 Err(meta.error(
12611 "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12612 ))
12613 })?;
12614 }
12615 Ok(out)
12616}
12617
12618fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12619 let Type::Path(TypePath { path, qself: None }) = ty else {
12620 return Err(syn::Error::new(
12621 span,
12622 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12623 ));
12624 };
12625 let last = path
12626 .segments
12627 .last()
12628 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12629
12630 if last.ident == "Option" {
12631 let inner = generic_inner(ty, &last.arguments, "Option")?;
12632 let (kind, nested) = detect_form_field(inner, span)?;
12633 if nested {
12634 return Err(syn::Error::new(
12635 span,
12636 "nested Option in Form fields is not supported",
12637 ));
12638 }
12639 return Ok((kind, true));
12640 }
12641
12642 let kind = match last.ident.to_string().as_str() {
12643 "String" => FormFieldKind::String,
12644 "i16" => FormFieldKind::I16,
12645 "i32" => FormFieldKind::I32,
12646 "i64" => FormFieldKind::I64,
12647 "f32" => FormFieldKind::F32,
12648 "f64" => FormFieldKind::F64,
12649 "bool" => FormFieldKind::Bool,
12650 other => {
12651 return Err(syn::Error::new(
12652 span,
12653 format!(
12654 "Form field type `{other}` is not supported in v0.8 — use String / \
12655 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12656 ),
12657 ));
12658 }
12659 };
12660 Ok((kind, false))
12661}
12662
12663#[allow(clippy::too_many_lines)]
12664fn render_form_field_parse(
12665 ident: &syn::Ident,
12666 name_lit: &str,
12667 kind: FormFieldKind,
12668 nullable: bool,
12669 attrs: &FormFieldAttrs,
12670) -> TokenStream2 {
12671 let (errs, data_param) = (form_local("__errors"), form_local("data"));
12672 // Pull the raw &str from the payload `Form::parse(data: &HashMap<…>)`.
12673 let lookup = quote! {
12674 let __raw: ::core::option::Option<&::std::string::String> = #data_param.get(#name_lit);
12675 };
12676
12677 let parsed_value = match kind {
12678 // `Option<bool>`: absent or blank is `None`, as for the other types.
12679 FormFieldKind::Bool if nullable => quote! {
12680 let __v: ::core::option::Option<bool> = match __raw {
12681 ::core::option::Option::None => ::core::option::Option::None,
12682 ::core::option::Option::Some(__s) if __s.is_empty() => {
12683 ::core::option::Option::None
12684 }
12685 ::core::option::Option::Some(__s) => ::core::option::Option::Some(!matches!(
12686 __s.to_ascii_lowercase().as_str(),
12687 "false" | "0" | "off" | "no"
12688 )),
12689 };
12690 },
12691 FormFieldKind::Bool => quote! {
12692 let __v: bool = match __raw {
12693 ::core::option::Option::None => false,
12694 ::core::option::Option::Some(__s) => !matches!(
12695 __s.to_ascii_lowercase().as_str(),
12696 "" | "false" | "0" | "off" | "no"
12697 ),
12698 };
12699 },
12700 FormFieldKind::String => {
12701 if nullable {
12702 quote! {
12703 let __v: ::core::option::Option<::std::string::String> = match __raw {
12704 ::core::option::Option::None => ::core::option::Option::None,
12705 ::core::option::Option::Some(__s) if __s.is_empty() => {
12706 ::core::option::Option::None
12707 }
12708 ::core::option::Option::Some(__s) => {
12709 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12710 }
12711 };
12712 }
12713 } else {
12714 quote! {
12715 let __v: ::std::string::String = match __raw {
12716 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12717 ::core::clone::Clone::clone(__s)
12718 }
12719 _ => {
12720 #errs.add(#name_lit, "This field is required.");
12721 ::std::string::String::new()
12722 }
12723 };
12724 }
12725 }
12726 }
12727 FormFieldKind::I16
12728 | FormFieldKind::I32
12729 | FormFieldKind::I64
12730 | FormFieldKind::F32
12731 | FormFieldKind::F64 => {
12732 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12733 let ty_lit = kind.parse_method();
12734 let default_val = match kind {
12735 FormFieldKind::I16 => quote! { 0i16 },
12736 FormFieldKind::I32 => quote! { 0i32 },
12737 FormFieldKind::I64 => quote! { 0i64 },
12738 FormFieldKind::F32 => quote! { 0f32 },
12739 FormFieldKind::F64 => quote! { 0f64 },
12740 _ => quote! { Default::default() },
12741 };
12742 // NaN and inf parse, then pass every min/max compare.
12743 let finite = if matches!(kind, FormFieldKind::F32 | FormFieldKind::F64) {
12744 quote! {
12745 .and_then(|__n| if __n.is_finite() {
12746 ::core::result::Result::Ok(__n)
12747 } else {
12748 ::core::result::Result::Err(::std::string::String::from("not a finite number"))
12749 })
12750 }
12751 } else {
12752 quote! {}
12753 };
12754 if nullable {
12755 quote! {
12756 let __v: ::core::option::Option<#parse_ty> = match __raw {
12757 ::core::option::Option::None => ::core::option::Option::None,
12758 ::core::option::Option::Some(__s) if __s.is_empty() => {
12759 ::core::option::Option::None
12760 }
12761 ::core::option::Option::Some(__s) => {
12762 match __s.parse::<#parse_ty>().map_err(|__e| ::std::string::ToString::to_string(&__e)) #finite {
12763 ::core::result::Result::Ok(__n) => {
12764 ::core::option::Option::Some(__n)
12765 }
12766 ::core::result::Result::Err(__e) => {
12767 #errs.add(
12768 #name_lit,
12769 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12770 );
12771 ::core::option::Option::None
12772 }
12773 }
12774 }
12775 };
12776 }
12777 } else {
12778 quote! {
12779 let __v: #parse_ty = match __raw {
12780 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12781 match __s.parse::<#parse_ty>().map_err(|__e| ::std::string::ToString::to_string(&__e)) #finite {
12782 ::core::result::Result::Ok(__n) => __n,
12783 ::core::result::Result::Err(__e) => {
12784 #errs.add(
12785 #name_lit,
12786 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12787 );
12788 #default_val
12789 }
12790 }
12791 }
12792 _ => {
12793 #errs.add(#name_lit, "This field is required.");
12794 #default_val
12795 }
12796 };
12797 }
12798 }
12799 }
12800 };
12801
12802 let validators = render_form_validators(name_lit, kind, nullable, attrs);
12803
12804 quote! {
12805 // `mut` so the per-field `clean` hook (#372) can rewrite the
12806 // parsed value in-place when it returns Ok with a normalized
12807 // form (e.g. trim / lowercase).
12808 let mut #ident = {
12809 #lookup
12810 #parsed_value
12811 #validators
12812 __v
12813 };
12814 }
12815}
12816
12817fn render_form_validators(
12818 name_lit: &str,
12819 kind: FormFieldKind,
12820 nullable: bool,
12821 attrs: &FormFieldAttrs,
12822) -> TokenStream2 {
12823 let errs = form_local("__errors");
12824 let mut checks: Vec<TokenStream2> = Vec::new();
12825
12826 let val_ref = if nullable {
12827 quote! { __v.as_ref() }
12828 } else {
12829 quote! { ::core::option::Option::Some(&__v) }
12830 };
12831
12832 let is_string = matches!(kind, FormFieldKind::String);
12833 let is_numeric = matches!(
12834 kind,
12835 FormFieldKind::I16
12836 | FormFieldKind::I32
12837 | FormFieldKind::I64
12838 | FormFieldKind::F32
12839 | FormFieldKind::F64
12840 );
12841
12842 if is_string {
12843 if let Some(min_len) = attrs.min_length {
12844 let min_len_usize = min_len as usize;
12845 checks.push(quote! {
12846 if let ::core::option::Option::Some(__s) = #val_ref {
12847 if __s.chars().count() < #min_len_usize {
12848 #errs.add(
12849 #name_lit,
12850 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12851 );
12852 }
12853 }
12854 });
12855 }
12856 if let Some(max_len) = attrs.max_length {
12857 let max_len_usize = max_len as usize;
12858 checks.push(quote! {
12859 if let ::core::option::Option::Some(__s) = #val_ref {
12860 if __s.chars().count() > #max_len_usize {
12861 #errs.add(
12862 #name_lit,
12863 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12864 );
12865 }
12866 }
12867 });
12868 }
12869 }
12870
12871 if is_numeric {
12872 if let Some(min) = attrs.min {
12873 checks.push(quote! {
12874 if let ::core::option::Option::Some(__n) = #val_ref {
12875 if (*__n as f64) < (#min as f64) {
12876 #errs.add(
12877 #name_lit,
12878 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12879 );
12880 }
12881 }
12882 });
12883 }
12884 if let Some(max) = attrs.max {
12885 checks.push(quote! {
12886 if let ::core::option::Option::Some(__n) = #val_ref {
12887 if (*__n as f64) > (#max as f64) {
12888 #errs.add(
12889 #name_lit,
12890 ::std::format!("Ensure this value is less than or equal to {}.", #max),
12891 );
12892 }
12893 }
12894 });
12895 }
12896 }
12897
12898 quote! { #( #checks )* }
12899}
12900
12901// ============================================================
12902// #[derive(ViewSet)]
12903// ============================================================
12904
12905struct ViewSetAttrs {
12906 model: syn::Path,
12907 fields: Vec<String>,
12908 filter_fields: Vec<String>,
12909 search_fields: Vec<String>,
12910 /// (field_name, desc)
12911 ordering: Vec<(String, bool)>,
12912 page_size: Option<usize>,
12913 read_only: bool,
12914 allow_anonymous: bool,
12915 perms: ViewSetPermsAttrs,
12916 /// `#[viewset(serializer = SomeSerializer)]` — render list /
12917 /// retrieve / create responses through this `#[derive(Serializer)]`
12918 /// type instead of the default field-level projection (requires the
12919 /// `serializer` feature). Tri-dialect.
12920 serializer: Option<syn::Path>,
12921}
12922
12923#[derive(Default)]
12924struct ViewSetPermsAttrs {
12925 list: Vec<String>,
12926 retrieve: Vec<String>,
12927 create: Vec<String>,
12928 update: Vec<String>,
12929 destroy: Vec<String>,
12930}
12931
12932fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12933 let root = rustango_root();
12934 let struct_name = &input.ident;
12935
12936 // Must be a unit struct or an empty named struct.
12937 match &input.data {
12938 Data::Struct(s) => match &s.fields {
12939 Fields::Unit | Fields::Named(_) => {}
12940 Fields::Unnamed(_) => {
12941 return Err(syn::Error::new_spanned(
12942 struct_name,
12943 "ViewSet can only be derived on a unit struct or an empty named struct",
12944 ));
12945 }
12946 },
12947 _ => {
12948 return Err(syn::Error::new_spanned(
12949 struct_name,
12950 "ViewSet can only be derived on a struct",
12951 ));
12952 }
12953 }
12954
12955 let attrs = parse_viewset_attrs(input)?;
12956 let model_path = &attrs.model;
12957
12958 let field_lits = attrs.fields.iter().map(|f| f.as_str());
12959
12960 let filter_fields_call = if attrs.filter_fields.is_empty() {
12961 quote!()
12962 } else {
12963 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12964 quote!(.filter_fields(&[ #(#lits),* ]))
12965 };
12966
12967 let search_fields_call = if attrs.search_fields.is_empty() {
12968 quote!()
12969 } else {
12970 let lits = attrs.search_fields.iter().map(|f| f.as_str());
12971 quote!(.search_fields(&[ #(#lits),* ]))
12972 };
12973
12974 let ordering_call = if attrs.ordering.is_empty() {
12975 quote!()
12976 } else {
12977 let pairs = attrs.ordering.iter().map(|(f, desc)| {
12978 let f = f.as_str();
12979 quote!((#f, #desc))
12980 });
12981 quote!(.ordering(&[ #(#pairs),* ]))
12982 };
12983
12984 let page_size_call = if let Some(n) = attrs.page_size {
12985 quote!(.page_size(#n))
12986 } else {
12987 quote!()
12988 };
12989
12990 let read_only_call = if attrs.read_only {
12991 quote!(.read_only())
12992 } else {
12993 quote!()
12994 };
12995 let allow_anonymous_call = if attrs.allow_anonymous {
12996 quote!(.allow_anonymous())
12997 } else {
12998 quote!()
12999 };
13000
13001 // `.serializer::<S>()` — reshape responses through a derived
13002 // serializer. Requires the downstream crate to enable the
13003 // `serializer` feature (the method is gated on it).
13004 let serializer_call = if let Some(ref ser) = attrs.serializer {
13005 quote!(.serializer::<#ser>())
13006 } else {
13007 quote!()
13008 };
13009
13010 let perms = &attrs.perms;
13011 let perms_call = if perms.list.is_empty()
13012 && perms.retrieve.is_empty()
13013 && perms.create.is_empty()
13014 && perms.update.is_empty()
13015 && perms.destroy.is_empty()
13016 {
13017 quote!()
13018 } else {
13019 let list_lits = perms.list.iter().map(|s| s.as_str());
13020 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
13021 let create_lits = perms.create.iter().map(|s| s.as_str());
13022 let update_lits = perms.update.iter().map(|s| s.as_str());
13023 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
13024 quote! {
13025 .permissions(#root::viewset::ViewSetPerms {
13026 list: ::std::vec![ #(#list_lits.to_owned()),* ],
13027 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
13028 create: ::std::vec![ #(#create_lits.to_owned()),* ],
13029 update: ::std::vec![ #(#update_lits.to_owned()),* ],
13030 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
13031 })
13032 }
13033 };
13034
13035 Ok(quote! {
13036 impl #struct_name {
13037 /// Build an `axum::Router` with the six standard REST endpoints
13038 /// for this ViewSet, mounted at `prefix`.
13039 ///
13040 /// Accepts any backend's pool — a `sqlx::PgPool`,
13041 /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
13042 /// enum — because they all `Into<Pool>` (#1273). This is why
13043 /// the derive no longer names `PgPool`, which would not
13044 /// exist on a project built without the `postgres` feature.
13045 pub fn router(
13046 prefix: &str,
13047 pool: impl ::core::convert::Into<#root::sql::Pool>,
13048 ) -> #root::__axum::Router {
13049 #root::viewset::ViewSet::for_model(
13050 <#model_path as #root::core::Model>::SCHEMA,
13051 &[ #(#field_lits),* ],
13052 )
13053 #filter_fields_call
13054 #search_fields_call
13055 #ordering_call
13056 #page_size_call
13057 #perms_call
13058 #read_only_call
13059 #allow_anonymous_call
13060 #serializer_call
13061 .router_pool(prefix, pool.into())
13062 }
13063 }
13064 })
13065}
13066
13067fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
13068 let mut model: Option<syn::Path> = None;
13069 let mut fields: Option<Vec<String>> = None;
13070 let mut filter_fields: Vec<String> = Vec::new();
13071 let mut search_fields: Vec<String> = Vec::new();
13072 let mut ordering: Vec<(String, bool)> = Vec::new();
13073 let mut page_size: Option<usize> = None;
13074 let mut read_only = false;
13075 let mut allow_anonymous = false;
13076 let mut perms = ViewSetPermsAttrs::default();
13077 let mut serializer: Option<syn::Path> = None;
13078
13079 for attr in &input.attrs {
13080 if !attr.path().is_ident("viewset") {
13081 continue;
13082 }
13083 attr.parse_nested_meta(|meta| {
13084 if meta.path.is_ident("model") {
13085 let path: syn::Path = meta.value()?.parse()?;
13086 model = Some(path);
13087 return Ok(());
13088 }
13089 if meta.path.is_ident("serializer") {
13090 let path: syn::Path = meta.value()?.parse()?;
13091 serializer = Some(path);
13092 return Ok(());
13093 }
13094 if meta.path.is_ident("fields") {
13095 let s: LitStr = meta.value()?.parse()?;
13096 fields = Some(split_field_list(&s.value()));
13097 return Ok(());
13098 }
13099 if meta.path.is_ident("filter_fields") {
13100 let s: LitStr = meta.value()?.parse()?;
13101 filter_fields = split_field_list(&s.value());
13102 return Ok(());
13103 }
13104 if meta.path.is_ident("search_fields") {
13105 let s: LitStr = meta.value()?.parse()?;
13106 search_fields = split_field_list(&s.value());
13107 return Ok(());
13108 }
13109 if meta.path.is_ident("ordering") {
13110 let s: LitStr = meta.value()?.parse()?;
13111 ordering = parse_ordering_list(&s.value());
13112 return Ok(());
13113 }
13114 if meta.path.is_ident("page_size") {
13115 let lit: syn::LitInt = meta.value()?.parse()?;
13116 page_size = Some(lit.base10_parse::<usize>()?);
13117 return Ok(());
13118 }
13119 if meta.path.is_ident("read_only") {
13120 read_only = true;
13121 return Ok(());
13122 }
13123 if meta.path.is_ident("allow_anonymous") {
13124 allow_anonymous = true;
13125 return Ok(());
13126 }
13127 if meta.path.is_ident("permissions") {
13128 meta.parse_nested_meta(|inner| {
13129 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
13130 let s: LitStr = inner.value()?.parse()?;
13131 Ok(split_field_list(&s.value()))
13132 };
13133 if inner.path.is_ident("list") {
13134 perms.list = parse_codenames(&inner)?;
13135 } else if inner.path.is_ident("retrieve") {
13136 perms.retrieve = parse_codenames(&inner)?;
13137 } else if inner.path.is_ident("create") {
13138 perms.create = parse_codenames(&inner)?;
13139 } else if inner.path.is_ident("update") {
13140 perms.update = parse_codenames(&inner)?;
13141 } else if inner.path.is_ident("destroy") {
13142 perms.destroy = parse_codenames(&inner)?;
13143 } else {
13144 return Err(inner.error(
13145 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
13146 ));
13147 }
13148 Ok(())
13149 })?;
13150 return Ok(());
13151 }
13152 Err(meta.error(
13153 "unknown viewset attribute (supported: model, fields, filter_fields, \
13154 search_fields, ordering, page_size, read_only, allow_anonymous, serializer, \
13155 permissions(...))",
13156 ))
13157 })?;
13158 }
13159
13160 let model = model.ok_or_else(|| {
13161 syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
13162 })?;
13163 // The field list is the write allow-list, so it is never implied.
13164 let fields = fields.ok_or_else(|| {
13165 syn::Error::new_spanned(
13166 &input.ident,
13167 "`#[viewset(fields = \"id, title\")]` is required: the fields the API renders and writes",
13168 )
13169 })?;
13170
13171 Ok(ViewSetAttrs {
13172 model,
13173 fields,
13174 filter_fields,
13175 search_fields,
13176 ordering,
13177 page_size,
13178 read_only,
13179 allow_anonymous,
13180 perms,
13181 serializer,
13182 })
13183}
13184
13185// ============================================================ #[derive(Serializer)]
13186
13187struct SerializerContainerAttrs {
13188 model: syn::Path,
13189 /// `#[serializer(validate = "fn_name")]` on the struct — the
13190 /// cross-field validation hook (#436). The named inherent method
13191 /// must take `&self` and return
13192 /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
13193 /// `validate()` runs every per-field validator first; then calls
13194 /// the cross-field method if declared; aggregates all errors into
13195 /// one `FormErrors`.
13196 cross_validate: Option<syn::Ident>,
13197}
13198
13199#[derive(Default)]
13200struct SerializerFieldAttrs {
13201 read_only: bool,
13202 write_only: bool,
13203 source: Option<String>,
13204 skip: bool,
13205 /// `#[serializer(method = "fn_name")]` — computed field. The macro
13206 /// emits a `from_model` initializer that calls
13207 /// `Self::fn_name(&model)` and stores the return value.
13208 method: Option<String>,
13209 /// `#[serializer(validate = "fn_name")]` — per-field validator
13210 /// callable run by `Self::validate(&self)`. Must return
13211 /// `Result<(), String>`. Errors land in `FormErrors` keyed by
13212 /// the field name.
13213 validate: Option<String>,
13214 /// `#[serializer(nested)]` on a field whose type is another
13215 /// `Serializer` — the macro emits `from_model` initializer that
13216 /// reads the parent via `model.<source>.value()` then calls the
13217 /// child serializer's `from_model(parent)`. When the FK is
13218 /// unloaded the field falls back to `Default::default()` (does
13219 /// NOT panic) so a missing prefetch in prod degrades gracefully.
13220 /// Source field on the model defaults to the field name; override
13221 /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
13222 /// panic-on-unloaded behavior for tests.
13223 nested: bool,
13224 /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
13225 /// strict behavior: panic when the FK isn't loaded. Useful in
13226 /// test code where forgetting select_related must trip a hard
13227 /// failure rather than render a blank nested object.
13228 nested_strict: bool,
13229 /// `#[serializer(many = TagSerializer)]` — declare the field as
13230 /// a list of nested serializers. Field type must be `Vec<S>`
13231 /// where `S` is the inner serializer. The macro initializes the
13232 /// field to `Vec::new()` in `from_model` and emits a typed
13233 /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
13234 /// maps each model row through `S::from_model`. Auto-load isn't
13235 /// possible (the M2M / one-to-many accessor is async); callers
13236 /// fetch the children + call the setter post-from_model.
13237 many: Option<syn::Type>,
13238 /// `#[serializer(slug = "name")]` — render a relation as one of
13239 /// its fields.
13240 /// Source field on the model must be a `ForeignKey<T>`; the
13241 /// macro emits `from_model` glue that walks
13242 /// `model.<source>.value()?.<slug>` and clones it. Field type on
13243 /// the serializer is typically `String` (whatever type the slug
13244 /// column has). When the FK is unloaded the field falls back to
13245 /// `Default::default()`, same graceful-degrade contract as
13246 /// `nested`. Source defaults to the field name; override with
13247 /// `source = "..."`. v0.44.
13248 slug: Option<String>,
13249 /// `#[serializer(max_length = N)]` — caps
13250 /// the character count of a string field on write. Overrides the
13251 /// model's `max_length`; when absent the model value is inherited.
13252 max_length: Option<u64>,
13253 /// `#[serializer(min_length = N)]` — minimum character count.
13254 /// Serializer-only (the model has no `min_length` column).
13255 min_length: Option<u64>,
13256 /// `#[serializer(min = N)]` — inclusive
13257 /// integer lower bound; overrides the model's `min` when given.
13258 min: Option<i64>,
13259 /// `#[serializer(max = N)]` — inclusive
13260 /// integer upper bound; overrides the model's `max` when given.
13261 max: Option<i64>,
13262}
13263
13264fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
13265 let mut model: Option<syn::Path> = None;
13266 let mut cross_validate: Option<syn::Ident> = None;
13267 for attr in &input.attrs {
13268 if !attr.path().is_ident("serializer") {
13269 continue;
13270 }
13271 attr.parse_nested_meta(|meta| {
13272 if meta.path.is_ident("model") {
13273 let _eq: syn::Token![=] = meta.input.parse()?;
13274 model = Some(meta.input.parse()?);
13275 return Ok(());
13276 }
13277 if meta.path.is_ident("validate") {
13278 // #436 — container-level `validate = "fn_name"` for
13279 // cross-field validation. Field-level
13280 // `#[serializer(validate = "...")]` on a field is
13281 // parsed separately in `parse_serializer_field_attrs`.
13282 let s: LitStr = meta.value()?.parse()?;
13283 cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
13284 return Ok(());
13285 }
13286 Err(meta.error(
13287 "unknown serializer container attribute \
13288 (supported: `model`, `validate`)",
13289 ))
13290 })?;
13291 }
13292 let model = model.ok_or_else(|| {
13293 syn::Error::new_spanned(
13294 &input.ident,
13295 "`#[serializer(model = SomeModel)]` is required",
13296 )
13297 })?;
13298 Ok(SerializerContainerAttrs {
13299 model,
13300 cross_validate,
13301 })
13302}
13303
13304fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
13305 let mut out = SerializerFieldAttrs::default();
13306 for attr in &field.attrs {
13307 if !attr.path().is_ident("serializer") {
13308 continue;
13309 }
13310 attr.parse_nested_meta(|meta| {
13311 if meta.path.is_ident("read_only") {
13312 out.read_only = true;
13313 return Ok(());
13314 }
13315 if meta.path.is_ident("write_only") {
13316 out.write_only = true;
13317 return Ok(());
13318 }
13319 if meta.path.is_ident("skip") {
13320 out.skip = true;
13321 return Ok(());
13322 }
13323 if meta.path.is_ident("source") {
13324 let s: LitStr = meta.value()?.parse()?;
13325 out.source = Some(s.value());
13326 return Ok(());
13327 }
13328 if meta.path.is_ident("method") {
13329 let s: LitStr = meta.value()?.parse()?;
13330 out.method = Some(s.value());
13331 return Ok(());
13332 }
13333 if meta.path.is_ident("validate") {
13334 let s: LitStr = meta.value()?.parse()?;
13335 out.validate = Some(s.value());
13336 return Ok(());
13337 }
13338 if meta.path.is_ident("many") {
13339 let _eq: syn::Token![=] = meta.input.parse()?;
13340 out.many = Some(meta.input.parse()?);
13341 return Ok(());
13342 }
13343 if meta.path.is_ident("nested") {
13344 out.nested = true;
13345 // Optional strict flag inside parentheses:
13346 // #[serializer(nested(strict))]
13347 if meta.input.peek(syn::token::Paren) {
13348 meta.parse_nested_meta(|inner| {
13349 if inner.path.is_ident("strict") {
13350 out.nested_strict = true;
13351 return Ok(());
13352 }
13353 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
13354 })?;
13355 }
13356 return Ok(());
13357 }
13358 if meta.path.is_ident("slug") {
13359 let s: LitStr = meta.value()?.parse()?;
13360 out.slug = Some(s.value());
13361 return Ok(());
13362 }
13363 if meta.path.is_ident("max_length") {
13364 let lit: syn::LitInt = meta.value()?.parse()?;
13365 out.max_length = Some(lit.base10_parse::<u64>()?);
13366 return Ok(());
13367 }
13368 if meta.path.is_ident("min_length") {
13369 let lit: syn::LitInt = meta.value()?.parse()?;
13370 out.min_length = Some(lit.base10_parse::<u64>()?);
13371 return Ok(());
13372 }
13373 if meta.path.is_ident("min") {
13374 let lit: syn::LitInt = meta.value()?.parse()?;
13375 out.min = Some(lit.base10_parse::<i64>()?);
13376 return Ok(());
13377 }
13378 if meta.path.is_ident("max") {
13379 let lit: syn::LitInt = meta.value()?.parse()?;
13380 out.max = Some(lit.base10_parse::<i64>()?);
13381 return Ok(());
13382 }
13383 Err(meta.error(
13384 "unknown serializer field attribute (supported: \
13385 `read_only`, `write_only`, `source`, `skip`, `method`, \
13386 `validate`, `nested`, `many`, `slug`, `max_length`, \
13387 `min_length`, `min`, `max`)",
13388 ))
13389 })?;
13390 }
13391 // Validate: read_only + write_only is nonsensical
13392 if out.read_only && out.write_only {
13393 return Err(syn::Error::new_spanned(
13394 field,
13395 "a field cannot be both `read_only` and `write_only`",
13396 ));
13397 }
13398 if out.method.is_some() && out.source.is_some() {
13399 return Err(syn::Error::new_spanned(
13400 field,
13401 "`method` and `source` are mutually exclusive — `method` computes \
13402 the value from a method, `source` reads it from a different model field",
13403 ));
13404 }
13405 if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
13406 return Err(syn::Error::new_spanned(
13407 field,
13408 "`slug` is mutually exclusive with `method`, `nested`, and `many` \
13409 — pick one strategy for populating the field",
13410 ));
13411 }
13412 Ok(out)
13413}
13414
13415fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
13416 let root = rustango_root();
13417 let struct_name = &input.ident;
13418 let struct_name_lit = struct_name.to_string();
13419
13420 let Data::Struct(data) = &input.data else {
13421 return Err(syn::Error::new_spanned(
13422 struct_name,
13423 "Serializer can only be derived on structs",
13424 ));
13425 };
13426 let Fields::Named(named) = &data.fields else {
13427 return Err(syn::Error::new_spanned(
13428 struct_name,
13429 "Serializer requires a struct with named fields",
13430 ));
13431 };
13432
13433 let container = parse_serializer_container_attrs(input)?;
13434 let model_path = &container.model;
13435
13436 // Classify each field. `ty` is only consumed by the
13437 // `#[cfg(feature = "openapi")]` block below, but we always
13438 // capture it to keep the field-info build a single pass.
13439 #[allow(dead_code)]
13440 struct FieldInfo {
13441 ident: syn::Ident,
13442 ty: syn::Type,
13443 attrs: SerializerFieldAttrs,
13444 }
13445 let mut fields_info: Vec<FieldInfo> = Vec::new();
13446 for field in &named.named {
13447 let ident = field.ident.clone().expect("named field has ident");
13448 let attrs = parse_serializer_field_attrs(field)?;
13449 fields_info.push(FieldInfo {
13450 ident,
13451 ty: field.ty.clone(),
13452 attrs,
13453 });
13454 }
13455
13456 // Generate from_model body: struct literal with each field assigned.
13457 let from_model_fields = fields_info.iter().map(|fi| {
13458 let ident = &fi.ident;
13459 if let Some(_inner) = &fi.attrs.many {
13460 // Many — collection field. Initialize empty; caller
13461 // populates via the macro-emitted set_<field> helper
13462 // after fetching the M2M children.
13463 quote! { #ident: ::std::vec::Vec::new() }
13464 } else if let Some(method) = &fi.attrs.method {
13465 // SerializerMethodField: call Self::<method>(&model) to
13466 // compute the value. Method signature must be
13467 // `fn <method>(model: &T) -> <field type>`.
13468 let method_ident = syn::Ident::new(method, ident.span());
13469 quote! { #ident: Self::#method_ident(model) }
13470 } else if let Some(slug_field) = &fi.attrs.slug {
13471 // v0.44 — SlugRelatedField. Source defaults to the field
13472 // name on this struct; override via `source = "..."`. The
13473 // source field on the model is expected to be a
13474 // `ForeignKey<T>`; the slug field on the parent is named
13475 // by the attribute value. When the FK is unloaded the
13476 // field falls back to `Default::default()` — same
13477 // graceful-degrade contract as `nested`; a NULL
13478 // `Option<ForeignKey>` renders `None`.
13479 let src_name = fi
13480 .attrs
13481 .source
13482 .as_deref()
13483 .unwrap_or(&ident_name(&fi.ident))
13484 .to_owned();
13485 let src_ident = ident_from_name(&src_name, ident.span());
13486 let slug_ident = syn::Ident::new(slug_field, ident.span());
13487 quote! {
13488 #ident: #root::serializer::LoadedRelation::render(
13489 &model.#src_ident,
13490 |__loaded| ::core::clone::Clone::clone(&__loaded.#slug_ident),
13491 ::core::default::Default::default,
13492 )
13493 }
13494 } else if fi.attrs.nested {
13495 // Nested serializer. Source defaults to the field name on
13496 // this struct; override via `source = "..."`. The source
13497 // field on the model is a `ForeignKey<T>` or
13498 // `Option<ForeignKey<T>>`, read through `LoadedRelation`.
13499 //
13500 // Behavior matrix (tweakable per-field):
13501 // * FK loaded → nested object materializes via
13502 // ChildSerializer::from_model(parent).
13503 // * FK unloaded → fall back to ChildSerializer::default()
13504 // (so prod doesn't crash on a missing
13505 // prefetch — just renders a blank nested
13506 // object). Add `#[serializer(nested(
13507 // strict))]` to keep the v0.18.1
13508 // panic-on-unloaded behavior for tests
13509 // that want hard guardrails.
13510 // * NULL `Option<ForeignKey>` → `None` in an `Option<Child>`
13511 // field (JSON null), strict or not.
13512 let src_name = fi.attrs.source.as_deref().unwrap_or(&ident_name(&fi.ident)).to_owned();
13513 let src_ident = ident_from_name(&src_name, ident.span());
13514 let unloaded = if fi.attrs.nested_strict {
13515 let panic_msg = format!(
13516 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
13517 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
13518 );
13519 quote! { || ::core::panic!("{}", #panic_msg) }
13520 } else {
13521 quote! { ::core::default::Default::default }
13522 };
13523 quote! {
13524 #ident: #root::serializer::LoadedRelation::render(
13525 &model.#src_ident,
13526 |__loaded| #root::serializer::ModelSerializer::from_model(__loaded),
13527 #unloaded,
13528 )
13529 }
13530 } else if fi.attrs.write_only || fi.attrs.skip {
13531 // Not read from model — use default
13532 quote! { #ident: ::core::default::Default::default() }
13533 } else if let Some(src) = &fi.attrs.source {
13534 let src_ident = ident_from_name(src, ident.span());
13535 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13536 } else {
13537 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13538 }
13539 });
13540
13541 // is_writable predicate (also used by writable_lits /
13542 // writable_source_lits below).
13543 let is_writable = |fi: &&FieldInfo| {
13544 !fi.attrs.read_only
13545 && !fi.attrs.skip
13546 && fi.attrs.method.is_none()
13547 && !fi.attrs.nested
13548 && fi.attrs.many.is_none()
13549 && fi.attrs.slug.is_none()
13550 };
13551
13552 // Declarative field constraints: one block
13553 // per writable field, run inside `validate()`. Each resolves its
13554 // bounds as the serializer attr when given (`#[serializer(max_length
13555 // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13556 // `max` / `choices`), then dispatches on the field's JSON value via
13557 // `forms::validators::check_value` (string → length/choices, integer
13558 // → min/max).
13559 let opt_usize = |v: Option<u64>| match v {
13560 Some(n) => {
13561 let n = n as usize;
13562 quote!(::core::option::Option::Some(#n))
13563 }
13564 None => quote!(::core::option::Option::None),
13565 };
13566 let opt_i64 = |v: Option<i64>| match v {
13567 Some(n) => quote!(::core::option::Option::Some(#n)),
13568 None => quote!(::core::option::Option::None),
13569 };
13570 let constraint_blocks: Vec<_> = fields_info
13571 .iter()
13572 .filter(is_writable)
13573 .map(|fi| {
13574 let ident = &fi.ident;
13575 let fname = ident_name(ident);
13576 let mname = fi
13577 .attrs
13578 .source
13579 .clone()
13580 .unwrap_or_else(|| ident_name(&fi.ident));
13581 let attr_max_len = opt_usize(fi.attrs.max_length);
13582 let attr_min_len = opt_usize(fi.attrs.min_length);
13583 let attr_min = opt_i64(fi.attrs.min);
13584 let attr_max = opt_i64(fi.attrs.max);
13585 // A bound the field's type cannot take is a compile error, not a no-op.
13586 let ty = &fi.ty;
13587 let len_check = (fi.attrs.max_length.is_some() || fi.attrs.min_length.is_some())
13588 .then(|| quote! { #root::forms::validators::assert_length_bounded::<#ty>(); });
13589 let num_check = (fi.attrs.min.is_some() || fi.attrs.max.is_some())
13590 .then(|| quote! { #root::forms::validators::assert_number_bounded::<#ty>(); });
13591 quote! {
13592 #len_check
13593 #num_check
13594 if __sent(#fname) {
13595 let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13596 let __max_length: ::core::option::Option<usize> = #attr_max_len
13597 .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13598 let __min_length: ::core::option::Option<usize> = #attr_min_len;
13599 let __min: ::core::option::Option<i64> =
13600 #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13601 let __max: ::core::option::Option<i64> =
13602 #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13603 let __choices = __sf.and_then(|__f| __f.choices);
13604 let __v = #root::__serde_json::to_value(&self.#ident)
13605 .unwrap_or(#root::__serde_json::Value::Null);
13606 #root::forms::validators::check_value(
13607 #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13608 &mut __errors,
13609 );
13610 }
13611 }
13612 })
13613 .collect();
13614 let has_constraints = !constraint_blocks.is_empty();
13615
13616 // Per-field validators. Emit a
13617 // `validate(&self)` method that runs each user-defined validator
13618 // and aggregates errors into `FormErrors`.
13619 let validator_calls: Vec<_> = fields_info
13620 .iter()
13621 .filter_map(|fi| {
13622 let ident = &fi.ident;
13623 let name_lit = ident_name(ident);
13624 let method = fi.attrs.validate.as_ref()?;
13625 let method_ident = syn::Ident::new(method, ident.span());
13626 Some(quote! {
13627 if __sent(#name_lit) {
13628 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13629 __errors.add(#name_lit.to_owned(), __e);
13630 }
13631 }
13632 })
13633 })
13634 .collect();
13635 // #436 — cross-field `validate(&self)`. If the
13636 // container declared `#[serializer(validate = "fn_name")]`,
13637 // the macro-generated `validate(&self)` runs every per-field
13638 // validator first, then calls the user's cross-field method,
13639 // merging its `FormErrors` into the per-field errors. Either
13640 // alone is enough to emit the wrapper.
13641 let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13642 quote! {
13643 // Merge cross-field errors into the per-field bucket so
13644 // a single .validate() call surfaces both layers.
13645 if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13646 __errors.merge(__cross);
13647 }
13648 }
13649 });
13650 let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13651 // Anything to run? Declarative constraints (for any writable field) +
13652 // per-field validators + cross-field hook.
13653 let has_run_validations = has_validators || has_constraints;
13654 // Shared body: declarative field constraints first (length / range /
13655 // choices, serializer-attr-or-model), then per-field validators, then
13656 // the cross-field hook. Field rules run only where `__sent(name)` (#1995).
13657 let checks = quote! {
13658 let mut __errors = #root::forms::FormErrors::default();
13659 #( #constraint_blocks )*
13660 #( #validator_calls )*
13661 #cross_validate_call
13662 if __errors.is_empty() {
13663 ::core::result::Result::Ok(())
13664 } else {
13665 ::core::result::Result::Err(__errors)
13666 }
13667 };
13668 let validate_body = quote! {
13669 let __sent = |_: &str| true;
13670 #checks
13671 };
13672 // PATCH: only the keys the body carries.
13673 let validate_patch_body = quote! {
13674 let __sent = |__n: &str| __obj.is_some_and(|__o| __o.contains_key(__n));
13675 #checks
13676 };
13677 // Inherent `validate(&self)` — kept for back-compat with direct
13678 // `serializer.validate()` calls that don't import `ModelSerializer`.
13679 // Emitted only when the serializer declares per-field/cross-field
13680 // validators (unchanged rule) so it never collides with a
13681 // hand-written inherent `validate`.
13682 let validate_method = if has_validators {
13683 quote! {
13684 impl #struct_name {
13685 /// Run the declarative field constraints, every
13686 /// `#[serializer(validate = "...")]` per-field validator,
13687 /// and (when declared) the container-level cross-field
13688 /// validator. Aggregates errors into `FormErrors` keyed by
13689 /// the field name. Returns `Ok(())` when all pass.
13690 pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13691 #validate_body
13692 }
13693 }
13694 }
13695 } else {
13696 quote! {}
13697 };
13698 // Trait-method override so the type-erased ViewSet write path can
13699 // dispatch `ModelSerializer::validate` generically. Emitted whenever
13700 // there's anything to run (declarative constraints inherited from the
13701 // model count); otherwise the trait's default no-op is used.
13702 let trait_validate_override = if has_run_validations {
13703 quote! {
13704 fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13705 #validate_body
13706 }
13707 }
13708 } else {
13709 quote! {}
13710 };
13711
13712 // For every `#[serializer(many = S)]` field, emit a
13713 // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13714 // helper that maps the parents through `S::from_model`.
13715 let many_setters: Vec<_> = fields_info
13716 .iter()
13717 .filter_map(|fi| {
13718 let many_ty = fi.attrs.many.as_ref()?;
13719 let ident = &fi.ident;
13720 let setter = syn::Ident::new(&format!("set_{}", ident_name(ident)), ident.span());
13721 Some(quote! {
13722 /// Populate this `many` field by mapping each parent model
13723 /// through the inner serializer's `from_model`. Call after
13724 /// fetching the M2M / one-to-many children since
13725 /// `from_model` itself can't await an SQL query.
13726 pub fn #setter(
13727 &mut self,
13728 models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13729 ) -> &mut Self {
13730 self.#ident = models.iter()
13731 .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13732 .collect();
13733 self
13734 }
13735 })
13736 })
13737 .collect();
13738 let many_setters_impl = if many_setters.is_empty() {
13739 quote! {}
13740 } else {
13741 quote! {
13742 impl #struct_name {
13743 #( #many_setters )*
13744 }
13745 }
13746 };
13747
13748 // Generate custom Serialize: skip write_only fields
13749 let output_fields: Vec<_> = fields_info
13750 .iter()
13751 .filter(|fi| !fi.attrs.write_only)
13752 .collect();
13753 let output_field_count = output_fields.len();
13754 let serialize_fields = output_fields.iter().map(|fi| {
13755 let ident = &fi.ident;
13756 let name_lit = ident_name(ident);
13757 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13758 });
13759
13760 // writable_fields: normal + write_only.
13761 // Exclude:
13762 // - `read_only` — server-computed.
13763 // - `skip` — caller sets manually post-from_model.
13764 // - `method` — computed from a Self::fn(&model) call; accepting
13765 // it on write is meaningless.
13766 // - `nested` / `many` — populated from related-model data, not
13767 // from a field on the wire body.
13768 // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13769 // `many` in `writable_fields()`, which made the ViewSet write
13770 // path accept those fields from the JSON body and try to bind
13771 // them to the SQL UPDATE — a silent no-op at best, a type
13772 // mismatch at worst.
13773 // (`is_writable` is defined above, near the constraint codegen.)
13774 let writable_lits: Vec<_> = fields_info
13775 .iter()
13776 .filter(is_writable)
13777 .map(|fi| ident_name(&fi.ident))
13778 .collect();
13779
13780 // `writable_source_fields`: the MODEL field names of writable
13781 // serializer fields (source-resolved). The ViewSet write path skips
13782 // every model column NOT in this set, so `read_only` / `method` /
13783 // computed fields a client posts are ignored instead of written.
13784 let writable_source_lits: Vec<String> = fields_info
13785 .iter()
13786 .filter(is_writable)
13787 .map(|fi| {
13788 fi.attrs
13789 .source
13790 .clone()
13791 .unwrap_or_else(|| ident_name(&fi.ident))
13792 })
13793 .collect();
13794
13795 // `readable_source_fields`: model columns the JSON output copies
13796 // straight from the model, so `?ordering=` cannot sort on a column
13797 // the API never shows (#1845).
13798 let readable_source_lits: Vec<String> = fields_info
13799 .iter()
13800 .filter(|fi| {
13801 !fi.attrs.write_only
13802 && !fi.attrs.skip
13803 && fi.attrs.method.is_none()
13804 && !fi.attrs.nested
13805 && fi.attrs.many.is_none()
13806 && fi.attrs.slug.is_none()
13807 })
13808 .map(|fi| {
13809 fi.attrs
13810 .source
13811 .clone()
13812 .unwrap_or_else(|| ident_name(&fi.ident))
13813 })
13814 .collect();
13815
13816 // `from_writable_json`: build a partial instance for input
13817 // validation. Writable fields are parsed from the JSON body (keyed
13818 // by serializer field name); every other field defaults. Per-field
13819 // type errors collect into `FormErrors` keyed by the field name.
13820 // Construction is field-by-field (not `Self::default()`) so it works
13821 // for serializers regardless of a struct-level `Default`.
13822 let from_writable_json_inits: Vec<_> = fields_info
13823 .iter()
13824 .map(|fi| {
13825 let ident = &fi.ident;
13826 let fname = ident_name(ident);
13827 let ty = &fi.ty;
13828 if is_writable(&fi) {
13829 quote! {
13830 #ident: match __obj.and_then(|__o| __o.get(#fname)) {
13831 ::core::option::Option::Some(__v) => {
13832 match #root::__serde_json::from_value::<#ty>(
13833 ::core::clone::Clone::clone(__v),
13834 ) {
13835 ::core::result::Result::Ok(__x) => __x,
13836 ::core::result::Result::Err(__e) => {
13837 __errors.add(#fname.to_owned(), __e.to_string());
13838 ::core::default::Default::default()
13839 }
13840 }
13841 }
13842 ::core::option::Option::None => ::core::default::Default::default(),
13843 }
13844 }
13845 } else {
13846 quote! { #ident: ::core::default::Default::default() }
13847 }
13848 })
13849 .collect();
13850 // `validate_patch`: overlay each sent writable field on the stored row.
13851 let patch_overlays: Vec<_> = fields_info
13852 .iter()
13853 .filter(is_writable)
13854 .map(|fi| {
13855 let ident = &fi.ident;
13856 let fname = ident_name(ident);
13857 let ty = &fi.ty;
13858 quote! {
13859 if let ::core::option::Option::Some(__v) = __obj.and_then(|__o| __o.get(#fname)) {
13860 match #root::__serde_json::from_value::<#ty>(::core::clone::Clone::clone(__v)) {
13861 ::core::result::Result::Ok(__x) => __merged.#ident = __x,
13862 ::core::result::Result::Err(__e) => {
13863 __errors.add(#fname.to_owned(), __e.to_string());
13864 }
13865 }
13866 }
13867 }
13868 })
13869 .collect();
13870 let patch_validate_call = if has_run_validations {
13871 quote! { __merged.__rustango_validate_patch(__obj) }
13872 } else {
13873 quote! { let _ = &__merged; ::core::result::Result::Ok(()) }
13874 };
13875 let patch_validate_helper = if has_run_validations {
13876 quote! {
13877 impl #struct_name {
13878 #[doc(hidden)]
13879 fn __rustango_validate_patch(
13880 &self,
13881 __obj: ::core::option::Option<&#root::__serde_json::Map<::std::string::String, #root::__serde_json::Value>>,
13882 ) -> ::core::result::Result<(), #root::forms::FormErrors> {
13883 #validate_patch_body
13884 }
13885 }
13886 }
13887 } else {
13888 quote! {}
13889 };
13890
13891 // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
13892 // Only includes fields shown in JSON output (skips write_only). For each
13893 // `Option<T>` field, omit from `required` and add `.nullable()`.
13894 let openapi_impl = {
13895 #[cfg(feature = "openapi")]
13896 {
13897 let property_calls = output_fields.iter().map(|fi| {
13898 let ident = &fi.ident;
13899 let name_lit = ident_name(ident);
13900 let ty = &fi.ty;
13901 let nullable_call = if is_option(ty) {
13902 quote! { .nullable() }
13903 } else {
13904 quote! {}
13905 };
13906 quote! {
13907 .property(
13908 #name_lit,
13909 <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13910 #nullable_call,
13911 )
13912 }
13913 });
13914 let required_lits: Vec<_> = output_fields
13915 .iter()
13916 .filter(|fi| !is_option(&fi.ty))
13917 .map(|fi| ident_name(&fi.ident))
13918 .collect();
13919 quote! {
13920 impl #root::openapi::OpenApiSchema for #struct_name {
13921 fn openapi_schema() -> #root::openapi::Schema {
13922 #root::openapi::Schema::object()
13923 #( #property_calls )*
13924 .required([ #( #required_lits ),* ])
13925 }
13926 }
13927 }
13928 }
13929 #[cfg(not(feature = "openapi"))]
13930 {
13931 quote! {}
13932 }
13933 };
13934
13935 Ok(quote! {
13936 impl #root::serializer::ModelSerializer for #struct_name {
13937 type Model = #model_path;
13938
13939 fn from_model(model: &Self::Model) -> Self {
13940 Self {
13941 #( #from_model_fields ),*
13942 }
13943 }
13944
13945 fn writable_fields() -> &'static [&'static str] {
13946 &[ #( #writable_lits ),* ]
13947 }
13948
13949 fn writable_source_fields() -> &'static [&'static str] {
13950 &[ #( #writable_source_lits ),* ]
13951 }
13952
13953 fn readable_source_fields() -> &'static [&'static str] {
13954 &[ #( #readable_source_lits ),* ]
13955 }
13956
13957 fn from_writable_json(
13958 __body: &#root::__serde_json::Value,
13959 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
13960 let mut __errors = #root::forms::FormErrors::default();
13961 let __obj = __body.as_object();
13962 let __out = Self {
13963 #( #from_writable_json_inits ),*
13964 };
13965 if __errors.is_empty() {
13966 ::core::result::Result::Ok(__out)
13967 } else {
13968 ::core::result::Result::Err(__errors)
13969 }
13970 }
13971
13972 fn validate_patch(
13973 __stored: &Self::Model,
13974 __body: &#root::__serde_json::Value,
13975 ) -> ::core::result::Result<(), #root::forms::FormErrors> {
13976 let mut __errors = #root::forms::FormErrors::default();
13977 let __obj = __body.as_object();
13978 #[allow(unused_mut)]
13979 let mut __merged = <Self as #root::serializer::ModelSerializer>::from_model(__stored);
13980 #( #patch_overlays )*
13981 if !__errors.is_empty() {
13982 return ::core::result::Result::Err(__errors);
13983 }
13984 #patch_validate_call
13985 }
13986
13987 #trait_validate_override
13988 }
13989
13990 #patch_validate_helper
13991
13992 impl #root::__serde::Serialize for #struct_name {
13993 fn serialize<S>(&self, serializer: S)
13994 -> ::core::result::Result<S::Ok, S::Error>
13995 where
13996 S: #root::__serde::Serializer,
13997 {
13998 use #root::__serde::ser::SerializeStruct;
13999 let mut __state = serializer.serialize_struct(
14000 #struct_name_lit,
14001 #output_field_count,
14002 )?;
14003 #( #serialize_fields )*
14004 __state.end()
14005 }
14006 }
14007
14008 #openapi_impl
14009
14010 #validate_method
14011
14012 #many_setters_impl
14013 })
14014}
14015
14016/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
14017/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
14018/// when the feature is off.
14019#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
14020fn is_option(ty: &syn::Type) -> bool {
14021 if let syn::Type::Path(p) = ty {
14022 if let Some(last) = p.path.segments.last() {
14023 return last.ident == "Option";
14024 }
14025 }
14026 false
14027}
14028
14029#[cfg(test)]
14030mod main_attr_tests {
14031 use super::*;
14032
14033 fn expand(args: &str) -> String {
14034 let body = quote! { async fn main() { run().await } };
14035 expand_main(args.parse().unwrap(), body)
14036 .expect("expansion failed")
14037 .to_string()
14038 }
14039
14040 /// Asserts on the expansion rather than on `parse_logging`,
14041 /// because the defect was a subscriber reaching the output, not
14042 /// an argument being misread (#1465).
14043 #[test]
14044 fn logging_false_emits_no_subscriber() {
14045 let out = expand("logging = false");
14046 assert!(
14047 !out.contains("logging :: Setup"),
14048 "`logging = false` must not install a subscriber:\n{out}",
14049 );
14050 assert!(
14051 out.contains("block_on"),
14052 "the runtime must still be built:\n{out}",
14053 );
14054 }
14055
14056 #[test]
14057 fn default_still_installs_a_subscriber() {
14058 // The control. Without it, an `expand_main` that dropped the
14059 // prologue unconditionally would pass the test above while
14060 // leaving every default project with no logging at all.
14061 let out = expand("");
14062 assert!(
14063 out.contains("logging :: Setup"),
14064 "the default entrypoint must still install one:\n{out}",
14065 );
14066 // `./app serve` or `./app --port 8080` must keep logging to stdout:
14067 // only `Cli`, which knows its verbs, moves logs to stderr.
14068 assert!(
14069 !out.contains("args ()"),
14070 "the entrypoint must not route logs by argv:\n{out}",
14071 );
14072 }
14073
14074 #[test]
14075 fn logging_false_composes_with_flavor() {
14076 let out = expand("flavor = \"current_thread\", logging = false");
14077 assert!(!out.contains("logging :: Setup"), "{out}");
14078 assert!(out.contains("new_current_thread"), "{out}");
14079 }
14080
14081 #[test]
14082 fn logging_true_is_the_default_shape() {
14083 // `logging = true` is accepted and means the default, so the
14084 // match must not fire on the substring `logging`.
14085 assert!(parse_logging(&"logging = true".parse().unwrap()));
14086 assert!(!parse_logging(&"logging=false".parse().unwrap()));
14087 }
14088}
14089
14090#[cfg(test)]
14091mod main_arg_warning_tests {
14092 use super::*;
14093
14094 fn expand(args: &str) -> String {
14095 let body = quote! { async fn main() { run().await } };
14096 expand_main(args.parse().unwrap(), body)
14097 .expect("expansion failed")
14098 .to_string()
14099 }
14100
14101 /// A typo or unknown key used to be dropped without a word.
14102 #[test]
14103 fn unknown_or_misspelt_args_warn() {
14104 for args in ["logging = flase", "multi_thread", "workers = 4"] {
14105 let out = expand(args);
14106 assert!(out.contains("unknown argument"), "{args}:\n{out}");
14107 }
14108 }
14109
14110 #[test]
14111 fn supported_args_do_not_warn() {
14112 for args in [
14113 "",
14114 r#"flavor = "current_thread""#,
14115 r#"flavor = "multi_thread", logging = false"#,
14116 "logging = true",
14117 "worker_threads = 4, start_paused = true",
14118 r#"crate = "tokio", unhandled_panic = "shutdown_runtime""#,
14119 r#"flavor = "curent_thread""#,
14120 ] {
14121 assert!(!expand(args).contains("RUSTANGO_WARNING"), "{args}");
14122 }
14123 }
14124}
14125
14126#[cfg(test)]
14127mod embed_migrations_tests {
14128 use super::*;
14129
14130 fn expand(path: &str) -> String {
14131 expand_embed_migrations(quote! { #path })
14132 .expect("expansion failed")
14133 .to_string()
14134 }
14135
14136 #[test]
14137 fn missing_dir_warns() {
14138 let out = expand("./no_such_migrations_dir");
14139 assert!(out.contains("deprecated"), "missing dir must warn:\n{out}");
14140 assert!(out.contains("no_such_migrations_dir"), "{out}");
14141 }
14142
14143 #[test]
14144 fn existing_dir_does_not_warn() {
14145 let out = expand("./src");
14146 assert!(!out.contains("deprecated"), "{out}");
14147 }
14148}
14149
14150#[cfg(test)]
14151mod field_attr_tests {
14152 use super::*;
14153
14154 fn error_for(field: syn::Field) -> Option<String> {
14155 let attrs = parse_field_attrs(&field).unwrap();
14156 let kind = detect_type(&field.ty).unwrap().kind;
14157 check_bound_compatibility(&field, &attrs, kind)
14158 .err()
14159 .map(|e| e.to_string())
14160 }
14161
14162 /// `citext`, `vector` and `geometry` on the wrong type are errors, not no-ops (#1937).
14163 #[test]
14164 fn type_specific_attrs_reject_other_types() {
14165 let f: syn::FieldsNamed = syn::parse_quote!({
14166 #[rustango(citext)] a: i64,
14167 #[rustango(vector(dims = 3))] b: String,
14168 #[rustango(geometry(srid = 4326))] c: String,
14169 #[rustango(citext)] d: String,
14170 });
14171 let errs: Vec<_> = f.named.into_iter().map(error_for).collect();
14172 assert!(errs[0].as_deref().unwrap().contains("citext"), "{errs:?}");
14173 assert!(errs[1].as_deref().unwrap().contains("vector"), "{errs:?}");
14174 assert!(errs[2].as_deref().unwrap().contains("geometry"), "{errs:?}");
14175 assert_eq!(errs[3], None);
14176 }
14177
14178 /// `manager(ext)` emitted a trait nobody could add methods to; it is refused (#2132).
14179 #[test]
14180 fn manager_ext_is_refused() {
14181 let input: DeriveInput = syn::parse_quote! {
14182 #[rustango(table = "p", manager(ext = "PostManagerExt"))]
14183 struct Post { id: i64 }
14184 };
14185 let err = parse_container_attrs(&input).err().expect("refused");
14186 assert!(err.to_string().contains("#2132"), "{err}");
14187 }
14188
14189 /// `set_null` on a NOT NULL `ForeignKey<T>` would fail every parent delete.
14190 #[test]
14191 fn set_null_needs_an_optional_foreign_key() {
14192 let input: DeriveInput = syn::parse_quote! {
14193 #[rustango(table = "c")]
14194 struct Child {
14195 #[rustango(primary_key)] id: i64,
14196 #[rustango(on_delete = "set_null")] parent: ForeignKey<Parent>,
14197 }
14198 };
14199 let err = expand(&input).err().expect("refused");
14200 assert!(err.to_string().contains("set_null"), "{err}");
14201 let input: DeriveInput = syn::parse_quote! {
14202 #[rustango(table = "c")]
14203 struct Child {
14204 #[rustango(primary_key)] id: i64,
14205 #[rustango(on_delete = "set_null")] parent: Option<ForeignKey<Parent>>,
14206 }
14207 };
14208 assert!(expand(&input).is_ok());
14209 }
14210
14211 /// The same for `fk = "…"` / `o2o = "…"` on a plain column.
14212 #[test]
14213 fn set_null_needs_a_nullable_fk_column() {
14214 let input: DeriveInput = syn::parse_quote! {
14215 #[rustango(table = "c")]
14216 struct Child {
14217 #[rustango(primary_key)] id: i64,
14218 #[rustango(fk = "p", on_delete = "set_null")] parent_id: i64,
14219 #[rustango(o2o = "q", on_delete = "set_null")] other_id: Option<i64>,
14220 }
14221 };
14222 let err = expand(&input).err().expect("refused");
14223 assert!(err.to_string().contains("set_null"), "{err}");
14224 let input: DeriveInput = syn::parse_quote! {
14225 #[rustango(table = "c")]
14226 struct Child {
14227 #[rustango(primary_key)] id: i64,
14228 #[rustango(fk = "p", on_delete = "set_null")] parent_id: std::option::Option<i64>,
14229 #[rustango(o2o = "q", on_delete = "set_null")] other_id: ::core::option::Option<i64>,
14230 #[rustango(on_delete = "set_null")] parent: core::option::Option<ForeignKey<Parent>>,
14231 }
14232 };
14233 assert!(expand(&input).is_ok());
14234 }
14235
14236 /// An index or `get_latest_by` naming no column warns; it does not fail.
14237 #[test]
14238 fn unknown_index_columns_warn() {
14239 let input: DeriveInput = syn::parse_quote! {
14240 #[rustango(
14241 table = "w",
14242 unique_together = "a, nope",
14243 index_together = "a, b_typo",
14244 index("missing"),
14245 get_latest_by = "-when_typo"
14246 )]
14247 struct W { #[rustango(primary_key)] id: i64, a: i64, b: i64 }
14248 };
14249 let out = expand(&input).expect("still compiles").to_string();
14250 for name in ["nope", "b_typo", "missing", "when_typo"] {
14251 assert!(out.contains(&format!("`{name}`")), "{name}:\n{out}");
14252 }
14253 }
14254
14255 /// Real columns, field names for `get_latest_by` and expression
14256 /// indexes emit nothing new.
14257 #[test]
14258 fn known_index_columns_do_not_warn() {
14259 let input: DeriveInput = syn::parse_quote! {
14260 #[rustango(
14261 table = "w",
14262 unique_together = "a, b_col",
14263 index("lower(a)"),
14264 get_latest_by = "-b"
14265 )]
14266 struct W {
14267 #[rustango(primary_key)] id: i64,
14268 a: i64,
14269 #[rustango(column = "b_col")] b: i64,
14270 }
14271 };
14272 let out = expand(&input).expect("compiles").to_string();
14273 assert!(!out.contains("RUSTANGO_WARNING"), "{out}");
14274 }
14275
14276 /// Two FKs to one parent would emit the same reverse accessor twice.
14277 #[test]
14278 fn reverse_accessor_clash_names_related_name() {
14279 let input: DeriveInput = syn::parse_quote! {
14280 #[rustango(table = "m")]
14281 struct Message {
14282 #[rustango(primary_key)] id: i64,
14283 sender: ForeignKey<User>,
14284 recipient: ForeignKey<User>,
14285 }
14286 };
14287 let err = expand(&input).err().expect("refused");
14288 assert!(err.to_string().contains("related_name"), "{err}");
14289 }
14290
14291 /// `related_name = "+"` emits no reverse accessor, so a parent from
14292 /// another crate, which cannot take an inherent impl, still derives.
14293 #[test]
14294 fn related_name_plus_skips_the_reverse_accessor() {
14295 let input: DeriveInput = syn::parse_quote! {
14296 #[rustango(table = "m")]
14297 struct Message {
14298 #[rustango(primary_key)] id: i64,
14299 sender: ForeignKey<User>,
14300 #[rustango(related_name = "+")] recipient: ForeignKey<User>,
14301 #[rustango(related_name = "+")] owner: ForeignKey<other::Account>,
14302 }
14303 };
14304 let out = expand(&input).expect("derives").to_string();
14305 assert_eq!(out.matches("message_set_pool").count(), 1, "{out}");
14306 assert!(!out.contains("impl other :: Account {"), "{out}");
14307 }
14308
14309 /// A raw ident derives names without `r#`; this used to panic (#1937).
14310 #[test]
14311 fn raw_idents_derive_plain_names() {
14312 let ident: syn::Ident = syn::parse_quote!(r#type);
14313 assert_eq!(column_type_ident(&ident).to_string(), "type_col");
14314 assert_eq!(ident_from_name("type", ident.span()).to_string(), "r#type");
14315 }
14316}