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, rgo))]
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; requests are capped
87/// at `ViewSet::max_page_size`, default 100).
88/// * `read_only` — flag; wires only `list` + `retrieve` (no mutations).
89/// * `allow_anonymous` — flag; write actions without codenames are
90/// intended, so the mount warning is skipped.
91/// * `serializer = SomeSerializer` — render list / retrieve / create
92/// responses through a `#[derive(Serializer)]` type instead of the
93/// default field-level projection (`read_only` / `source` / `method`
94/// / `write_only` overrides apply). Tri-dialect. Requires the
95/// `serializer` feature.
96/// * `permissions(list = "...", retrieve = "...", create = "...",
97/// update = "...", destroy = "...")` — codenames required per action.
98#[proc_macro_derive(ViewSet, attributes(viewset))]
99pub fn derive_viewset(input: TokenStream) -> TokenStream {
100 let input = parse_macro_input!(input as DeriveInput);
101 expand_viewset(&input)
102 .unwrap_or_else(syn::Error::into_compile_error)
103 .into()
104}
105
106/// Derive `rustango::forms::Form` (slice 8.4B). Generates a
107/// `parse(&HashMap<String, String>) -> Result<Self, FormErrors>` impl
108/// that walks every named field and:
109///
110/// * Parses the string value into the field's Rust type (`String`,
111/// `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
112/// nullable case).
113/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
114/// `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
115/// validators in declaration order, returning `FormError::Parse`
116/// on the first failure.
117///
118/// Example:
119///
120/// ```ignore
121/// #[derive(Form)]
122/// pub struct CreateItemForm {
123/// #[form(min_length = 1, max_length = 64)]
124/// pub name: String,
125/// #[form(min = 0, max = 150)]
126/// pub age: i32,
127/// pub active: bool,
128/// pub email: Option<String>,
129/// }
130///
131/// let parsed = CreateItemForm::parse(&form_map)?;
132/// ```
133#[proc_macro_derive(Form, attributes(form))]
134pub fn derive_form(input: TokenStream) -> TokenStream {
135 let input = parse_macro_input!(input as DeriveInput);
136 expand_form(&input)
137 .unwrap_or_else(syn::Error::into_compile_error)
138 .into()
139}
140
141/// Derive `rustango::serializer::ModelSerializer` for a struct.
142/// (intra-doc link disabled — the macro crate doesn't depend on
143/// `rustango` itself, so rustdoc can't resolve the path.)
144///
145/// # Container attribute (required)
146/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
147///
148/// # Field attributes
149/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
150/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
151/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
152/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
153/// - `#[serializer(method = "fn_name")]` — computed field: calls `Self::fn_name(&model)` for the field value; excluded from `writable_fields()`
154/// - `#[serializer(nested)]` / `nested(strict)` — auto-resolves nested serializer from a loaded `ForeignKey`; excluded from `writable_fields()`
155/// - `#[serializer(many = ChildSerializer)]` — collection of nested serializers; populated via macro-emitted `set_<field>(&[Child::Model])`; excluded from `writable_fields()`
156/// - `#[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)
157/// - `#[serializer(validate = "fn_name")]` — per-field validator surfaced by `Self::validate(&self)`
158/// - `#[serializer(max_length = N)]` / `min_length` / `min` / `max` — declarative bounds checked on
159/// write; auto-inherit from the model's `FieldSchema` (`max_length`/`min`/`max`/`choices`) when
160/// no attr is given, override it when given (`min_length` is serializer-only)
161///
162/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
163/// It honours `#[serde(rename, rename_all, skip, skip_serializing, skip_serializing_if,
164/// skip_deserializing)]`; other `serde` keys are ignored.
165#[proc_macro_derive(Serializer, attributes(serializer, serde))]
166pub fn derive_serializer(input: TokenStream) -> TokenStream {
167 let input = parse_macro_input!(input as DeriveInput);
168 expand_serializer(&input)
169 .unwrap_or_else(syn::Error::into_compile_error)
170 .into()
171}
172
173/// Bake every `*.json` migration file in a directory into the binary
174/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
175/// of `(name, json_content)` pairs, lex-sorted by file stem.
176///
177/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
178/// behaviour as `migrate(pool, dir)` but with no filesystem access.
179/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
180/// (i.e. the crate that invokes the macro). Default is
181/// `"./migrations"` if no argument is supplied.
182///
183/// ```ignore
184/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
185/// // or:
186/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
187///
188/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
189/// ```
190///
191/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
192/// file's `name` field must equal its file stem, every `prev`
193/// reference must point to another migration in the same directory,
194/// and the JSON must parse. A broken chain — orphan `prev`, missing
195/// predecessor, malformed file — fails at macro-expansion time with
196/// a clear `compile_error!`. *No other Rust web framework
197/// validates migration chains at compile time*: Cot's migrations are
198/// imperative Rust code (no static chain), Loco's are SeaORM
199/// up/down (same), Rwf's are raw SQL (no chain at all).
200///
201/// Each migration is included via `include_str!` so cargo's rebuild
202/// detection picks up file *content* changes. Cargo does not watch
203/// directory listings, so a new migration file is missed until the
204/// crate rebuilds. Add a `build.rs` that tracks the directory:
205///
206/// ```ignore
207/// fn main() { println!("cargo:rerun-if-changed=migrations"); }
208/// ```
209///
210/// A missing directory embeds nothing and emits a deprecation warning.
211#[proc_macro]
212pub fn embed_migrations(input: TokenStream) -> TokenStream {
213 expand_embed_migrations(input.into())
214 .unwrap_or_else(syn::Error::into_compile_error)
215 .into()
216}
217
218/// `Q!()` — `Model.field__lookup = value` filter syntax, resolved at
219/// compile time against typed columns. Issue #269 / T1.7.
220///
221/// Each invocation lowers to the equivalent typed-column method call:
222///
223/// ```ignore
224/// // These expand identically:
225/// Q!(User.email__icontains = "alice")
226/// User::email.ilike("%alice%")
227/// ```
228///
229/// Field-name typos fail the build (the macro emits `User::no_such_field`
230/// which doesn't exist) — the headline ergonomic win of this slice over
231/// filters keyed by a plain string.
232///
233/// # Supported lookup suffixes
234///
235/// * bare `=` / `__exact` → `.eq(value)`
236/// * `__iexact` → `.ilike(value)` (case-insensitive equality, no wildcards)
237/// * `__ne` → `.ne(value)`
238/// * `__gt` / `__gte` / `__lt` / `__lte` → corresponding comparison
239/// * `__contains` / `__icontains` → `.like("%v%")` / `.ilike("%v%")`
240/// * `__startswith` / `__istartswith` → `.like("v%")` / `.ilike("v%")`
241/// * `__endswith` / `__iendswith` → `.like("%v")` / `.ilike("%v")`
242/// * `__in` → `.is_in(iterable)`
243/// * `__not_in` → `.not_in(iterable)`
244/// * `__isnull = true` → `.is_null()`; `__isnull = false` → `.is_not_null()`
245/// * `__between` accepts a tuple literal `(lo, hi)` → `.between(lo, hi)`
246/// * `__regex` / `__iregex` → `.regex(pattern)` / `.iregex(pattern)`
247///
248/// Unknown suffixes fail the build with a `compile_error!` pointing at
249/// the lookup token.
250///
251/// # Combine
252///
253/// Each `Q!()` returns a `TypedFilter<Model>` — chain via the existing
254/// `.and()` / `.or()` / `.not()` methods:
255///
256/// ```ignore
257/// User::objects()
258/// .where_(
259/// Q!(User.active = true)
260/// .and(Q!(User.email__icontains = "alice"))
261/// )
262/// .fetch(&pool).await?;
263/// ```
264///
265/// All emitted code routes through existing per-dialect writers — no new
266/// SQL emission machinery. Tri-dialect support is inherent.
267#[allow(non_snake_case)]
268#[proc_macro]
269pub fn Q(input: TokenStream) -> TokenStream {
270 expand_q(input.into())
271 .unwrap_or_else(syn::Error::into_compile_error)
272 .into()
273}
274
275/// `#[rustango::main]` — the runserver entrypoint. Wraps
276/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
277/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
278/// functions are zero-boilerplate. `RUST_LOG` and
279/// `RUSTANGO__LOGGING__FORMAT` (e.g. `json`) come from the real
280/// environment, else from `./.env` (only those keys are read):
281///
282/// ```ignore
283/// #[rustango::main]
284/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
285/// rustango::server::Builder::from_env().await?
286/// .migrate("migrations").await?
287/// .api(my_app::urls::api())
288/// .seed_with(my_app::seed::run).await?
289/// .serve("0.0.0.0:8080").await
290/// }
291/// ```
292///
293/// Optional `flavor = "current_thread"` passes through to
294/// `#[tokio::main]`; default is the multi-threaded runtime.
295///
296/// `logging = false` suppresses the default subscriber, for apps
297/// that install their own. Needed whenever the body reaches
298/// `Cli::with_logging()` or `logging::Setup`: the subscriber this
299/// macro installs gets there first, and `tracing` keeps the first
300/// one, so the `[logging]` section would otherwise be read and
301/// discarded (#1465).
302///
303/// ```ignore
304/// #[rustango::main(logging = false)]
305/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
306/// rustango::manage::Cli::new()
307/// .with_settings_from_env()
308/// .with_logging() // now the first installer
309/// .run().await
310/// }
311/// ```
312///
313/// Pulls `tracing-subscriber` into the rustango crate behind the
314/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
315/// out get plain `#[tokio::main]` ergonomics without the dependency.
316#[proc_macro_attribute]
317pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
318 expand_main(args.into(), item.into())
319 .unwrap_or_else(syn::Error::into_compile_error)
320 .into()
321}
322
323fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
324 let mut input: syn::ItemFn = syn::parse2(item)?;
325 if input.sig.asyncness.is_none() {
326 return Err(syn::Error::new(
327 input.sig.ident.span(),
328 "`#[rustango::main]` must wrap an `async fn`",
329 ));
330 }
331
332 // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
333 // to `#[tokio::main]`. Tokio's proc-macro internally emits
334 // `::tokio::*` paths that resolve against the user crate's deps,
335 // so calling it through the rustango re-export still requires the
336 // user to add tokio to their own Cargo.toml. Building the
337 // runtime ourselves keeps the dep transitive through the
338 // `runtime` feature on rustango.
339 //
340 // Parse optional `flavor = "current_thread"` / `flavor =
341 // "multi_thread"` from the attribute args. Unknown args still
342 // compile, with a warning that they are ignored.
343 let root = rustango_root();
344 let arg_warnings = main_arg_warnings(&args);
345 let flavor = parse_flavor(&args);
346 let builder_call = match flavor {
347 Flavor::CurrentThread => quote! {
348 #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
349 },
350 Flavor::MultiThread => quote! {
351 #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
352 },
353 };
354
355 // Detach the user body and rewrite `main` as a sync fn that
356 // builds the runtime and blocks on the async body.
357 let user_body = input.block.clone();
358 input.sig.asyncness = None;
359
360 // `logging = false` drops the block entirely, so a later
361 // `Cli::with_logging()` is the first installer and its
362 // `[logging]` settings actually take effect (#1465).
363 let logging_prologue = if parse_logging(&args) {
364 quote! {
365 // `Setup`'s defaults, not a copy of them: filter, format and
366 // the `Color::Auto` rule live in one place (#1493).
367 // `logging` and this macro share the `runtime` gate.
368 // The real `RUST_LOG` still wins over `./.env`'s (#2204).
369 let _ = #root::logging::Setup::new()
370 .with_default_env_filter(#root::__private_runtime::main_default_filter())
371 .with_format(#root::__private_runtime::main_format())
372 .install();
373 }
374 } else {
375 quote! {}
376 };
377
378 input.block = syn::parse2(quote! {{
379 #arg_warnings
380 { #logging_prologue }
381 let __rt = #builder_call
382 .enable_all()
383 .build()
384 .expect("failed to build tokio runtime");
385 __rt.block_on(async move #user_body)
386 }})?;
387
388 Ok(quote! {
389 #input
390 })
391}
392
393enum Flavor {
394 MultiThread,
395 CurrentThread,
396}
397
398/// `#[tokio::main]` argument names, accepted without a warning.
399const TOKIO_MAIN_ARGS: &[&str] = &[
400 "flavor",
401 "worker_threads",
402 "start_paused",
403 "crate",
404 "unhandled_panic",
405];
406
407/// A warning per `#[rustango::main]` argument the parsers below ignore.
408fn main_arg_warnings(args: &TokenStream2) -> TokenStream2 {
409 use syn::parse::Parser as _;
410 let parsed = syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated
411 .parse2(args.clone());
412 let metas = match parsed {
413 Ok(m) => m,
414 Err(e) => {
415 return deprecation_warning(
416 e.span(),
417 &format!("#[rustango::main]: cannot parse the arguments ({e}); they are ignored"),
418 )
419 }
420 };
421 let lit_bool = |m: &syn::MetaNameValue| match &m.value {
422 syn::Expr::Lit(syn::ExprLit {
423 lit: syn::Lit::Bool(_),
424 ..
425 }) => true,
426 _ => false,
427 };
428 metas
429 .iter()
430 .filter_map(|meta| {
431 // `#[tokio::main]`'s own names stay silent: a `deny(warnings)`
432 // crate passing one must keep compiling in a patch release.
433 let tokio_arg = TOKIO_MAIN_ARGS.iter().any(|n| meta.path().is_ident(n));
434 let ok = match meta {
435 syn::Meta::NameValue(m) if m.path.is_ident("logging") => lit_bool(m),
436 _ => tokio_arg,
437 };
438 (!ok).then(|| {
439 deprecation_warning(
440 meta.span(),
441 "#[rustango::main]: unknown argument, ignored (supported: \
442 `flavor = \"current_thread\" | \"multi_thread\"`, `logging = false`)",
443 )
444 })
445 })
446 .collect()
447}
448
449/// A compile warning at `span`: a proc macro's only stable way to warn
450/// is a `#[deprecated]` item that its own output then uses.
451fn deprecation_warning(span: proc_macro2::Span, note: &str) -> TokenStream2 {
452 quote::quote_spanned! {span=>
453 const _: () = {
454 #[deprecated(note = #note)]
455 const RUSTANGO_WARNING: () = ();
456 RUSTANGO_WARNING
457 };
458 }
459}
460
461fn parse_flavor(args: &TokenStream2) -> Flavor {
462 // Cheap parser: look for the literal token sequence
463 // `flavor = "current_thread"`. Everything else (including
464 // bare `multi_thread` or no args) defaults to multi-thread.
465 let s = args.to_string();
466 if s.contains("current_thread") {
467 Flavor::CurrentThread
468 } else {
469 Flavor::MultiThread
470 }
471}
472
473/// `false` when the attribute carries `logging = false` — the caller
474/// installs their own subscriber, usually via `Cli::with_logging`
475/// (#1465). Same cheap token match as `parse_flavor`.
476fn parse_logging(args: &TokenStream2) -> bool {
477 !args.to_string().replace(' ', "").contains("logging=false")
478}
479
480/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
481struct QInput {
482 base_path: syn::Path,
483 field: syn::Ident,
484 value: syn::Expr,
485}
486
487impl syn::parse::Parse for QInput {
488 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
489 let base_path: syn::Path = input.parse()?;
490 input.parse::<syn::Token![.]>()?;
491 let field: syn::Ident = input.parse()?;
492 input.parse::<syn::Token![=]>()?;
493 let value: syn::Expr = input.parse()?;
494 Ok(QInput {
495 base_path,
496 field,
497 value,
498 })
499 }
500}
501
502fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
503 let q: QInput = syn::parse2(input)?;
504 let root = rustango_root();
505 let field_str = ident_name(&q.field);
506 let field_span = q.field.span();
507 let (base, suffix) = match field_str.find("__") {
508 Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
509 None => (field_str.as_str(), ""),
510 };
511 if base.is_empty() {
512 return Err(syn::Error::new(
513 field_span,
514 "Q!(): field name is empty before `__` suffix",
515 ));
516 }
517 let base_ident = ident_from_name(base, field_span);
518 let value = &q.value;
519 let path = &q.base_path;
520
521 // Most suffixes map directly to a Column method with the value
522 // forwarded unchanged. Some need value-shape massaging (wildcards
523 // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
524 // ISNULL). Unknown suffixes fail the build.
525 let expanded = match suffix {
526 "" | "exact" => quote! {
527 #root::core::Column::eq(#path::#base_ident, #value)
528 },
529 "ne" => quote! {
530 #root::core::Column::ne(#path::#base_ident, #value)
531 },
532 "gt" => quote! {
533 #root::core::Column::gt(#path::#base_ident, #value)
534 },
535 "gte" => quote! {
536 #root::core::Column::gte(#path::#base_ident, #value)
537 },
538 "lt" => quote! {
539 #root::core::Column::lt(#path::#base_ident, #value)
540 },
541 "lte" => quote! {
542 #root::core::Column::lte(#path::#base_ident, #value)
543 },
544 // The escaped-LIKE lookups delegate to the typed Column
545 // methods, which escape the value and pair it with the
546 // *Escaped ops so `%` / `_` match literally on every dialect
547 // (#1257) — the macro no longer owns any pattern wrapping.
548 "iexact" => quote! {
549 #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
550 },
551 "contains" => quote! {
552 #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
553 },
554 "icontains" => quote! {
555 #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
556 },
557 "startswith" => quote! {
558 #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
559 },
560 "istartswith" => quote! {
561 #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
562 },
563 "endswith" => quote! {
564 #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
565 },
566 "iendswith" => quote! {
567 #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
568 },
569 "in" => quote! {
570 #root::core::Column::is_in(#path::#base_ident, #value)
571 },
572 "not_in" => quote! {
573 #root::core::Column::not_in(#path::#base_ident, #value)
574 },
575 "isnull" => {
576 // Must be a bool literal at macro time so we can route to
577 // is_null vs is_not_null without a runtime branch.
578 let b = match value {
579 syn::Expr::Lit(syn::ExprLit {
580 lit: syn::Lit::Bool(b),
581 ..
582 }) => b.value(),
583 _ => {
584 return Err(syn::Error::new_spanned(
585 value,
586 "Q!(): `__isnull` requires a `true` or `false` literal",
587 ));
588 }
589 };
590 if b {
591 quote! { #root::core::Column::is_null(#path::#base_ident) }
592 } else {
593 quote! { #root::core::Column::is_not_null(#path::#base_ident) }
594 }
595 }
596 "between" => {
597 // Accept a tuple literal `(lo, hi)`.
598 let tuple = match value {
599 syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
600 _ => {
601 return Err(syn::Error::new_spanned(
602 value,
603 "Q!(): `__between` requires a tuple literal `(lo, hi)`",
604 ));
605 }
606 };
607 let lo = &tuple.elems[0];
608 let hi = &tuple.elems[1];
609 quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
610 }
611 "regex" => quote! {
612 #root::core::Column::regex(#path::#base_ident, #value)
613 },
614 "iregex" => quote! {
615 #root::core::Column::iregex(#path::#base_ident, #value)
616 },
617 _ => {
618 return Err(syn::Error::new(
619 field_span,
620 format!(
621 "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",
622 suffix
623 ),
624 ));
625 }
626 };
627 Ok(expanded)
628}
629
630fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
631 // Default to "./migrations" if invoked without args.
632 let path_str = if input.is_empty() {
633 "./migrations".to_string()
634 } else {
635 let lit: LitStr = syn::parse2(input)?;
636 lit.value()
637 };
638
639 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
640 syn::Error::new(
641 proc_macro2::Span::call_site(),
642 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
643 )
644 })?;
645 let abs = std::path::Path::new(&manifest).join(&path_str);
646
647 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
648 if abs.is_dir() {
649 let read = std::fs::read_dir(&abs).map_err(|e| {
650 syn::Error::new(
651 proc_macro2::Span::call_site(),
652 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
653 )
654 })?;
655 for entry in read.flatten() {
656 let path = entry.path();
657 if !path.is_file() {
658 continue;
659 }
660 if path.extension().and_then(|s| s.to_str()) != Some("json") {
661 continue;
662 }
663 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
664 continue;
665 };
666 entries.push((stem.to_owned(), path));
667 }
668 }
669 entries.sort_by(|a, b| a.0.cmp(&b.0));
670
671 // Compile-time chain validation: read each migration's JSON,
672 // pull `name` and `prev` (file-stem-keyed for the chain check),
673 // and verify every `prev` points to another migration in the
674 // slice. Mismatches between the file stem and the embedded
675 // `name` field — and broken `prev` chains — fail at MACRO
676 // EXPANSION time so a misshapen migration set never compiles.
677 //
678 // This is the v0.4 Slice 5 distinguisher: rustango's JSON
679 // migrations + a Rust proc-macro that reads them is a combo no
680 // other Rust web framework matches (Cot's are imperative Rust
681 // code, Loco's are SeaORM up/down, Rwf's are raw SQL — none have
682 // a static chain to validate).
683 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
684 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
685 for (stem, path) in &entries {
686 let raw = std::fs::read_to_string(path).map_err(|e| {
687 syn::Error::new(
688 proc_macro2::Span::call_site(),
689 format!(
690 "embed_migrations!: cannot read {} for chain validation: {e}",
691 path.display()
692 ),
693 )
694 })?;
695 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
696 syn::Error::new(
697 proc_macro2::Span::call_site(),
698 format!(
699 "embed_migrations!: {} is not valid JSON: {e}",
700 path.display()
701 ),
702 )
703 })?;
704 let name = json
705 .get("name")
706 .and_then(|v| v.as_str())
707 .ok_or_else(|| {
708 syn::Error::new(
709 proc_macro2::Span::call_site(),
710 format!(
711 "embed_migrations!: {} is missing the `name` field",
712 path.display()
713 ),
714 )
715 })?
716 .to_owned();
717 if name != *stem {
718 return Err(syn::Error::new(
719 proc_macro2::Span::call_site(),
720 format!(
721 "embed_migrations!: file stem `{stem}` does not match the migration's \
722 `name` field `{name}` — rename the file or fix the JSON",
723 ),
724 ));
725 }
726 let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
727 chain_names.push(name.clone());
728 prev_refs.push((name, prev));
729 }
730
731 let name_set: std::collections::HashSet<&str> =
732 chain_names.iter().map(String::as_str).collect();
733 for (name, prev) in &prev_refs {
734 if let Some(p) = prev {
735 if !name_set.contains(p.as_str()) {
736 return Err(syn::Error::new(
737 proc_macro2::Span::call_site(),
738 format!(
739 "embed_migrations!: broken migration chain — `{name}` declares \
740 prev=`{p}` but no migration with that name exists in {}",
741 abs.display()
742 ),
743 ));
744 }
745 }
746 }
747
748 let pairs: Vec<TokenStream2> = entries
749 .iter()
750 .map(|(name, path)| {
751 let path_lit = path.display().to_string();
752 quote! { (#name, ::core::include_str!(#path_lit)) }
753 })
754 .collect();
755
756 // A wrong path used to embed nothing silently; a deprecation use is
757 // the only stable way for a proc macro to warn without breaking builds.
758 let missing_warning = if abs.is_dir() {
759 quote! {}
760 } else {
761 let note = format!(
762 "embed_migrations!: directory {} does not exist; no migrations are embedded",
763 abs.display()
764 );
765 quote! {
766 #[deprecated(note = #note)]
767 const __RUSTANGO_MIGRATIONS_DIR_MISSING: () = ();
768 #[allow(clippy::let_unit_value)]
769 let _ = __RUSTANGO_MIGRATIONS_DIR_MISSING;
770 }
771 };
772
773 Ok(quote! {
774 {
775 #missing_warning
776 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
777 __RUSTANGO_EMBEDDED
778 }
779 })
780}
781
782fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
783 let root = rustango_root();
784 let struct_name = &input.ident;
785
786 let Data::Struct(data) = &input.data else {
787 return Err(syn::Error::new_spanned(
788 struct_name,
789 "Model can only be derived on structs",
790 ));
791 };
792 let Fields::Named(named) = &data.fields else {
793 return Err(syn::Error::new_spanned(
794 struct_name,
795 "Model requires a struct with named fields",
796 ));
797 };
798
799 let mut container = parse_container_attrs(input)?;
800 let table = container
801 .table
802 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
803 let model_name = struct_name.to_string();
804
805 let collected = collect_fields(named, &table)?;
806 // `reverse_has` / `generic_has` join on the PK column unless they name another.
807 if let Some((_, pk_column)) = &collected.primary_key {
808 for rel in &mut container.reverse_has_relations {
809 rel.self_pk_column.get_or_insert_with(|| pk_column.clone());
810 }
811 for rel in &mut container.generic_has_relations {
812 rel.self_pk_column.get_or_insert_with(|| pk_column.clone());
813 }
814 }
815
816 // Validate that #[rustango(display = "…")] names a real field.
817 if let Some((ref display, span)) = container.display {
818 if !collected.field_names.iter().any(|n| n == display) {
819 return Err(syn::Error::new(
820 span,
821 format!("`display = \"{display}\"` does not match any field on this struct"),
822 ));
823 }
824 }
825 let display = container.display.map(|(name, _)| name);
826 let app_label = container.app.clone();
827
828 // Validate admin field-name lists against declared field names.
829 // Note: `list_display` is intentionally NOT validated here. As of
830 // v0.32 it may also reference inventory-registered computed
831 // fields (via `register_admin_computed!`) whose existence the
832 // macro can't see at compile time — they're submitted from any
833 // crate that depends on rustango. The runtime list-view resolves
834 // unknown names against the inventory + silently drops the
835 // truly-bogus ones, which is the cheaper trade-off versus
836 // forcing a per-Model attr to opt out.
837 if let Some(admin) = &container.admin {
838 for (label, list) in [
839 ("search_fields", &admin.search_fields),
840 ("readonly_fields", &admin.readonly_fields),
841 ("list_filter", &admin.list_filter),
842 ] {
843 if let Some((names, span)) = list {
844 for name in names {
845 if !collected.field_names.iter().any(|n| n == name) {
846 return Err(syn::Error::new(
847 *span,
848 format!(
849 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
850 ),
851 ));
852 }
853 }
854 }
855 }
856 if let Some((pairs, span)) = &admin.ordering {
857 for (name, _) in pairs {
858 if !collected.field_names.iter().any(|n| n == name) {
859 return Err(syn::Error::new(
860 *span,
861 format!(
862 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
863 ),
864 ));
865 }
866 }
867 }
868 if let Some((groups, span)) = &admin.fieldsets {
869 for (_, fields) in groups {
870 for name in fields {
871 if !collected.field_names.iter().any(|n| n == name) {
872 return Err(syn::Error::new(
873 *span,
874 format!(
875 "`fieldsets`: \"{name}\" is not a declared field on this struct"
876 ),
877 ));
878 }
879 }
880 }
881 }
882 }
883 if let Some(audit) = &container.audit {
884 if let Some((names, span)) = &audit.track {
885 for name in names {
886 if !collected.field_names.iter().any(|n| n == name) {
887 return Err(syn::Error::new(
888 *span,
889 format!(
890 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
891 ),
892 ));
893 }
894 }
895 }
896 }
897
898 // Issue #291 / T2.5 — validate each `default_order` column name
899 // against the model's collected fields. Typos fail at macro-expand
900 // time, not at the database.
901 for (col, _desc, span) in &container.default_order {
902 if !collected.field_names.iter().any(|n| n == col) {
903 return Err(syn::Error::new(
904 *span,
905 format!(
906 "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
907 ),
908 ));
909 }
910 }
911
912 let column_warnings =
913 unknown_column_warnings(&container.indexes, &container.get_latest_by, &collected);
914
915 // Build the audit_track list for ModelSchema: None when no audit attr,
916 // Some(empty) when audit present without track, Some(names) when explicit.
917 let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
918 audit
919 .track
920 .as_ref()
921 .map(|(names, _)| names.clone())
922 .unwrap_or_default()
923 });
924
925 // The upsert conflict target: a container-level composite unique,
926 // never a field `index(unique)` or a partial index PG can't target.
927 let upsert_unique: Option<Vec<String>> = container
928 .indexes
929 .iter()
930 .find(|i| i.unique && i.where_clause.is_none() && !i.columns.is_empty())
931 .map(|i| i.columns.clone());
932
933 // Merge field-level indexes into the container's index list.
934 let mut all_indexes: Vec<IndexAttr> = container.indexes;
935 for field in &named.named {
936 let ident = field.ident.as_ref().expect("named");
937 // The same default column as `process_field` (#1937).
938 let col = ident_name(ident);
939 // Re-parse field attrs to check for index flag
940 if let Ok(fa) = parse_field_attrs(field) {
941 if fa.index {
942 let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
943 let auto_name = capped_index_name(if fa.index_unique {
944 format!("{table}_{col_name}_uq_idx")
945 } else {
946 format!("{table}_{col_name}_idx")
947 });
948 all_indexes.push(IndexAttr {
949 span: field.span(),
950 name: fa.index_name.or(Some(auto_name)),
951 columns: vec![col_name],
952 unique: fa.index_unique,
953 method: fa.index_method,
954 where_clause: None,
955 include: Vec::new(),
956 });
957 }
958 }
959 }
960 // An unbounded String is LONGTEXT on MySQL, which no key can hold (1170, #2575).
961 for field in &named.named {
962 let (Ok(fa), Ok(det)) = (parse_field_attrs(field), detect_type(&field.ty)) else {
963 continue;
964 };
965 if det.kind != DetectedKind::String || fa.max_length.is_some() {
966 continue;
967 }
968 let col = fa
969 .column
970 .clone()
971 .unwrap_or_else(|| ident_name(field.ident.as_ref().expect("named")));
972 // MySQL indexes every foreign-key column too.
973 let relation = det.fk_inner.is_some() || fa.fk.is_some() || fa.o2o.is_some();
974 if fa.primary_key
975 || fa.unique
976 || relation
977 || all_indexes.iter().any(|i| i.columns.contains(&col))
978 {
979 return Err(syn::Error::new_spanned(
980 field,
981 "a primary-key, unique, indexed or foreign-key `String` needs \
982 `max_length`: MySQL cannot index an unbounded (LONGTEXT) column",
983 ));
984 }
985 }
986
987 let model_impl = model_impl_tokens(
988 struct_name,
989 &model_name,
990 &table,
991 display.as_deref(),
992 app_label.as_deref(),
993 container.admin.as_ref(),
994 &container.default_order,
995 &collected.field_schemas,
996 collected.soft_delete_column.as_deref(),
997 container.permissions,
998 audit_track_names.as_deref(),
999 &container.m2m,
1000 &all_indexes,
1001 &container.checks,
1002 &container.excludes,
1003 &container.composite_fks,
1004 &container.generic_fks,
1005 container.scope.as_deref(),
1006 container.is_view,
1007 container.verbose_name.as_deref(),
1008 container.verbose_name_plural.as_deref(),
1009 container.managed,
1010 container.base_manager_name.as_deref(),
1011 container.order_with_respect_to.as_deref(),
1012 container.proxy,
1013 &container.required_db_features,
1014 container.required_db_vendor.as_deref(),
1015 container.default_related_name.as_deref(),
1016 container.db_table_comment.as_deref(),
1017 container
1018 .get_latest_by
1019 .as_ref()
1020 .map(|(c, d, _)| (c.as_str(), *d)),
1021 &container.extra_permissions,
1022 &container.default_permissions,
1023 &container.global_scopes,
1024 &container.reverse_has_relations,
1025 &container.generic_has_relations,
1026 );
1027 let module_ident = column_module_ident(struct_name);
1028 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
1029 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
1030 let track_set: Option<std::collections::HashSet<&str>> = audit
1031 .track
1032 .as_ref()
1033 .map(|(names, _)| names.iter().map(String::as_str).collect());
1034 collected
1035 .column_entries
1036 .iter()
1037 .filter(|c| {
1038 track_set
1039 .as_ref()
1040 .map_or(true, |s| s.contains(c.name.as_str()))
1041 })
1042 .collect()
1043 });
1044 let inherent_impl = inherent_impl_tokens(
1045 struct_name,
1046 &collected,
1047 collected.primary_key.as_ref(),
1048 &column_consts,
1049 audited_fields.as_deref(),
1050 upsert_unique.as_deref(),
1051 &container.manager_fns,
1052 );
1053 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
1054 let from_row_impl = from_row_impl_tokens(
1055 struct_name,
1056 &collected.from_row_inits,
1057 &collected.from_row_inits_my,
1058 );
1059 let reverse_helpers = reverse_helper_tokens(
1060 struct_name,
1061 &collected.fk_relations,
1062 container.default_related_name.as_deref(),
1063 )?;
1064 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
1065 let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
1066 // Issue #817 — `#[rustango(through(...))]` accessors.
1067 let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
1068 // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
1069 let reverse_has_accessors = reverse_has_accessor_tokens(
1070 struct_name,
1071 &container.reverse_has_relations,
1072 &collected.column_entries,
1073 );
1074 let generic_fk_accessors = generic_fk_accessor_tokens(
1075 struct_name,
1076 &container.generic_fks,
1077 &collected.column_entries,
1078 );
1079
1080 Ok(quote! {
1081 #column_warnings
1082 #model_impl
1083 #inherent_impl
1084 #from_row_impl
1085 #column_module
1086 #reverse_helpers
1087 #m2m_accessors
1088 #generic_m2m_accessors
1089 #through_accessors
1090 #reverse_has_accessors
1091 #generic_fk_accessors
1092
1093 #root::core::inventory::submit! {
1094 // `module_path!()` evaluates at the registration site,
1095 // so a Model declared in `crate::blog::models` records
1096 // `"<crate>::blog::models"` and `resolved_app_label()`
1097 // can infer "blog" without an explicit attribute.
1098 #root::core::ModelEntry::new(
1099 <#struct_name as #root::core::Model>::SCHEMA,
1100 ::core::module_path!(),
1101 )
1102 .with_audited(
1103 <#struct_name as #root::core::Model>::__rustango_audited_update,
1104 <#struct_name as #root::core::Model>::__rustango_audited_delete,
1105 )
1106 .with_audited_create(<#struct_name as #root::core::Model>::__rustango_audited_create)
1107 .with_audited_update_record(
1108 <#struct_name as #root::core::Model>::__rustango_audited_update_record,
1109 )
1110 .with_audited_cascade(<#struct_name as #root::core::Model>::__rustango_audited_cascade)
1111 }
1112 })
1113}
1114
1115/// A deprecation-style warning per index or `get_latest_by` name that is
1116/// no column of the model: an error would break code that compiles today.
1117fn unknown_column_warnings(
1118 indexes: &[IndexAttr],
1119 get_latest_by: &Option<(String, bool, proc_macro2::Span)>,
1120 collected: &CollectedFields,
1121) -> TokenStream2 {
1122 let is_column = |c: &str| collected.column_entries.iter().any(|e| e.column == c);
1123 // Only plain names: an expression index such as `lower(email)` is legal.
1124 let plain = |c: &str| c.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_');
1125 let mut found: Vec<(proc_macro2::Span, String)> = Vec::new();
1126 for idx in indexes {
1127 for col in idx.columns.iter().filter(|c| plain(c) && !is_column(c)) {
1128 found.push((
1129 idx.span,
1130 format!("index column `{col}` is not a column of this model; its DDL will fail"),
1131 ));
1132 }
1133 }
1134 if let Some((col, _, span)) = get_latest_by {
1135 if !is_column(col) && !collected.field_names.iter().any(|n| n == col) {
1136 found.push((
1137 *span,
1138 format!("`get_latest_by`: `{col}` is not a field of this model"),
1139 ));
1140 }
1141 }
1142 found
1143 .into_iter()
1144 .map(|(span, note)| deprecation_warning(span, ¬e))
1145 .collect()
1146}
1147
1148/// Guard before decoding a `select_related` target (#2293): a NULL nullable
1149/// FK leaves the relation unloaded; a set FK whose row is missing is an error.
1150fn join_guard_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1151 let root = rustango_root();
1152 let parent_ty = &rel.parent_type;
1153 let field_ident = &rel.field_ident;
1154 let null_fk = if rel.nullable {
1155 quote! {
1156 if #receiver.#field_ident.is_none() {
1157 return ::core::result::Result::Ok(false);
1158 }
1159 }
1160 } else {
1161 quote! {}
1162 };
1163 quote! {
1164 #null_fk
1165 #root::sql::__rustango_require_join(
1166 row, <#parent_ty as #root::core::Model>::SCHEMA, alias,
1167 )?;
1168 }
1169}
1170
1171/// A target an earlier chain already loaded (`a__b` before `a__c`) is
1172/// reused, and the deeper chain stitched into it; re-decoding it would
1173/// drop what that chain loaded.
1174fn loaded_reuse_tokens(
1175 rel: &FkRelation,
1176 receiver: &TokenStream2,
1177 load: &TokenStream2,
1178) -> TokenStream2 {
1179 let root = rustango_root();
1180 let field_ident = &rel.field_ident;
1181 let pattern = if rel.nullable {
1182 quote!(::core::option::Option::Some(#root::sql::ForeignKey::Loaded { value: __loaded, .. }))
1183 } else {
1184 quote!(#root::sql::ForeignKey::Loaded { value: __loaded, .. })
1185 };
1186 quote! {
1187 if let #pattern = &mut #receiver.#field_ident {
1188 if let ::core::option::Option::Some(__r) = __rest {
1189 let _ = #load(&mut **__loaded, row, __r, &__next_alias)?;
1190 }
1191 return ::core::result::Result::Ok(true);
1192 }
1193 }
1194}
1195
1196/// Store the decoded `_parent` on the FK field, keeping the key the row
1197/// holds: with `on = <unique column>` it is not the parent's PK.
1198fn loaded_assign_tokens(rel: &FkRelation, receiver: &TokenStream2) -> TokenStream2 {
1199 let root = rustango_root();
1200 let field_ident = &rel.field_ident;
1201 if rel.nullable {
1202 quote! {
1203 if let ::core::option::Option::Some(__fk) = &mut #receiver.#field_ident {
1204 let _pk = ::core::clone::Clone::clone(__fk.pk_ref());
1205 *__fk = #root::sql::ForeignKey::loaded(_pk, _parent);
1206 }
1207 }
1208 } else {
1209 quote! {
1210 let _pk = ::core::clone::Clone::clone(#receiver.#field_ident.pk_ref());
1211 #receiver.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1212 }
1213 }
1214}
1215
1216/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
1217/// matches `field_name` against the model's FK fields and, for a
1218/// match, decodes the FK target via the parent's macro-generated
1219/// `__rustango_from_aliased_row`, and stores `ForeignKey::Loaded` on
1220/// `self` under the key the row already holds.
1221///
1222/// Always emitted (with empty arms for FK-less models, which
1223/// return `Ok(false)` for any field name) so the `T: LoadRelated`
1224/// trait bound on `fetch_on` is universally satisfied — users
1225/// never have to think about implementing it.
1226fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1227 let root = rustango_root();
1228 let arms = fk_relations.iter().map(|rel| {
1229 let parent_ty = &rel.parent_type;
1230 let field_ident = &rel.field_ident;
1231 let fk_name = ident_name(field_ident);
1232 let fk_col = fk_name.as_str();
1233 let guard = join_guard_tokens(rel, "e!(self));
1234 let reuse = loaded_reuse_tokens(
1235 rel,
1236 "e!(self),
1237 "e!(#root::sql::LoadRelated::__rustango_load_related),
1238 );
1239 let assign = loaded_assign_tokens(rel, "e!(self));
1240 quote! {
1241 #fk_col => {
1242 #guard
1243 #reuse
1244 let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
1245 // Audit #451 — multi-hop `select_related("a__b__c")`:
1246 // stitch the deeper relation onto this parent first,
1247 // decoding it at the accumulated `__next_alias`. The
1248 // parent type also impls `LoadRelated`, so this recurses
1249 // the FK chain to arbitrary depth.
1250 if let ::core::option::Option::Some(__r) = __rest {
1251 let _ = #root::sql::LoadRelated::__rustango_load_related(
1252 &mut _parent, row, __r, &__next_alias,
1253 )?;
1254 }
1255 #assign
1256 ::core::result::Result::Ok(true)
1257 }
1258 }
1259 });
1260 quote! {
1261 #[cfg(feature = "postgres")]
1262 impl #root::sql::LoadRelated for #struct_name {
1263 #[allow(unused_variables)]
1264 fn __rustango_load_related(
1265 &mut self,
1266 row: &#root::sql::sqlx::postgres::PgRow,
1267 field_name: &str,
1268 alias: &str,
1269 ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1270 // Audit #451 — split the multi-hop path: `base` is the FK
1271 // on THIS model, `__rest` (if any) is the remaining chain
1272 // to stitch onto the loaded parent. `__next_alias` is the
1273 // accumulated join alias the parent's columns live under
1274 // (`{alias}__{next-hop}`), matching `lower_select_related`.
1275 let (__base, __rest): (&str, ::core::option::Option<&str>) =
1276 match field_name.split_once("__") {
1277 ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1278 ::core::option::Option::None => (field_name, ::core::option::Option::None),
1279 };
1280 let __next_alias: ::std::string::String = match __rest {
1281 ::core::option::Option::Some(__r) => {
1282 let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1283 ::std::format!("{}__{}", alias, __rb)
1284 }
1285 ::core::option::Option::None => ::std::string::String::new(),
1286 };
1287 match __base {
1288 #( #arms )*
1289 _ => ::core::result::Result::Ok(false),
1290 }
1291 }
1292 }
1293 }
1294}
1295
1296/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1297/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1298/// which expands to a `LoadRelatedMy` impl when rustango is built with
1299/// the `mysql` feature, and to nothing otherwise. The decoded parent
1300/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1301/// decoder, also batch8) so the dual emission is symmetric across
1302/// backends.
1303fn load_related_impl_my_tokens(
1304 struct_name: &syn::Ident,
1305 fk_relations: &[FkRelation],
1306) -> TokenStream2 {
1307 let root = rustango_root();
1308 let arms = fk_relations.iter().map(|rel| {
1309 let parent_ty = &rel.parent_type;
1310 let field_ident = &rel.field_ident;
1311 let fk_name = ident_name(field_ident);
1312 let fk_col = fk_name.as_str();
1313 let guard = join_guard_tokens(rel, "e!(__self));
1314 let reuse = loaded_reuse_tokens(
1315 rel,
1316 "e!(__self),
1317 "e!(#root::sql::LoadRelatedMy::__rustango_load_related_my),
1318 );
1319 let assign = loaded_assign_tokens(rel, "e!(__self));
1320 // `self` IS hygiene-tracked through macro_rules — emitted from
1321 // a different context than the `&mut self` parameter inside
1322 // the macro_rules-expanded fn. Pass it through as `__self`
1323 // and let the macro_rules rebind it to the receiver.
1324 quote! {
1325 #fk_col => {
1326 #guard
1327 #reuse
1328 let mut _parent: #parent_ty =
1329 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1330 // Audit #451 — multi-hop: stitch the deeper relation onto
1331 // the parent at the accumulated alias (see PG twin).
1332 if let ::core::option::Option::Some(__r) = __rest {
1333 let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1334 &mut _parent, row, __r, &__next_alias,
1335 )?;
1336 }
1337 #assign
1338 ::core::result::Result::Ok(true)
1339 }
1340 }
1341 });
1342 quote! {
1343 #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1344 #( #arms )*
1345 });
1346 }
1347}
1348
1349/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1350/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1351/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1352fn load_related_impl_sqlite_tokens(
1353 struct_name: &syn::Ident,
1354 fk_relations: &[FkRelation],
1355) -> TokenStream2 {
1356 let root = rustango_root();
1357 let arms = fk_relations.iter().map(|rel| {
1358 let parent_ty = &rel.parent_type;
1359 let field_ident = &rel.field_ident;
1360 let fk_name = ident_name(field_ident);
1361 let fk_col = fk_name.as_str();
1362 let guard = join_guard_tokens(rel, "e!(__self));
1363 let reuse = loaded_reuse_tokens(
1364 rel,
1365 "e!(__self),
1366 "e!(#root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite),
1367 );
1368 let assign = loaded_assign_tokens(rel, "e!(__self));
1369 quote! {
1370 #fk_col => {
1371 #guard
1372 #reuse
1373 let mut _parent: #parent_ty =
1374 <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1375 // Audit #451 — multi-hop: stitch the deeper relation onto
1376 // the parent at the accumulated alias (see PG twin).
1377 if let ::core::option::Option::Some(__r) = __rest {
1378 let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1379 &mut _parent, row, __r, &__next_alias,
1380 )?;
1381 }
1382 #assign
1383 ::core::result::Result::Ok(true)
1384 }
1385 }
1386 });
1387 quote! {
1388 #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1389 #( #arms )*
1390 });
1391 }
1392}
1393
1394/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1395/// matches `field_name` against the model's FK fields and returns
1396/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1397/// group children by parent PK.
1398///
1399/// Always emitted (with `_ => None` for FK-less models) so the
1400/// trait bound on `fetch_with_prefetch` is universally satisfied.
1401fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1402 let root = rustango_root();
1403 let arms = fk_relations.iter().map(|rel| {
1404 let field_ident = &rel.field_ident;
1405 let fk_name = ident_name(field_ident);
1406 let fk_col = fk_name.as_str();
1407 if rel.pk_kind == DetectedKind::I64 {
1408 // i64 FK — return the stored PK so prefetch_related can
1409 // group children by it. Nullable variant unwraps via
1410 // `as_ref().map(...)`: an unset (NULL) FK column yields
1411 // `None` and that child sits out of the grouping (correct
1412 // semantics — it has no parent to attach to).
1413 if rel.nullable {
1414 quote! {
1415 #fk_col => self.#field_ident
1416 .as_ref()
1417 .map(|fk| #root::sql::ForeignKey::pk(fk)),
1418 }
1419 } else {
1420 quote! {
1421 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1422 }
1423 }
1424 } else {
1425 // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1426 // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1427 // i64-keyed grouping path — the trait signature is
1428 // `Option<i64>` and a non-i64 PK can't lower into it.
1429 // The FK still works for everything else (CRUD, lazy
1430 // load via `.get()`, select_related JOINs); only the
1431 // bulk prefetch grouper needs the integer key.
1432 quote! {
1433 #fk_col => ::core::option::Option::None,
1434 }
1435 }
1436 });
1437 // PK-type-agnostic version: every FK arm emits an
1438 // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1439 // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1440 // opt OUT of the legacy i64 method (it returns None) but opt IN
1441 // here.
1442 let value_arms = fk_relations.iter().map(|rel| {
1443 let field_ident = &rel.field_ident;
1444 let fk_name = ident_name(field_ident);
1445 let fk_col = fk_name.as_str();
1446 if rel.nullable {
1447 quote! {
1448 #fk_col => self.#field_ident
1449 .as_ref()
1450 .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1451 #root::sql::ForeignKey::pk(fk)
1452 )),
1453 }
1454 } else {
1455 quote! {
1456 #fk_col => ::core::option::Option::Some(
1457 ::core::convert::Into::<#root::core::SqlValue>::into(
1458 self.#field_ident.pk()
1459 )
1460 ),
1461 }
1462 }
1463 });
1464 quote! {
1465 impl #root::sql::FkPkAccess for #struct_name {
1466 #[allow(unused_variables)]
1467 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1468 match field_name {
1469 #( #arms )*
1470 _ => ::core::option::Option::None,
1471 }
1472 }
1473 #[allow(unused_variables)]
1474 fn __rustango_fk_pk_value(
1475 &self,
1476 field_name: &str,
1477 ) -> ::core::option::Option<#root::core::SqlValue> {
1478 match field_name {
1479 #( #value_arms )*
1480 _ => ::core::option::Option::None,
1481 }
1482 }
1483 }
1484 }
1485}
1486
1487/// `related_name = "+"`: the FK gets no reverse accessor on its parent.
1488const NO_REVERSE_ACCESSOR: &str = "+";
1489
1490/// For every `ForeignKey<Parent>` field on `Child`, emit
1491/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1492/// Reads the column the FK points at (`on`, else the PK) and runs one
1493/// `SELECT … WHERE <fk_column> = $1`: one round trip, no N+1.
1494///
1495/// **PG-only emission**: the accessor is bounded on
1496/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1497/// both of which are gated behind the `postgres` cargo feature. The
1498/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1499/// model derive itself compiles on tri-dialect / sqlite-only
1500/// downstream builds — the accessor just isn't materialised. A tri-
1501/// dialect `_set_pool` variant is a separate follow-up.
1502fn reverse_helper_tokens(
1503 child_ident: &syn::Ident,
1504 fk_relations: &[FkRelation],
1505 default_related_name: Option<&str>,
1506) -> syn::Result<TokenStream2> {
1507 let root = rustango_root();
1508 // `related_name = "+"`: no accessor, so a parent from another crate works.
1509 let fk_relations: Vec<&FkRelation> = fk_relations
1510 .iter()
1511 .filter(|rel| rel.related_name.as_deref() != Some(NO_REVERSE_ACCESSOR))
1512 .collect();
1513 if fk_relations.is_empty() {
1514 return Ok(TokenStream2::new());
1515 }
1516 // Method-name resolution per FK (issue #816 + follow-up):
1517 // 1. Field-level `#[rustango(related_name = "...")]` on the FK
1518 // itself — wins over everything else.
1519 // 2. Container-level `default_related_name = "..."` on the
1520 // child. Applies to every FK on this model that didn't
1521 // override.
1522 // 3. Fallback: `<child_snake>_set`. `Post` → `post_set`,
1523 // `BlogComment` → `blog_comment_set`. Avoids English-plural
1524 // edge cases.
1525 //
1526 // The PG-on-executor variant keeps the resolved name; the
1527 // tri-dialect `_pool` variant appends `_pool` to it (matches the
1528 // framework's convention for the `&Pool` flavor of every helper).
1529 let default_pg_suffix = default_related_name
1530 .map(str::to_owned)
1531 .unwrap_or_else(|| format!("{}_set", to_snake_case(&ident_name(child_ident))));
1532 let name_of = |rel: &FkRelation| {
1533 rel.related_name
1534 .clone()
1535 .unwrap_or_else(|| default_pg_suffix.clone())
1536 };
1537 // Two FKs to one parent would define the same method twice (E0592).
1538 for (i, rel) in fk_relations.iter().enumerate() {
1539 let parent_ty = &rel.parent_type;
1540 let parent = quote!(#parent_ty).to_string();
1541 let name = name_of(rel);
1542 if let Some(first) = fk_relations[..i].iter().find(|r| {
1543 let other = &r.parent_type;
1544 quote!(#other).to_string() == parent && name_of(r) == name
1545 }) {
1546 return Err(syn::Error::new_spanned(
1547 &rel.field_ident,
1548 format!(
1549 "`{}` and `{}` both point at `{parent}` and would both add `{parent}::{name}`; \
1550 give one `#[rustango(related_name = \"...\")]`, or `related_name = \"+\"` for none",
1551 first.field_ident, rel.field_ident
1552 ),
1553 ));
1554 }
1555 }
1556 let impls = fk_relations.iter().map(|rel| {
1557 let pg_suffix = name_of(rel);
1558 let pool_suffix = format!("{}_pool", pg_suffix);
1559 let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1560 let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1561 let parent_ty = &rel.parent_type;
1562 let fk_name = ident_name(&rel.field_ident);
1563 let fk_col = fk_name.as_str();
1564 let doc = format!(
1565 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1566 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1567 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1568 further `{child_ident}::objects()` filters via direct queryset use."
1569 );
1570 let pool_doc = format!(
1571 "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1572 [`#root::sql::Pool`] and dispatches per backend so the \
1573 reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1574 Use this from framework code that holds a `&Pool` (admin, \
1575 tenancy resolver, viewset handlers); reach for the executor- \
1576 bound variant when you already have a typed `sqlx::Executor`."
1577 );
1578 quote! {
1579 #[cfg(feature = "postgres")]
1580 impl #parent_ty {
1581 #[doc = #doc]
1582 ///
1583 /// # Errors
1584 /// Returns [`#root::sql::ExecError`] for SQL-writing
1585 /// or driver failures.
1586 pub async fn #pg_method_ident<'_c, _E>(
1587 &self,
1588 _executor: _E,
1589 ) -> ::core::result::Result<
1590 ::std::vec::Vec<#child_ident>,
1591 #root::sql::ExecError,
1592 >
1593 where
1594 _E: #root::sql::sqlx::Executor<
1595 '_c,
1596 Database = #root::sql::sqlx::Postgres,
1597 >,
1598 {
1599 let _key = #root::sql::__reverse_fk_key::<Self, #child_ident>(self, #fk_col)?;
1600 #root::query::QuerySet::<#child_ident>::new()
1601 .filter_op(#fk_col, #root::core::Op::Eq, _key)
1602 .fetch_on(_executor)
1603 .await
1604 }
1605 }
1606
1607 impl #parent_ty {
1608 #[doc = #pool_doc]
1609 ///
1610 /// # Errors
1611 /// Returns [`#root::sql::ExecError`] for SQL-writing
1612 /// or driver failures.
1613 pub async fn #pool_method_ident(
1614 &self,
1615 pool: &#root::sql::Pool,
1616 ) -> ::core::result::Result<
1617 ::std::vec::Vec<#child_ident>,
1618 #root::sql::ExecError,
1619 > {
1620 use #root::sql::FetcherPool as _;
1621 let _key = #root::sql::__reverse_fk_key::<Self, #child_ident>(self, #fk_col)?;
1622 #root::query::QuerySet::<#child_ident>::new()
1623 .filter_op(#fk_col, #root::core::Op::Eq, _key)
1624 .fetch(pool)
1625 .await
1626 }
1627 }
1628 }
1629 });
1630 Ok(quote! { #( #impls )* })
1631}
1632
1633/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1634/// relation declared on the model.
1635/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1636/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1637///
1638/// Closes #239 + #240 — `comment.content_object` /
1639/// `comment.content_object = post` ergonomics on top of the existing
1640/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1641///
1642/// `column_entries` is passed so we can resolve each `ct_column` /
1643/// `pk_column` SQL name back to its Rust field ident — the macro
1644/// only sees the column-side strings in the attribute, but the
1645/// emitted accessor needs to read the actual struct field.
1646fn generic_fk_accessor_tokens(
1647 struct_name: &syn::Ident,
1648 generic_fks: &[GenericFkAttr],
1649 column_entries: &[ColumnEntry],
1650) -> TokenStream2 {
1651 let root = rustango_root();
1652 if generic_fks.is_empty() {
1653 return TokenStream2::new();
1654 }
1655 let methods = generic_fks.iter().filter_map(|gfk| {
1656 // Resolve `ct_column` + `pk_column` to the struct's Rust
1657 // field idents. A typo (column name doesn't match any field)
1658 // emits no method for that registration — the user will see
1659 // the compiler reject the SCHEMA literal anyway, so there's
1660 // a clear error path without us double-reporting.
1661 let ct_ident = column_entries
1662 .iter()
1663 .find(|c| c.column == gfk.ct_column)
1664 .map(|c| c.ident.clone())?;
1665 let pk_ident = column_entries
1666 .iter()
1667 .find(|c| c.column == gfk.pk_column)
1668 .map(|c| c.ident.clone())?;
1669
1670 let accessor_ident =
1671 syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1672 let setter_ident =
1673 syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1674 let name_literal = gfk.name.as_str();
1675
1676 Some(quote! {
1677 #[doc = concat!(
1678 "Resolve the polymorphic `",
1679 #name_literal,
1680 "` relation. Reads `self.",
1681 stringify!(#ct_ident),
1682 "` + `self.",
1683 stringify!(#pk_ident),
1684 "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1685 "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1686 #name_literal,
1687 "\", ...))]`."
1688 )]
1689 pub async fn #accessor_ident(
1690 &self,
1691 pool: &#root::sql::Pool,
1692 ) -> ::core::result::Result<
1693 ::core::option::Option<#root::__serde_json::Value>,
1694 #root::sql::ExecError,
1695 > {
1696 let gfk = #root::contenttypes::GenericForeignKey::new(
1697 self.#ct_ident as i64,
1698 self.#pk_ident as i64,
1699 );
1700 gfk.get_object(pool).await
1701 }
1702
1703 #[doc = concat!(
1704 "Set the polymorphic `",
1705 #name_literal,
1706 "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1707 stringify!(#ct_ident),
1708 "` and `self.",
1709 stringify!(#pk_ident),
1710 "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1711 #name_literal,
1712 "\", ...))]`."
1713 )]
1714 pub async fn #setter_ident<T: #root::core::Model>(
1715 &mut self,
1716 pool: &#root::sql::Pool,
1717 target_pk: i64,
1718 ) -> ::core::result::Result<(), #root::sql::ExecError> {
1719 let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1720 pool,
1721 target_pk,
1722 ).await?;
1723 self.#ct_ident = gfk.content_type_id as _;
1724 self.#pk_ident = gfk.object_pk as _;
1725 ::core::result::Result::Ok(())
1726 }
1727 })
1728 });
1729 quote! {
1730 impl #struct_name {
1731 #( #methods )*
1732 }
1733 }
1734}
1735
1736fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1737 let root = rustango_root();
1738 if m2m_relations.is_empty() {
1739 return TokenStream2::new();
1740 }
1741 let methods = m2m_relations.iter().map(|rel| {
1742 let method_name = format!("{}_m2m", rel.name);
1743 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1744 let through = rel.through.as_str();
1745 let src_col = rel.src.as_str();
1746 let dst_col = rel.dst.as_str();
1747 quote! {
1748 pub fn #method_ident(&self) -> #root::sql::M2MManager {
1749 #root::sql::M2MManager {
1750 src_pk: self.__rustango_pk_value(),
1751 through: #through,
1752 src_col: #src_col,
1753 dst_col: #dst_col,
1754 }
1755 }
1756 }
1757 });
1758 quote! {
1759 impl #struct_name {
1760 #( #methods )*
1761 }
1762 }
1763}
1764
1765/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1766/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1767fn generic_m2m_accessor_tokens(
1768 struct_name: &syn::Ident,
1769 relations: &[GenericM2MAttr],
1770) -> TokenStream2 {
1771 let root = rustango_root();
1772 if relations.is_empty() {
1773 return TokenStream2::new();
1774 }
1775 let methods = relations.iter().map(|rel| {
1776 let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1777 let through = rel.through.as_str();
1778 let pk_col = rel.pk_column.as_str();
1779 let ct_col = rel.ct_column.as_str();
1780 let related_col = rel.related_column.as_str();
1781 quote! {
1782 pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1783 #root::sql::GenericM2MManager {
1784 src_pk: self.__rustango_pk_value(),
1785 src_schema: <Self as #root::core::Model>::SCHEMA,
1786 through: #through,
1787 pk_col: #pk_col,
1788 ct_col: #ct_col,
1789 dst_col: #related_col,
1790 }
1791 }
1792 }
1793 });
1794 // Lets an owner delete find and clear its junction rows.
1795 let entries = relations.iter().map(|rel| {
1796 let through = rel.through.as_str();
1797 let pk_col = rel.pk_column.as_str();
1798 let ct_col = rel.ct_column.as_str();
1799 quote! {
1800 #root::core::inventory::submit! {
1801 #root::core::GenericM2MEntry::new(
1802 <#struct_name as #root::core::Model>::SCHEMA,
1803 #through,
1804 #pk_col,
1805 #ct_col,
1806 )
1807 }
1808 }
1809 });
1810 quote! {
1811 impl #struct_name {
1812 #( #methods )*
1813 }
1814 #( #entries )*
1815 }
1816}
1817
1818/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1819/// associated functions for each `#[rustango(reverse_has(...))]`
1820/// attribute. Issue #830.
1821///
1822/// The two emitted functions return ready-to-use `WhereExpr` nodes
1823/// that downstream callers drop into
1824/// `QuerySet::<Self>::where_raw(...)`:
1825///
1826/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1827/// <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1828/// `whereHas` parity (without the closure-style sub-predicate
1829/// refinement — that's a follow-up; users can layer additional
1830/// predicates by constructing the SelectQuery themselves and
1831/// calling `where_raw(exists(query))` from `crate::core::subquery`).
1832/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1833/// `whereDoesntHave` parity.
1834///
1835/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1836/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1837/// machinery threads the outer-table reference through automatically
1838/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1839fn reverse_has_accessor_tokens(
1840 struct_name: &syn::Ident,
1841 reverse_has_relations: &[ReverseHasAttr],
1842 columns: &[ColumnEntry],
1843) -> TokenStream2 {
1844 let root = rustango_root();
1845 if reverse_has_relations.is_empty() {
1846 return TokenStream2::new();
1847 }
1848 let methods = reverse_has_relations.iter().map(|rel| {
1849 let exists_name = format!("{}_exists_expr", rel.name);
1850 let not_exists_name = format!("{}_not_exists_expr", rel.name);
1851 let count_name = format!("{}_count", rel.name);
1852 let fetch_name = format!("{}_fetch", rel.name);
1853 let first_name = format!("{}_first", rel.name);
1854 let pluck_name = format!("{}_pluck", rel.name);
1855 let accessor_name = rel.name.as_str();
1856 let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1857 let not_exists_ident = syn::Ident::new(¬_exists_name, struct_name.span());
1858 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1859 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1860 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1861 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1862 let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1863 let child = &rel.child;
1864 let child_fk_column = rel.child_fk_column.as_str();
1865 let self_pk_column = rel.self_pk_column();
1866 // The instance helpers filter on the value the EXISTS form joins on.
1867 let self_value = columns
1868 .iter()
1869 .find(|c| c.column == self_pk_column)
1870 .map_or_else(
1871 || quote! { self.__rustango_pk_value() },
1872 |c| {
1873 let ident = &c.ident;
1874 quote! {
1875 ::core::convert::Into::<#root::core::SqlValue>::into(
1876 ::core::clone::Clone::clone(&self.#ident)
1877 )
1878 }
1879 },
1880 );
1881 let exists_doc = format!(
1882 "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1883 FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1884 Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1885 filter to {struct_name}s with at least one matching child.",
1886 );
1887 let not_exists_doc = format!(
1888 "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1889 (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1890 <outer>.{self_pk_column})`. Drop into \
1891 `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1892 {struct_name}s with **no** matching child.",
1893 );
1894 let count_doc = format!(
1895 "Eloquent `$model->{name}->count()` analog — returns \
1896 the number of `{child}` rows whose `{child_fk_column}` \
1897 matches this `{struct_name}` instance's `{self_pk_column}`. \
1898 Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1899 {child_fk_column} = <self.{self_pk_column}>`.",
1900 name = rel.name,
1901 );
1902 let accessor_doc = format!(
1903 "Eloquent `$model->{name}` accessor — returns a \
1904 `QuerySet<{child}>` filtered to rows whose \
1905 `{child_fk_column}` matches this `{struct_name}` \
1906 instance's `{self_pk_column}`. **Chainable**: compose `.filter()` \
1907 / `.order_by()` / `.limit()` etc. on top, then call \
1908 `.fetch(&pool)` (the QuerySet trait method) when \
1909 done. For the simple \"fetch all\" hot path with no \
1910 further composition, prefer the bare-name \
1911 `{name}_fetch(&pool)` companion.",
1912 name = rel.name,
1913 );
1914 let fetch_doc = format!(
1915 "Eloquent `$model->{name}->get()` — bare-name hot-path \
1916 over `{name}(&self).fetch(&pool)`. Use this when \
1917 you don't need further `.filter()` / `.order_by()` \
1918 composition; falls back to the chainable accessor when \
1919 you do. Avoids the `_pool` suffix on the most common \
1920 call-site shape.",
1921 name = rel.name,
1922 );
1923 quote! {
1924 #[doc = #accessor_doc]
1925 pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1926 #root::query::QuerySet::<#child>::new()
1927 .filter(#child_fk_column, #self_value)
1928 }
1929
1930 #[doc = #fetch_doc]
1931 pub async fn #fetch_ident(
1932 &self,
1933 pool: &#root::sql::Pool,
1934 ) -> ::core::result::Result<
1935 ::std::vec::Vec<#child>,
1936 #root::sql::ExecError,
1937 > {
1938 use #root::sql::FetcherPool as _;
1939 self.#accessor_ident().fetch(pool).await
1940 }
1941
1942 /// Eloquent `$model->relation->first()` / `hasOne`
1943 /// semantics — bare-name shortcut over
1944 /// `self.<name>().first(&pool)`. Returns `None` when no
1945 /// child rows match. Useful when the relation is
1946 /// nominally many-to-one in shape but at most one row is
1947 /// expected (latest comment, primary tag, etc.).
1948 pub async fn #first_ident(
1949 &self,
1950 pool: &#root::sql::Pool,
1951 ) -> ::core::result::Result<
1952 ::core::option::Option<#child>,
1953 #root::sql::ExecError,
1954 > {
1955 self.#accessor_ident().first(pool).await
1956 }
1957
1958 /// Eloquent `$model->relation->pluck($col)` — project a
1959 /// single column from the child rows into a `Vec<U>`.
1960 /// Skips the typed `Child` decode, which is cheaper when
1961 /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1962 pub async fn #pluck_ident<U>(
1963 &self,
1964 col: &'static str,
1965 pool: &#root::sql::Pool,
1966 ) -> ::core::result::Result<
1967 ::std::vec::Vec<U>,
1968 #root::sql::ExecError,
1969 >
1970 where
1971 U: #root::sql::FlatScalar,
1972 {
1973 self.#accessor_ident()
1974 .values_list_flat(col)
1975 .fetch::<U>(pool)
1976 .await
1977 }
1978
1979 #[doc = #exists_doc]
1980 pub fn #exists_ident() -> #root::core::WhereExpr {
1981 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1982 let child_schema =
1983 <#child as #root::core::Model>::SCHEMA;
1984 let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1985 lhs: Expr::Column(#child_fk_column),
1986 op: Op::Eq,
1987 rhs: Expr::OuterRef(#self_pk_column),
1988 });
1989 WhereExpr::Exists(::std::boxed::Box::new(inner))
1990 }
1991
1992 #[doc = #not_exists_doc]
1993 pub fn #not_exists_ident() -> #root::core::WhereExpr {
1994 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1995 let child_schema =
1996 <#child as #root::core::Model>::SCHEMA;
1997 let inner = SelectQuery::new(child_schema).where_clause(WhereExpr::ExprCompare {
1998 lhs: Expr::Column(#child_fk_column),
1999 op: Op::Eq,
2000 rhs: Expr::OuterRef(#self_pk_column),
2001 });
2002 WhereExpr::NotExists(::std::boxed::Box::new(inner))
2003 }
2004
2005 #[doc = #count_doc]
2006 pub async fn #count_ident(
2007 &self,
2008 pool: &#root::sql::Pool,
2009 ) -> ::core::result::Result<
2010 i64,
2011 #root::sql::ExecError,
2012 > {
2013 use #root::sql::CounterPool as _;
2014 self.#accessor_ident().count(pool).await
2015 }
2016 }
2017 });
2018 quote! {
2019 impl #struct_name {
2020 #( #methods )*
2021 }
2022 }
2023}
2024
2025/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
2026/// `#[rustango(through(...))]` attribute. Issue #817.
2027///
2028/// Each accessor builds a correlated subquery via
2029/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
2030/// intermediate table, filters on the FK column pointing at this
2031/// model, and projects the intermediate PK. The outer queryset
2032/// filters the far model by its FK-to-intermediate column being in
2033/// that set.
2034///
2035/// The returned `QuerySet<Far>` is **chainable** — the subquery
2036/// lives inside a `where_raw` clause so the user's later
2037/// `.filter()` / `.order_by()` / `.limit()` compositions don't
2038/// disturb it.
2039///
2040/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
2041/// SQLite — no LATERAL or backend-specific syntax involved.
2042fn through_accessor_tokens(
2043 struct_name: &syn::Ident,
2044 through_relations: &[ThroughAttr],
2045) -> TokenStream2 {
2046 let root = rustango_root();
2047 if through_relations.is_empty() {
2048 return TokenStream2::new();
2049 }
2050 let methods = through_relations.iter().map(|rel| {
2051 let method_name = format!("{}_through", rel.name);
2052 let count_name = format!("{}_through_count", rel.name);
2053 let fetch_name = format!("{}_through_fetch", rel.name);
2054 let first_name = format!("{}_through_first", rel.name);
2055 let pluck_name = format!("{}_through_pluck", rel.name);
2056 let method_ident = syn::Ident::new(&method_name, struct_name.span());
2057 let count_ident = syn::Ident::new(&count_name, struct_name.span());
2058 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
2059 let first_ident = syn::Ident::new(&first_name, struct_name.span());
2060 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
2061 let far = &rel.far;
2062 let intermediate = &rel.intermediate;
2063 let far_fk_column = rel.far_fk_column.as_str();
2064 let intermediate_fk_column = rel.intermediate_fk_column.as_str();
2065 let intermediate_pk_column = rel.intermediate_pk_column.as_str();
2066 let doc = format!(
2067 "Eloquent `hasManyThrough` accessor — returns a \
2068 `QuerySet<{far}>` whose rows reach this `{struct_name}` \
2069 instance through the intermediate `{intermediate}` table. \
2070 Generated SQL shape: \
2071 `… WHERE {far_fk_column} IN (SELECT \
2072 {intermediate_pk_column} FROM <{intermediate}> WHERE \
2073 {intermediate_fk_column} = self.pk)`. Chainable like any \
2074 other QuerySet — compose `.filter()` / `.order_by()` / \
2075 `.limit()` etc. on top.",
2076 );
2077 let count_doc = format!(
2078 "Eloquent `$model->{name}->count()` analog for the \
2079 through-relation — returns the number of `{far}` rows \
2080 reachable through `{intermediate}`. Equivalent to \
2081 `self.{name}_through().count(pool)` but spelled \
2082 as a bare instance method for parity with the \
2083 `reverse_has` `<name>_count` shape.",
2084 name = rel.name,
2085 );
2086 quote! {
2087 #[doc = #doc]
2088 pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
2089 use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
2090 let intermediate_schema =
2091 <#intermediate as #root::core::Model>::SCHEMA;
2092 let sub = SelectQuery::new(intermediate_schema)
2093 .where_clause(WhereExpr::Predicate(Filter::new(
2094 #intermediate_fk_column,
2095 Op::Eq,
2096 self.__rustango_pk_value(),
2097 )))
2098 .projection(::std::vec![#intermediate_pk_column]);
2099 #root::query::QuerySet::<#far>::new().where_raw(
2100 WhereExpr::InSubquery {
2101 column: #far_fk_column,
2102 negated: false,
2103 subquery: ::std::boxed::Box::new(sub),
2104 },
2105 )
2106 }
2107
2108 #[doc = #count_doc]
2109 pub async fn #count_ident(
2110 &self,
2111 pool: &#root::sql::Pool,
2112 ) -> ::core::result::Result<
2113 i64,
2114 #root::sql::ExecError,
2115 > {
2116 use #root::sql::CounterPool as _;
2117 self.#method_ident().count(pool).await
2118 }
2119
2120 /// Eloquent `$model->relation->get()` for the
2121 /// through-relation — bare-name hot-path over
2122 /// `self.<name>_through().fetch(&pool)`. Use this
2123 /// when you don't need further `.filter()` /
2124 /// `.order_by()` composition; falls back to the
2125 /// chainable accessor when you do. Avoids the `_pool`
2126 /// suffix on the most common call-site shape.
2127 pub async fn #fetch_ident(
2128 &self,
2129 pool: &#root::sql::Pool,
2130 ) -> ::core::result::Result<
2131 ::std::vec::Vec<#far>,
2132 #root::sql::ExecError,
2133 > {
2134 use #root::sql::FetcherPool as _;
2135 self.#method_ident().fetch(pool).await
2136 }
2137
2138 /// Eloquent `hasOneThrough` analog — bare-name shortcut
2139 /// over `self.<name>_through().first(&pool)`. Returns
2140 /// `None` when no far rows are reachable through the
2141 /// intermediate. Useful when at most one row is
2142 /// expected (latest comment by country, primary tag,
2143 /// etc.).
2144 pub async fn #first_ident(
2145 &self,
2146 pool: &#root::sql::Pool,
2147 ) -> ::core::result::Result<
2148 ::core::option::Option<#far>,
2149 #root::sql::ExecError,
2150 > {
2151 self.#method_ident().first(pool).await
2152 }
2153
2154 /// Pluck a single column from the far rows into `Vec<U>`
2155 /// — cheaper than the typed `<Far>` decode when only one
2156 /// scalar column is needed.
2157 pub async fn #pluck_ident<U>(
2158 &self,
2159 col: &'static str,
2160 pool: &#root::sql::Pool,
2161 ) -> ::core::result::Result<
2162 ::std::vec::Vec<U>,
2163 #root::sql::ExecError,
2164 >
2165 where
2166 U: #root::sql::FlatScalar,
2167 {
2168 self.#method_ident()
2169 .values_list_flat(col)
2170 .fetch::<U>(pool)
2171 .await
2172 }
2173 }
2174 });
2175 quote! {
2176 impl #struct_name {
2177 #( #methods )*
2178 }
2179 }
2180}
2181
2182struct ColumnEntry {
2183 /// The struct field ident, used both for the inherent const name on
2184 /// the model and for the inner column type's name.
2185 ident: syn::Ident,
2186 /// The struct's field type, used as `Column::Value`.
2187 value_ty: Type,
2188 /// Rust-side field name (e.g. `"id"`).
2189 name: String,
2190 /// SQL-side column name (e.g. `"user_id"`).
2191 column: String,
2192 /// `#root::core::FieldType::I64` etc.
2193 field_type_tokens: TokenStream2,
2194 /// A bare or optional `Uuid` field; MySQL decodes it from text.
2195 uuid_column: bool,
2196}
2197
2198struct CollectedFields {
2199 field_schemas: Vec<TokenStream2>,
2200 from_row_inits: Vec<TokenStream2>,
2201 /// Aliased counterparts of `from_row_inits` — read columns via
2202 /// `format!("{prefix}__{col}")` aliases so a Model can be
2203 /// decoded from a JOINed row's projected target columns.
2204 from_aliased_row_inits: Vec<TokenStream2>,
2205 /// MySQL twins of `from_row_inits` / `from_aliased_row_inits`.
2206 from_row_inits_my: Vec<TokenStream2>,
2207 from_aliased_row_inits_my: Vec<TokenStream2>,
2208 /// Static column-name list — used by the simple insert path
2209 /// (no `Auto<T>` fields). Aligned with `insert_values`.
2210 insert_columns: Vec<TokenStream2>,
2211 /// Static `Into<SqlValue>` expressions, one per field. Aligned
2212 /// with `insert_columns`. Used by the simple insert path only.
2213 insert_values: Vec<TokenStream2>,
2214 /// Per-field push expressions for the dynamic (Auto-aware)
2215 /// insert path. Each statement either unconditionally pushes
2216 /// `(column, value)` or, for an `Auto<T>` field, conditionally
2217 /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
2218 insert_pushes: Vec<TokenStream2>,
2219 /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
2220 /// when `has_auto == false`.
2221 returning_cols: Vec<TokenStream2>,
2222 /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
2223 /// field. Run after `insert_returning` to populate the model.
2224 auto_assigns: Vec<TokenStream2>,
2225 /// `(ident, column_literal, is_uuid)` for every Auto field. Used by
2226 /// the bulk_insert codegen to rebuild assigns against `_row_mut`
2227 /// instead of `self`.
2228 auto_field_idents: Vec<(syn::Ident, String, bool)>,
2229 /// #1028 — `(ident, column)` for each `generated_as` field. Drives
2230 /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
2231 /// back into the struct after insert (MySQL has no RETURNING → the
2232 /// field stays at its placeholder until the next read).
2233 generated_field_idents: Vec<(syn::Ident, String)>,
2234 /// First `Auto<T>` field the database fills, with its inner `T`: the
2235 /// MySQL `LAST_INSERT_ID()` target. Rust-filled ones never are (#1934).
2236 first_db_auto: Option<(syn::Ident, Type)>,
2237 /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2238 /// by the all-Auto-Unset bulk path (Auto cols dropped from
2239 /// `columns`).
2240 bulk_pushes_no_auto: Vec<TokenStream2>,
2241 /// Bulk-insert per-row pushes for **all fields including Auto**.
2242 /// Used by the all-Auto-Set bulk path (Auto col included with the
2243 /// caller-supplied value).
2244 bulk_pushes_all: Vec<TokenStream2>,
2245 /// Column-name literals for non-Auto fields only (paired with
2246 /// `bulk_pushes_no_auto`).
2247 bulk_columns_no_auto: Vec<TokenStream2>,
2248 /// Column-name literals for every field including Auto (paired
2249 /// with `bulk_pushes_all`).
2250 bulk_columns_all: Vec<TokenStream2>,
2251 /// Fills each `default_uuid_v7` field of `_row` in place, so a
2252 /// mutable bulk insert hands the PKs back (#1934).
2253 bulk_fill_in_place: Vec<TokenStream2>,
2254 /// Rust-filled `Auto` fields (`default_uuid_v7`, `auto_now(_add)`), reset when `insert_or_ignore` skips (#1937).
2255 rust_filled_idents: Vec<syn::Ident>,
2256 /// `auto_now` fields: `upsert` stamps them before binding.
2257 auto_now_idents: Vec<syn::Ident>,
2258 /// `default_uuid_v7` fields: an `Unset` one gets a fresh id, so the
2259 /// upsert marks it `generated` for the MySQL key guard.
2260 uuid_v7_fields: Vec<(syn::Ident, String)>,
2261 /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2262 /// + the loop that asserts every row matches. One pair per Auto
2263 /// field. Empty when `has_auto == false`.
2264 bulk_auto_uniformity: Vec<TokenStream2>,
2265 /// Identifier of the first Auto field, used as the witness for
2266 /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2267 first_auto_ident: Option<syn::Ident>,
2268 /// `true` if any field on the struct is `Auto<T>`.
2269 has_auto: bool,
2270 /// `true` when the primary-key field's Rust type is `Auto<T>`.
2271 /// Gates `save()` codegen — only Auto PKs let us infer
2272 /// insert-vs-update from the in-memory value.
2273 pk_is_auto: bool,
2274 /// `Assignment` constructors for every non-PK column. Drives the
2275 /// UPDATE branch of `save()`.
2276 update_assignments: Vec<TokenStream2>,
2277 /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2278 /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2279 upsert_update_columns: Vec<TokenStream2>,
2280 primary_key: Option<(syn::Ident, String)>,
2281 column_entries: Vec<ColumnEntry>,
2282 /// Rust-side field names, in declaration order. Used to validate
2283 /// container attributes like `display = "…"`.
2284 field_names: Vec<String>,
2285 /// FK fields on this child model. Drives the reverse-relation
2286 /// helper emit — for each FK, the macro adds an inherent
2287 /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2288 /// method on the parent type.
2289 fk_relations: Vec<FkRelation>,
2290 /// SQL column name of the `#[rustango(soft_delete)]` field, if
2291 /// the model has one. Drives emission of the `soft_delete_on` /
2292 /// `restore_on` inherent methods. At most one such column per
2293 /// model is allowed; collect_fields rejects duplicates.
2294 soft_delete_column: Option<String>,
2295 /// Rust field ident of the `#[rustango(soft_delete)]` field —
2296 /// companion to `soft_delete_column` for emitting predicates
2297 /// that need to read the field off `&self` (e.g. `trashed()`).
2298 soft_delete_field_ident: Option<syn::Ident>,
2299}
2300
2301#[derive(Clone)]
2302struct FkRelation {
2303 /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2304 /// helper is emitted as `impl <ParentType> { … }`.
2305 parent_type: Type,
2306 /// Rust field ident of the FK. Its name is the key for select_related,
2307 /// prefetch and filters; the SQL column may differ (#1936).
2308 field_ident: syn::Ident,
2309 /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2310 /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2311 /// default `ForeignKey<T>` (no explicit K); other kinds when the
2312 /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2313 pk_kind: DetectedKind,
2314 /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2315 /// FK column). Drives the `Some(...)` wrapping in load_related
2316 /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2317 /// the codegen matches the field's declared shape.
2318 nullable: bool,
2319 /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2320 /// override. When set, the reverse helper picks this name instead
2321 /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2322 /// #816 (issue's "Related" note re: per-FK override).
2323 related_name: Option<String>,
2324}
2325
2326fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2327 let root = rustango_root();
2328 let cap = named.named.len();
2329 let mut out = CollectedFields {
2330 field_schemas: Vec::with_capacity(cap),
2331 from_row_inits: Vec::with_capacity(cap),
2332 from_aliased_row_inits: Vec::with_capacity(cap),
2333 from_row_inits_my: Vec::with_capacity(cap),
2334 from_aliased_row_inits_my: Vec::with_capacity(cap),
2335 insert_columns: Vec::with_capacity(cap),
2336 insert_values: Vec::with_capacity(cap),
2337 insert_pushes: Vec::with_capacity(cap),
2338 returning_cols: Vec::new(),
2339 auto_assigns: Vec::new(),
2340 auto_field_idents: Vec::new(),
2341 generated_field_idents: Vec::new(),
2342 first_db_auto: None,
2343 bulk_pushes_no_auto: Vec::with_capacity(cap),
2344 bulk_pushes_all: Vec::with_capacity(cap),
2345 bulk_columns_no_auto: Vec::with_capacity(cap),
2346 bulk_columns_all: Vec::with_capacity(cap),
2347 bulk_fill_in_place: Vec::new(),
2348 rust_filled_idents: Vec::new(),
2349 auto_now_idents: Vec::new(),
2350 uuid_v7_fields: Vec::new(),
2351 bulk_auto_uniformity: Vec::new(),
2352 first_auto_ident: None,
2353 has_auto: false,
2354 pk_is_auto: false,
2355 update_assignments: Vec::with_capacity(cap),
2356 upsert_update_columns: Vec::with_capacity(cap),
2357 primary_key: None,
2358 column_entries: Vec::with_capacity(cap),
2359 field_names: Vec::with_capacity(cap),
2360 fk_relations: Vec::new(),
2361 soft_delete_column: None,
2362 soft_delete_field_ident: None,
2363 };
2364
2365 for field in &named.named {
2366 let info = process_field(field, table)?;
2367 out.field_names.push(ident_name(info.ident));
2368 out.field_schemas.push(info.schema);
2369 out.from_row_inits.push(info.from_row_init);
2370 out.from_aliased_row_inits.push(info.from_aliased_row_init);
2371 out.from_row_inits_my.push(info.from_row_init_my);
2372 out.from_aliased_row_inits_my
2373 .push(info.from_aliased_row_init_my);
2374 if let Some(parent_ty) = info.fk_inner.clone() {
2375 out.fk_relations.push(FkRelation {
2376 parent_type: parent_ty,
2377 field_ident: info.ident.clone(),
2378 pk_kind: info.fk_pk_kind,
2379 nullable: info.nullable,
2380 related_name: info.related_name.clone(),
2381 });
2382 }
2383 if info.soft_delete {
2384 if out.soft_delete_column.is_some() {
2385 return Err(syn::Error::new_spanned(
2386 field,
2387 "only one field may be marked `#[rustango(soft_delete)]`",
2388 ));
2389 }
2390 out.soft_delete_column = Some(info.column.clone());
2391 out.soft_delete_field_ident = Some(info.ident.clone());
2392 }
2393 let column = info.column.as_str();
2394 let ident = info.ident;
2395 // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2396 // skip every write path — Postgres recomputes the value
2397 // from EXPR. Push only the column-entry record (so typed
2398 // column constants still exist for filtering / projection)
2399 // and the schema literal (already pushed above) and move
2400 // on. No insert_columns/values, no insert_pushes, no
2401 // bulk_*, no update_assignments, no upsert_update_columns,
2402 // no returning_cols.
2403 if info.generated_as.is_some() {
2404 out.column_entries.push(ColumnEntry {
2405 ident: ident.clone(),
2406 value_ty: info.value_ty.clone(),
2407 name: ident_name(ident),
2408 column: info.column.clone(),
2409 field_type_tokens: info.field_type_tokens,
2410 uuid_column: info.uuid_column,
2411 });
2412 // #1028 — refresh the DB-computed value after insert on
2413 // RETURNING-capable backends. The column joins `returning_cols`
2414 // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2415 // ident is recorded so the `AssignAutoPkPool` impl decodes it
2416 // back into the struct. MySQL has no `INSERT … RETURNING`, so
2417 // it keeps the placeholder until the next read.
2418 out.returning_cols.push(quote!(#column));
2419 out.generated_field_idents
2420 .push((ident.clone(), info.column.clone()));
2421 continue;
2422 }
2423 out.insert_columns.push(quote!(#column));
2424 out.insert_values.push(quote! {
2425 ::core::convert::Into::<#root::core::SqlValue>::into(
2426 ::core::clone::Clone::clone(&self.#ident)
2427 )
2428 });
2429 if info.auto {
2430 out.has_auto = true;
2431 if out.first_auto_ident.is_none() {
2432 out.first_auto_ident = Some(ident.clone());
2433 }
2434 // `default_uuid_v7` (issue #823) generates the PK Rust-side
2435 // before binding, so the value is already in
2436 // `self.#ident` after the insert_push — RETURNING is
2437 // redundant. Skip adding this column to returning_cols /
2438 // auto_assigns / auto_field_idents to avoid (a) an
2439 // unnecessary RETURNING column on every dialect, and (b)
2440 // the MySQL `LAST_INSERT_ID()` path that can only fill an
2441 // integer PK.
2442 // `auto_now_add` / `auto_now` join `default_uuid_v7` here
2443 // for the same reason: they are filled Rust-side below and
2444 // bound, so the value is already in `self` and RETURNING
2445 // would be a redundant column on every dialect.
2446 if !info.default_uuid_v7 && !info.auto_now_add && !info.auto_now {
2447 if out.first_db_auto.is_none() {
2448 out.first_db_auto = auto_inner_type(info.value_ty)
2449 .cloned()
2450 .map(|ty| (ident.clone(), ty));
2451 }
2452 out.returning_cols.push(quote!(#column));
2453 out.auto_field_idents.push((
2454 ident.clone(),
2455 info.column.clone(),
2456 info.fk_pk_kind == DetectedKind::Uuid,
2457 ));
2458 out.auto_assigns.push(quote! {
2459 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2460 });
2461 }
2462 if info.default_uuid_v7 {
2463 // Rust-side UUIDv7 generation (issue #823, Eloquent
2464 // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2465 // and bind; Auto::Set → bind the user's value. The
2466 // column is ALWAYS present in the INSERT statement —
2467 // no RETURNING / no DB DEFAULT needed.
2468 out.insert_pushes.push(quote! {
2469 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2470 self.#ident = #root::sql::Auto::Set(
2471 #root::__uuid::Uuid::now_v7(),
2472 );
2473 }
2474 if let #root::sql::Auto::Set(_v) = &self.#ident {
2475 _columns.push(#column);
2476 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2477 ::core::clone::Clone::clone(_v)
2478 ));
2479 }
2480 });
2481 } else if info.auto_now_add || info.auto_now {
2482 // Rust-side write timestamp (#1464), mirroring
2483 // `default_uuid_v7` above. `auto_now` has always bound
2484 // `Utc::now()` on UPDATE but not on INSERT, so the
2485 // first write of an `updated_at` took the DB default
2486 // while every later one took the clock — the same
2487 // column, filled two different ways.
2488 //
2489 // The default is the problem. `CREATE TABLE IF NOT
2490 // EXISTS` leaves an upgraded table's
2491 // `DEFAULT CURRENT_TIMESTAMP` in place, and SQLite's
2492 // ALTER TABLE grammar (RENAME/ADD/DROP) has no
2493 // statement that changes it — so every insert relying
2494 // on that default wrote the legacy shape forever, while
2495 // the migrate sweep converted the rows around it. The
2496 // column went permanently mixed and `ORDER BY` inverted
2497 // the admin audit log (#1616 rework review,
2498 // correctness-001 / dialects-005 / security-002).
2499 //
2500 // Binding it here makes the stale default unreachable
2501 // rather than trying to migrate it: the timestamp comes
2502 // from the application, not the database.
2503 out.insert_pushes.push(quote! {
2504 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2505 self.#ident = #root::sql::Auto::Set(
2506 #root::__chrono::Utc::now(),
2507 );
2508 }
2509 if let #root::sql::Auto::Set(_v) = &self.#ident {
2510 _columns.push(#column);
2511 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2512 ::core::clone::Clone::clone(_v)
2513 ));
2514 }
2515 });
2516 } else {
2517 out.insert_pushes.push(quote! {
2518 if let #root::sql::Auto::Set(_v) = &self.#ident {
2519 _columns.push(#column);
2520 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2521 ::core::clone::Clone::clone(_v)
2522 ));
2523 }
2524 });
2525 }
2526 // …except the timestamp columns, which must appear in BOTH
2527 // paths (#1464). The path is chosen by the *first* Auto
2528 // field — in practice the PK — so a bulk insert of rows
2529 // with an unset PK took the no-auto branch and dropped the
2530 // timestamp column, falling back to the DB default that
2531 // this change exists to stop depending on.
2532 //
2533 // `rows` is borrowed immutably here, so this fills per row
2534 // at push time rather than writing back into the struct. An
2535 // explicitly-Set value is still honoured; only `Unset`
2536 // takes the clock.
2537 // `default_uuid_v7` likewise: its column has no DB default (#1934).
2538 if info.default_uuid_v7 || info.auto_now_add || info.auto_now {
2539 out.rust_filled_idents.push(ident.clone());
2540 }
2541 if info.auto_now {
2542 out.auto_now_idents.push(ident.clone());
2543 }
2544 if info.default_uuid_v7 {
2545 out.uuid_v7_fields
2546 .push((ident.clone(), info.column.clone()));
2547 }
2548 let rust_fill = if info.default_uuid_v7 {
2549 Some(quote!(#root::__uuid::Uuid::now_v7()))
2550 } else if info.auto_now_add || info.auto_now {
2551 Some(quote!(#root::__chrono::Utc::now()))
2552 } else {
2553 None
2554 };
2555 // Bulk: Auto fields appear in the all-Set path. An unset filled
2556 // field takes its fill there too, not NULL (#1950).
2557 out.bulk_columns_all.push(quote!(#column));
2558 if let Some(fill) = &rust_fill {
2559 let push = quote! {
2560 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2561 match &_row.#ident {
2562 #root::sql::Auto::Set(_v) => ::core::clone::Clone::clone(_v),
2563 #root::sql::Auto::Unset => #fill,
2564 }
2565 ));
2566 };
2567 out.bulk_columns_no_auto.push(quote!(#column));
2568 out.bulk_pushes_no_auto.push(push.clone());
2569 out.bulk_pushes_all.push(push);
2570 } else {
2571 out.bulk_pushes_all.push(quote! {
2572 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2573 ::core::clone::Clone::clone(&_row.#ident)
2574 ));
2575 });
2576 }
2577 if info.default_uuid_v7 {
2578 out.bulk_fill_in_place.push(quote! {
2579 if matches!(&_row.#ident, #root::sql::Auto::Unset) {
2580 _row.#ident = #root::sql::Auto::Set(#root::__uuid::Uuid::now_v7());
2581 }
2582 });
2583 }
2584 // Uniformity check: every row's Auto state must match the
2585 // first row's. Mixed Set/Unset within one bulk_insert is
2586 // rejected here so the column list stays consistent.
2587 // A filled field may be unset on any row, so it is exempt.
2588 let ident_clone = ident.clone();
2589 if rust_fill.is_none() {
2590 out.bulk_auto_uniformity.push(quote! {
2591 for _r in rows.iter().skip(1) {
2592 if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2593 return ::core::result::Result::Err(
2594 #root::sql::ExecError::Sql(
2595 #root::sql::SqlError::BulkAutoMixed
2596 )
2597 );
2598 }
2599 }
2600 });
2601 }
2602 } else {
2603 out.insert_pushes.push(quote! {
2604 _columns.push(#column);
2605 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2606 ::core::clone::Clone::clone(&self.#ident)
2607 ));
2608 });
2609 // Bulk: non-Auto fields appear in BOTH paths.
2610 out.bulk_columns_no_auto.push(quote!(#column));
2611 out.bulk_columns_all.push(quote!(#column));
2612 let push_expr = quote! {
2613 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2614 ::core::clone::Clone::clone(&_row.#ident)
2615 ));
2616 };
2617 out.bulk_pushes_no_auto.push(push_expr.clone());
2618 out.bulk_pushes_all.push(push_expr);
2619 }
2620 if info.primary_key {
2621 if out.primary_key.is_some() {
2622 return Err(syn::Error::new_spanned(
2623 field,
2624 "only one field may be marked `#[rustango(primary_key)]`",
2625 ));
2626 }
2627 out.primary_key = Some((ident.clone(), info.column.clone()));
2628 if info.auto {
2629 out.pk_is_auto = true;
2630 }
2631 } else if info.auto_now_add {
2632 // Immutable post-insert: skip from UPDATE entirely.
2633 } else if info.auto_now {
2634 // `auto_now` columns: bind `chrono::Utc::now()` on every
2635 // UPDATE, and write it back so `self` matches the row.
2636 out.update_assignments.push(quote! {
2637 #root::core::Assignment::new(
2638 #column,
2639 {
2640 let _now = #root::__chrono::Utc::now();
2641 self.#ident = #root::sql::Auto::Set(_now);
2642 ::core::convert::Into::<#root::core::SqlValue>::into(_now)
2643 },
2644 )
2645 });
2646 out.upsert_update_columns.push(quote!(#column));
2647 } else {
2648 out.update_assignments.push(quote! {
2649 #root::core::Assignment::new(
2650 #column,
2651 ::core::convert::Into::<#root::core::SqlValue>::into(
2652 ::core::clone::Clone::clone(&self.#ident)
2653 ),
2654 )
2655 });
2656 out.upsert_update_columns.push(quote!(#column));
2657 }
2658 out.column_entries.push(ColumnEntry {
2659 ident: ident.clone(),
2660 value_ty: info.value_ty.clone(),
2661 name: ident_name(ident),
2662 column: info.column.clone(),
2663 field_type_tokens: info.field_type_tokens,
2664 uuid_column: info.uuid_column,
2665 });
2666 }
2667 Ok(out)
2668}
2669
2670/// A derived index name within PG's 63 bytes (MySQL takes 64); a longer one
2671/// keeps a prefix plus a hash of the whole, so two long names stay distinct.
2672fn capped_index_name(name: String) -> String {
2673 const MAX: usize = 63;
2674 if name.len() <= MAX {
2675 return name;
2676 }
2677 // FNV-1a: stable across builds, unlike `DefaultHasher`.
2678 let hash = name.bytes().fold(0x811c_9dc5_u32, |h, b| {
2679 (h ^ u32::from(b)).wrapping_mul(0x0100_0193)
2680 });
2681 let mut cut = MAX - 9;
2682 while !name.is_char_boundary(cut) {
2683 cut -= 1;
2684 }
2685 format!("{}_{hash:08x}", &name[..cut])
2686}
2687
2688fn model_impl_tokens(
2689 struct_name: &syn::Ident,
2690 model_name: &str,
2691 table: &str,
2692 display: Option<&str>,
2693 app_label: Option<&str>,
2694 admin: Option<&AdminAttrs>,
2695 default_order: &[(String, bool, proc_macro2::Span)],
2696 field_schemas: &[TokenStream2],
2697 soft_delete_column: Option<&str>,
2698 permissions: bool,
2699 audit_track: Option<&[String]>,
2700 m2m_relations: &[M2MAttr],
2701 indexes: &[IndexAttr],
2702 checks: &[CheckAttr],
2703 excludes: &[ExcludeAttr],
2704 composite_fks: &[CompositeFkAttr],
2705 generic_fks: &[GenericFkAttr],
2706 scope: Option<&str>,
2707 is_view: bool,
2708 verbose_name: Option<&str>,
2709 verbose_name_plural: Option<&str>,
2710 managed: bool,
2711 base_manager_name: Option<&str>,
2712 order_with_respect_to: Option<&str>,
2713 proxy: bool,
2714 required_db_features: &[String],
2715 required_db_vendor: Option<&str>,
2716 default_related_name: Option<&str>,
2717 db_table_comment: Option<&str>,
2718 get_latest_by: Option<(&str, bool)>,
2719 extra_permissions: &[(String, String)],
2720 default_permissions: &[String],
2721 global_scopes: &[GlobalScopeAttr],
2722 reverse_has_relations: &[ReverseHasAttr],
2723 generic_has_relations: &[GenericHasAttr],
2724) -> TokenStream2 {
2725 let root = rustango_root();
2726 let display_tokens = if let Some(name) = display {
2727 quote!(::core::option::Option::Some(#name))
2728 } else {
2729 quote!(::core::option::Option::None)
2730 };
2731 let app_label_tokens = if let Some(name) = app_label {
2732 quote!(::core::option::Option::Some(#name))
2733 } else {
2734 quote!(::core::option::Option::None)
2735 };
2736 let soft_delete_tokens = if let Some(col) = soft_delete_column {
2737 quote!(::core::option::Option::Some(#col))
2738 } else {
2739 quote!(::core::option::Option::None)
2740 };
2741 let audit_track_tokens = match audit_track {
2742 None => quote!(::core::option::Option::None),
2743 Some(names) => {
2744 let lits = names.iter().map(|n| n.as_str());
2745 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2746 }
2747 };
2748 let admin_tokens = admin_config_tokens(admin);
2749 // Default `tenant` so single-tenant projects (no `scope` attr
2750 // anywhere) keep the v0.24.x behavior. Container-attr parser
2751 // already validated the value is "registry" or "tenant".
2752 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2753 Some("registry") => quote!(#root::core::ModelScope::Registry),
2754 _ => quote!(#root::core::ModelScope::Tenant),
2755 };
2756 let verbose_name_tokens = optional_str(verbose_name);
2757 let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2758 let base_manager_name_tokens = optional_str(base_manager_name);
2759 let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2760 let required_db_features_lits: Vec<&str> =
2761 required_db_features.iter().map(String::as_str).collect();
2762 let required_db_vendor_tokens = optional_str(required_db_vendor);
2763 let default_related_name_tokens = optional_str(default_related_name);
2764 let db_table_comment_tokens = optional_str(db_table_comment);
2765 let get_latest_by_tokens = match get_latest_by {
2766 Some((col, desc)) => {
2767 quote!(::core::option::Option::Some((#col, #desc)))
2768 }
2769 None => quote!(::core::option::Option::None),
2770 };
2771 let extra_permission_tokens: Vec<_> = extra_permissions
2772 .iter()
2773 .map(|(c, l)| quote!((#c, #l)))
2774 .collect();
2775 let default_permission_tokens: Vec<_> = default_permissions
2776 .iter()
2777 .map(|action| quote!(#action))
2778 .collect();
2779 let indexes_tokens = indexes.iter().map(|idx| {
2780 // When no explicit `name = "..."` was given, derive a stable,
2781 // collision-free name from the table + columns
2782 // (`<table>_<col>_<col>_idx`) instead of a shared literal that
2783 // would clash the moment a model declares two unnamed indexes.
2784 let derived_name = idx
2785 .name
2786 .clone()
2787 .unwrap_or_else(|| capped_index_name(format!("{table}_{}_idx", idx.columns.join("_"))));
2788 let name = derived_name.as_str();
2789 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2790 let unique = idx.unique;
2791 // Map the parsed method string onto the IndexMethod enum
2792 // variant — kept at the codegen layer so the IR doesn't
2793 // carry the string form.
2794 let method_variant = match idx.method.as_str() {
2795 "gin" => quote!(#root::core::IndexMethod::Gin),
2796 "gist" => quote!(#root::core::IndexMethod::Gist),
2797 "brin" => quote!(#root::core::IndexMethod::Brin),
2798 "spgist" => quote!(#root::core::IndexMethod::SpGist),
2799 "hash" => quote!(#root::core::IndexMethod::Hash),
2800 "bloom" => quote!(#root::core::IndexMethod::Bloom),
2801 _ => quote!(#root::core::IndexMethod::BTree),
2802 };
2803 let where_clause = match &idx.where_clause {
2804 Some(s) => quote!(::core::option::Option::Some(#s)),
2805 None => quote!(::core::option::Option::None),
2806 };
2807 let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2808 quote! {{
2809 let mut i = #root::core::IndexSchema::new(#name, &[ #(#cols),* ]);
2810 i.unique = #unique;
2811 i.method = #method_variant;
2812 i.where_clause = #where_clause;
2813 i.include = &[ #(#include_lits),* ];
2814 i
2815 }}
2816 });
2817 let checks_tokens = checks.iter().map(|c| {
2818 let name = c.name.as_str();
2819 let expr = c.expr.as_str();
2820 quote!(#root::core::CheckConstraint::new(#name, #expr))
2821 });
2822 let excludes_tokens = excludes.iter().map(|e| {
2823 let name = e.name.as_str();
2824 let using = e.using.as_str();
2825 let element_tokens = e.elements.iter().map(|(col, op)| {
2826 let col_s = col.as_str();
2827 let op_s = op.as_str();
2828 quote!((#col_s, #op_s))
2829 });
2830 let where_tokens = match e.where_clause.as_deref() {
2831 Some(w) => quote!(::core::option::Option::Some(#w)),
2832 None => quote!(::core::option::Option::None),
2833 };
2834 quote! {{
2835 let mut e = #root::core::ExclusionConstraint::new(
2836 #name,
2837 #using,
2838 &[ #(#element_tokens),* ],
2839 );
2840 e.where_clause = #where_tokens;
2841 e
2842 }}
2843 });
2844 let composite_fk_tokens = composite_fks.iter().map(|rel| {
2845 let name = rel.name.as_str();
2846 let to = rel.to.as_str();
2847 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2848 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2849 quote! {
2850 #root::core::CompositeFkRelation::new(
2851 #name,
2852 #to,
2853 &[ #(#from_cols),* ],
2854 &[ #(#on_cols),* ],
2855 )
2856 }
2857 });
2858 let generic_fk_tokens = generic_fks.iter().map(|rel| {
2859 let name = rel.name.as_str();
2860 let ct_col = rel.ct_column.as_str();
2861 let pk_col = rel.pk_column.as_str();
2862 quote!(#root::core::GenericRelation::new(#name, #ct_col, #pk_col))
2863 });
2864 // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2865 // no `#[rustango(default_order = "...")]` attribute was supplied.
2866 let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2867 let col_lit = col.as_str();
2868 quote! { (#col_lit, #desc) }
2869 });
2870
2871 // Issue #820 — `global_scopes` slice literal. Empty when no
2872 // `#[rustango(global_scope(...))]` attribute was supplied. The
2873 // `apply` path is re-emitted verbatim so it resolves in the
2874 // consumer's scope; the name is stored as a string literal.
2875 let global_scope_tokens = global_scopes.iter().map(|s| {
2876 let name = s.name.as_str();
2877 // The slice sits in a nested `const`, where `Self` does not resolve.
2878 let mut apply = s.apply.clone();
2879 if let Some(first) = apply.segments.first_mut() {
2880 if first.ident == "Self" {
2881 first.ident = struct_name.clone();
2882 }
2883 }
2884 quote!(#root::core::GlobalScope::new(#name, #apply))
2885 });
2886
2887 let m2m_tokens = m2m_relations.iter().map(|rel| {
2888 let name = rel.name.as_str();
2889 let to = rel.to.as_str();
2890 let through = rel.through.as_str();
2891 let src = rel.src.as_str();
2892 let dst = rel.dst.as_str();
2893 let auto_create = rel.auto_create;
2894 quote! {{
2895 let mut m = #root::core::M2MRelation::new(#name, #to, #through, #src, #dst);
2896 m.auto_create = #auto_create;
2897 m
2898 }}
2899 });
2900 // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2901 // when the model declares `#[rustango(reverse_has(...))]`. Each
2902 // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2903 // a const expression. Models without reverse_has fall through to
2904 // the trait's empty default — no override emitted.
2905 let reverse_relations_override = if reverse_has_relations.is_empty() {
2906 quote!()
2907 } else {
2908 let entries = reverse_has_relations.iter().map(|rel| {
2909 let name = rel.name.as_str();
2910 let child = &rel.child;
2911 let child_fk_column = rel.child_fk_column.as_str();
2912 let self_pk_column = rel.self_pk_column();
2913 quote! {
2914 #root::core::ReverseRelation::new(
2915 #name,
2916 <#child as #root::core::Model>::SCHEMA,
2917 #child_fk_column,
2918 #self_pk_column,
2919 )
2920 }
2921 });
2922 quote! {
2923 fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2924 const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2925 RELS
2926 }
2927 }
2928 };
2929 // Issue #830 — `Model::generic_reverse_relations()` override for
2930 // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2931 let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2932 quote!()
2933 } else {
2934 let entries = generic_has_relations.iter().map(|rel| {
2935 let name = rel.name.as_str();
2936 let child = &rel.child;
2937 let ct_column = rel.ct_column.as_str();
2938 let pk_column = rel.pk_column.as_str();
2939 let self_pk_column = rel.self_pk_column();
2940 quote! {
2941 #root::core::GenericReverseRelation::new(
2942 #name,
2943 <#child as #root::core::Model>::SCHEMA,
2944 #ct_column,
2945 #pk_column,
2946 #self_pk_column,
2947 )
2948 }
2949 });
2950 quote! {
2951 fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2952 const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2953 RELS
2954 }
2955 }
2956 };
2957 let fields_slice = const_slice(
2958 "e!(#root::core::FieldSchema),
2959 field_schemas.iter().cloned(),
2960 );
2961 let m2m_slice = const_slice("e!(#root::core::M2MRelation), m2m_tokens);
2962 let indexes_slice = const_slice("e!(#root::core::IndexSchema), indexes_tokens);
2963 let checks_slice = const_slice("e!(#root::core::CheckConstraint), checks_tokens);
2964 let excludes_slice = const_slice("e!(#root::core::ExclusionConstraint), excludes_tokens);
2965 let composite_fk_slice = const_slice(
2966 "e!(#root::core::CompositeFkRelation),
2967 composite_fk_tokens,
2968 );
2969 let generic_fk_slice = const_slice("e!(#root::core::GenericRelation), generic_fk_tokens);
2970 let audited_update_override = if audit_track.is_some() {
2971 quote! {
2972 fn __rustango_audited_update() -> ::core::option::Option<#root::audit::AuditedUpdate> {
2973 ::core::option::Option::Some(Self::__rustango_update_audited)
2974 }
2975 fn __rustango_audited_delete() -> ::core::option::Option<#root::audit::AuditedDelete> {
2976 ::core::option::Option::Some(Self::__rustango_delete_audited)
2977 }
2978 fn __rustango_audited_create() -> ::core::option::Option<#root::audit::AuditedCreate> {
2979 ::core::option::Option::Some(Self::__rustango_create_audited)
2980 }
2981 fn __rustango_audited_update_record(
2982 ) -> ::core::option::Option<#root::audit::AuditedUpdateRecord> {
2983 ::core::option::Option::Some(Self::__rustango_update_record_audited)
2984 }
2985 fn __rustango_audited_cascade() -> ::core::option::Option<#root::audit::AuditedCascade> {
2986 ::core::option::Option::Some(Self::__rustango_cascade_audited)
2987 }
2988 }
2989 } else {
2990 quote!()
2991 };
2992 let global_scope_slice = const_slice("e!(#root::core::GlobalScope), global_scope_tokens);
2993 quote! {
2994 impl #root::core::Model for #struct_name {
2995 const SCHEMA: &'static #root::core::ModelSchema = &{
2996 #root::core::ModelSchema::from_parts(#root::core::ModelSchemaParts {
2997 name: #model_name,
2998 table: #table,
2999 fields: #fields_slice,
3000 display: #display_tokens,
3001 app_label: #app_label_tokens,
3002 admin: #admin_tokens,
3003 soft_delete_column: #soft_delete_tokens,
3004 permissions: #permissions,
3005 audit_track: #audit_track_tokens,
3006 m2m: #m2m_slice,
3007 indexes: #indexes_slice,
3008 check_constraints: #checks_slice,
3009 exclusion_constraints: #excludes_slice,
3010 composite_relations: #composite_fk_slice,
3011 generic_relations: #generic_fk_slice,
3012 scope: #scope_tokens,
3013 default_order: &[ #(#default_order_tokens),* ],
3014 is_view: #is_view,
3015 verbose_name: #verbose_name_tokens,
3016 verbose_name_plural: #verbose_name_plural_tokens,
3017 managed: #managed,
3018 base_manager_name: #base_manager_name_tokens,
3019 order_with_respect_to: #order_with_respect_to_tokens,
3020 proxy: #proxy,
3021 required_db_features: &[ #(#required_db_features_lits),* ],
3022 required_db_vendor: #required_db_vendor_tokens,
3023 default_related_name: #default_related_name_tokens,
3024 db_table_comment: #db_table_comment_tokens,
3025 get_latest_by: #get_latest_by_tokens,
3026 extra_permissions: &[ #(#extra_permission_tokens),* ],
3027 default_permissions: &[ #(#default_permission_tokens),* ],
3028 global_scopes: #global_scope_slice,
3029 })
3030 };
3031
3032 #reverse_relations_override
3033 #generic_reverse_relations_override
3034 #audited_update_override
3035 }
3036 }
3037}
3038
3039/// Emit the `admin: Option<&'static AdminConfig>` field for the model
3040/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
3041/// otherwise a static reference to a populated `AdminConfig`.
3042fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
3043 let root = rustango_root();
3044 let Some(admin) = admin else {
3045 return quote!(::core::option::Option::None);
3046 };
3047
3048 let list_display = admin
3049 .list_display
3050 .as_ref()
3051 .map(|(v, _)| v.as_slice())
3052 .unwrap_or(&[]);
3053 let list_display_lits = list_display.iter().map(|s| s.as_str());
3054
3055 let search_fields = admin
3056 .search_fields
3057 .as_ref()
3058 .map(|(v, _)| v.as_slice())
3059 .unwrap_or(&[]);
3060 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
3061
3062 let readonly_fields = admin
3063 .readonly_fields
3064 .as_ref()
3065 .map(|(v, _)| v.as_slice())
3066 .unwrap_or(&[]);
3067 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
3068
3069 let list_filter = admin
3070 .list_filter
3071 .as_ref()
3072 .map(|(v, _)| v.as_slice())
3073 .unwrap_or(&[]);
3074 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
3075
3076 let actions = admin
3077 .actions
3078 .as_ref()
3079 .map(|(v, _)| v.as_slice())
3080 .unwrap_or(&[]);
3081 let actions_lits = actions.iter().map(|s| s.as_str());
3082
3083 let fieldsets = admin
3084 .fieldsets
3085 .as_ref()
3086 .map(|(v, _)| v.as_slice())
3087 .unwrap_or(&[]);
3088 let fieldset_tokens = const_slice(
3089 "e!(#root::core::Fieldset),
3090 fieldsets.iter().map(|(title, fields)| {
3091 let title = title.as_str();
3092 let field_lits = fields.iter().map(String::as_str);
3093 quote!(#root::core::Fieldset::new(#title, &[ #( #field_lits ),* ]))
3094 }),
3095 );
3096
3097 let list_display_links = admin
3098 .list_display_links
3099 .as_ref()
3100 .map(|(v, _)| v.as_slice())
3101 .unwrap_or(&[]);
3102 let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
3103
3104 let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
3105 let actions_on_top = admin.actions_on_top.unwrap_or(true);
3106 let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
3107 let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
3108
3109 let prepopulated = admin
3110 .prepopulated_fields
3111 .as_ref()
3112 .map(|(v, _)| v.as_slice())
3113 .unwrap_or(&[]);
3114 let prepopulated_tokens = const_slice(
3115 "e!(#root::core::PrepopulatedField),
3116 prepopulated.iter().map(|(target, sources)| {
3117 let target = target.as_str();
3118 let source_lits = sources.iter().map(String::as_str);
3119 quote!(#root::core::PrepopulatedField::new(#target, &[ #( #source_lits ),* ]))
3120 }),
3121 );
3122
3123 let raw_id_fields = admin
3124 .raw_id_fields
3125 .as_ref()
3126 .map(|(v, _)| v.as_slice())
3127 .unwrap_or(&[]);
3128 let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
3129
3130 let autocomplete_fields = admin
3131 .autocomplete_fields
3132 .as_ref()
3133 .map(|(v, _)| v.as_slice())
3134 .unwrap_or(&[]);
3135 let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
3136
3137 // #352 — list_select_related accepts "all" | "none" | "field, field, …".
3138 let list_select_related_tokens = match admin.list_select_related.as_deref() {
3139 None | Some("all") => quote!(#root::core::ListSelectRelated::All),
3140 Some("none") => quote!(#root::core::ListSelectRelated::None),
3141 Some(raw) => {
3142 let names: Vec<&str> = raw
3143 .split(',')
3144 .map(str::trim)
3145 .filter(|s| !s.is_empty())
3146 .collect();
3147 quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
3148 }
3149 };
3150
3151 // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
3152 // a Vec<(String, String)>. Empty / unset → no overrides.
3153 let formfield_pairs: Vec<(&str, &str)> = admin
3154 .formfield_overrides
3155 .as_ref()
3156 .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
3157 .unwrap_or_default();
3158 let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
3159 let field = *field;
3160 let widget = *widget;
3161 quote!((#field, #widget))
3162 });
3163
3164 let list_per_page = admin.list_per_page.unwrap_or(0);
3165
3166 let ordering_pairs = admin
3167 .ordering
3168 .as_ref()
3169 .map(|(v, _)| v.as_slice())
3170 .unwrap_or(&[]);
3171 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
3172 let name = name.as_str();
3173 let desc = *desc;
3174 quote!((#name, #desc))
3175 });
3176
3177 quote! {
3178 ::core::option::Option::Some({
3179 const ADMIN: &#root::core::AdminConfig = &{
3180 #root::core::AdminConfig::from_parts(#root::core::AdminConfigParts {
3181 list_display: &[ #( #list_display_lits ),* ],
3182 search_fields: &[ #( #search_fields_lits ),* ],
3183 list_per_page: #list_per_page,
3184 ordering: &[ #( #ordering_tokens ),* ],
3185 readonly_fields: &[ #( #readonly_fields_lits ),* ],
3186 list_filter: &[ #( #list_filter_lits ),* ],
3187 actions: &[ #( #actions_lits ),* ],
3188 fieldsets: #fieldset_tokens,
3189 list_display_links: &[ #( #list_display_links_lits ),* ],
3190 search_help_text: #search_help_text,
3191 actions_on_top: #actions_on_top,
3192 actions_on_bottom: #actions_on_bottom,
3193 date_hierarchy: #date_hierarchy,
3194 prepopulated_fields: #prepopulated_tokens,
3195 raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
3196 autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
3197 list_select_related: #list_select_related_tokens,
3198 formfield_overrides: &[ #( #formfield_tokens ),* ],
3199 })
3200 };
3201 ADMIN
3202 })
3203 }
3204}
3205
3206fn inherent_impl_tokens(
3207 struct_name: &syn::Ident,
3208 fields: &CollectedFields,
3209 primary_key: Option<&(syn::Ident, String)>,
3210 column_consts: &TokenStream2,
3211 audited_fields: Option<&[&ColumnEntry]>,
3212 upsert_unique: Option<&[String]>,
3213 manager_fns: &[syn::Ident],
3214) -> TokenStream2 {
3215 let root = rustango_root();
3216 // Audit-emit fragments threaded into write paths. Non-empty only
3217 // when the model carries `#[rustango(audit(...))]`. They reborrow
3218 // `_executor` (a `&mut PgConnection` for audited models — the
3219 // macro switches the signature below) so the data write and the
3220 // audit INSERT both run on the same caller-supplied connection.
3221 let executor_passes_to_data_write = if audited_fields.is_some() {
3222 quote!(&mut *_executor)
3223 } else {
3224 quote!(_executor)
3225 };
3226 let executor_param = if audited_fields.is_some() {
3227 quote!(_executor: &mut #root::sql::sqlx::PgConnection)
3228 } else {
3229 quote!(_executor: _E)
3230 };
3231 let executor_generics = if audited_fields.is_some() {
3232 quote!()
3233 } else {
3234 quote!(<'_c, _E>)
3235 };
3236 let executor_where = if audited_fields.is_some() {
3237 quote!()
3238 } else {
3239 quote! {
3240 where
3241 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
3242 }
3243 };
3244 // Audited `_on` methods take `&mut PgConnection`; `&PgPool` is an Executor for the rest.
3245 // One transaction: the before-image's FOR UPDATE must hold through
3246 // the UPDATE and its audit row.
3247 let pool_to_save_on = if audited_fields.is_some() {
3248 // Any failure, COMMIT included, rolls the INSERT back: put the pk and
3249 // stamps back too, or a retry takes the UPDATE path and writes nothing.
3250 let restored: Vec<&syn::Ident> = primary_key
3251 .map(|(pk, _)| pk)
3252 .into_iter()
3253 .chain(&fields.auto_now_idents)
3254 .collect();
3255 let prev: Vec<syn::Ident> = (0..restored.len())
3256 .map(|i| quote::format_ident!("__rustango_save_prev_{}", i))
3257 .collect();
3258 quote! {
3259 #( let #prev = ::core::clone::Clone::clone(&self.#restored); )*
3260 let _saved: ::core::result::Result<u64, #root::sql::ExecError> = async {
3261 let mut _tx = pool.begin().await?;
3262 let _affected = self.save_on(&mut *_tx).await?;
3263 _tx.commit().await?;
3264 ::core::result::Result::Ok(_affected)
3265 }
3266 .await;
3267 if _saved.is_err() {
3268 #( self.#restored = #prev; )*
3269 }
3270 _saved
3271 }
3272 } else {
3273 quote!(self.save_on(pool).await)
3274 };
3275 // One transaction so the write and its audit row commit together.
3276 let pool_to_insert_on = if audited_fields.is_some() {
3277 // A failed COMMIT leaves the RETURNING values on `self`; put them back.
3278 let assigned: Vec<syn::Ident> = fields
3279 .auto_field_idents
3280 .iter()
3281 .map(|(ident, _, _)| ident.clone())
3282 .collect();
3283 restore_auto_now_unless(
3284 &assigned,
3285 "e!(::core::result::Result<(), #root::sql::ExecError>),
3286 "e!(_saved.is_ok()),
3287 "e! {
3288 let mut _tx = pool.begin().await?;
3289 self.insert_on(&mut *_tx).await?;
3290 _tx.commit().await?;
3291 ::core::result::Result::Ok(())
3292 },
3293 )
3294 } else {
3295 quote!(self.insert_on(pool).await)
3296 };
3297 let pool_to_delete_on = if audited_fields.is_some() {
3298 quote! {
3299 let mut _tx = pool.begin().await?;
3300 let _affected = self.delete_on(&mut *_tx).await?;
3301 _tx.commit().await?;
3302 ::core::result::Result::Ok(_affected)
3303 }
3304 } else {
3305 quote!(self.delete_on(pool).await)
3306 };
3307 // Past the bind cap one statement per chunk, all in one transaction.
3308 let bulk_columns = fields.bulk_columns_all.len();
3309 let one_statement = if audited_fields.is_some() {
3310 quote!()
3311 } else {
3312 quote! {
3313 if rows.len() <= _per {
3314 return Self::bulk_insert_on(rows, pool).await;
3315 }
3316 }
3317 };
3318 let pool_to_bulk_insert_on = |chunks: TokenStream2| {
3319 quote! {
3320 let _per = #root::sql::__macro_internals::bulk_insert_rows_per_statement(#bulk_columns);
3321 #one_statement
3322 let mut _tx = pool.begin().await?;
3323 for _chunk in rows.#chunks(_per) {
3324 Self::bulk_insert_on(_chunk, &mut *_tx).await?;
3325 }
3326 _tx.commit().await?;
3327 ::core::result::Result::Ok(())
3328 }
3329 };
3330 let pool_to_bulk_insert_mut = pool_to_bulk_insert_on(quote!(chunks_mut));
3331 let pool_to_bulk_insert_on = pool_to_bulk_insert_on(quote!(chunks));
3332 // Pre-existing bug surfaced by batch 22's first audited Auto<T>
3333 // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
3334 // directly, but `upsert_on` for audited models takes
3335 // `&mut PgConnection` (the audit emit needs a real connection).
3336 // Add the missing acquire shim to keep audited Auto-PK upsert
3337 // compiling.
3338 let pool_to_upsert_on = if audited_fields.is_some() {
3339 quote! {
3340 let mut _tx = pool.begin().await?;
3341 self.upsert_on(&mut *_tx).await?;
3342 _tx.commit().await?;
3343 ::core::result::Result::Ok(())
3344 }
3345 } else {
3346 quote!(self.upsert_on(pool).await)
3347 };
3348
3349 // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
3350 // (audit-on-connection over &Pool needs a bi-dialect transaction
3351 // helper, deferred). Two body shapes:
3352 // - has_auto: build InsertQuery skipping Auto::Unset columns,
3353 // request Auto cols in `returning`, dispatch via
3354 // `insert_returning_pool`, then on the returned `PgRow` /
3355 // `MySqlAutoId(id)` enum — pull each Auto field from the PG
3356 // row OR drop the single i64 into the first Auto field on MySQL
3357 // (multi-Auto models on MySQL error at runtime since
3358 // `LAST_INSERT_ID()` only reports one)
3359 // - non-Auto: build InsertQuery with explicit columns/values and
3360 // call `insert_pool` (no returning needed)
3361 // pool_insert_method body for the audited Auto-PK case is moved
3362 // to after audit_pair_tokens / audit_pk_to_string (they live
3363 // ~150 lines below). This block keeps the non-audited and
3364 // non-Auto branches in place — the audited Auto-PK arm is
3365 // computed below and merged via the dispatch helper variable.
3366 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
3367 // Audited models with explicit (non-Auto) PKs go through
3368 // the non-Auto insert path below — the audit emit is one
3369 // round-trip after the INSERT inside the same tx via
3370 // audit::save_one_with_audit? No, INSERT semantics
3371 // differ. For non-Auto PK + audited, route through a
3372 // dedicated insert + audit emit on the same tx, but defer
3373 // the macro emission to the audit-bundle-aware block below
3374 // — this `quote!()` placeholder gets overwritten there.
3375 quote!()
3376 } else if audited_fields.is_some() && fields.has_auto {
3377 // Audited Auto-PK insert_pool — assembled after the audit
3378 // bundles. Placeholder; real emission below.
3379 quote!()
3380 } else if fields.has_auto {
3381 let pushes = &fields.insert_pushes;
3382 let returning_cols = &fields.returning_cols;
3383 // A row not inserted was never saved, so a Rust-filled value must not stay (#1937).
3384 let filled = &fields.rust_filled_idents;
3385 let was_unset: Vec<syn::Ident> = filled
3386 .iter()
3387 .map(|i| quote::format_ident!("__{}_was_unset", ident_name(i)))
3388 .collect();
3389 let insert_or_ignore_call = quote! {
3390 #( let #was_unset = matches!(&self.#filled, #root::sql::Auto::Unset); )*
3391 let mut _columns: ::std::vec::Vec<&'static str> =
3392 ::std::vec::Vec::new();
3393 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3394 ::std::vec::Vec::new();
3395 #( #pushes )*
3396 let _query = #root::core::InsertQuery::new(
3397 <Self as #root::core::Model>::SCHEMA,
3398 _columns,
3399 _values,
3400 );
3401 let _result = #root::sql::insert_or_ignore(pool, &_query).await;
3402 if !matches!(_result, ::core::result::Result::Ok(true)) {
3403 #( if #was_unset { self.#filled = #root::sql::Auto::Unset; } )*
3404 }
3405 _result
3406 };
3407 // When every `Auto<T>` field is filled Rust-side
3408 // (`default_uuid_v7`, issue #823), there is no column to read
3409 // back from the database — `returning_cols` is empty. Route
3410 // through plain `insert_pool` instead of
3411 // `insert_returning_pool` to skip the redundant RETURNING /
3412 // LAST_INSERT_ID round-trip.
3413 if fields.returning_cols.is_empty() {
3414 quote! {
3415 /// Insert this row against either backend. Every
3416 /// `Auto<T>` PK on this model is filled Rust-side
3417 /// (e.g. `default_uuid_v7`) before binding, so no
3418 /// RETURNING round-trip is needed.
3419 ///
3420 /// # Errors
3421 /// As [`Self::insert`].
3422 pub async fn insert_pool(
3423 &mut self,
3424 pool: &#root::sql::Pool,
3425 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3426 let mut _columns: ::std::vec::Vec<&'static str> =
3427 ::std::vec::Vec::new();
3428 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3429 ::std::vec::Vec::new();
3430 #( #pushes )*
3431 let _query = #root::core::InsertQuery::new(
3432 <Self as #root::core::Model>::SCHEMA,
3433 _columns,
3434 _values,
3435 );
3436 #root::sql::insert_pool(pool, &_query).await
3437 }
3438
3439 /// Eloquent `Model::insertOrIgnore()` — same shape
3440 /// as the auto-PK branch above. Returns `Ok(true)`
3441 /// when inserted, `Ok(false)` when a conflict caused
3442 /// the INSERT to silently skip.
3443 ///
3444 /// # Errors
3445 /// As [`Self::insert`].
3446 pub async fn insert_or_ignore(
3447 &mut self,
3448 pool: &#root::sql::Pool,
3449 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3450 #insert_or_ignore_call
3451 }
3452 }
3453 } else {
3454 quote! {
3455 /// Insert this row against either backend, populating any
3456 /// `Auto<T>` PK from the auto-assigned value.
3457 ///
3458 /// # Errors
3459 /// As [`Self::insert`].
3460 pub async fn insert_pool(
3461 &mut self,
3462 pool: &#root::sql::Pool,
3463 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3464 let mut _columns: ::std::vec::Vec<&'static str> =
3465 ::std::vec::Vec::new();
3466 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3467 ::std::vec::Vec::new();
3468 #( #pushes )*
3469 let _query = #root::core::InsertQuery::new(
3470 <Self as #root::core::Model>::SCHEMA,
3471 _columns,
3472 _values,
3473 )
3474 .returning(::std::vec![ #( #returning_cols ),* ]);
3475 let _result = #root::sql::insert_returning_pool(
3476 pool, &_query,
3477 ).await?;
3478 #root::sql::apply_auto_pk(_result, self)
3479 }
3480
3481 /// Eloquent `Model::insertOrIgnore()` — INSERT this
3482 /// row or silently skip on unique-constraint
3483 /// violation. Returns `Ok(true)` when a row was
3484 /// inserted, `Ok(false)` when a conflict caused the
3485 /// INSERT to silently skip.
3486 ///
3487 /// **Caveat on auto-PK models**: when the row is
3488 /// skipped (conflict), this instance's `Auto<T>`
3489 /// fields stay `Unset` — no PK is back-populated
3490 /// because the server didn't auto-assign one. For
3491 /// "insert then read back the PK or the existing
3492 /// row's PK", use the `upsert` family or
3493 /// `get_or_create`.
3494 ///
3495 /// # Errors
3496 /// As [`Self::insert`].
3497 pub async fn insert_or_ignore(
3498 &mut self,
3499 pool: &#root::sql::Pool,
3500 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3501 #insert_or_ignore_call
3502 }
3503 }
3504 }
3505 } else {
3506 let insert_columns = &fields.insert_columns;
3507 let insert_values = &fields.insert_values;
3508 quote! {
3509 /// Insert this row into its table against either backend.
3510 /// Equivalent to [`Self::insert`] but takes
3511 /// [`#root::sql::Pool`].
3512 ///
3513 /// # Errors
3514 /// As [`Self::insert`].
3515 pub async fn insert_pool(
3516 &self,
3517 pool: &#root::sql::Pool,
3518 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3519 let _query = #root::core::InsertQuery::new(
3520 <Self as #root::core::Model>::SCHEMA,
3521 ::std::vec![ #( #insert_columns ),* ],
3522 ::std::vec![ #( #insert_values ),* ],
3523 );
3524 #root::sql::insert_pool(pool, &_query).await
3525 }
3526
3527 /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3528 /// or silently skip on unique-constraint violation. Maps
3529 /// to per-dialect "INSERT ... DO NOTHING on conflict":
3530 /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3531 /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL an
3532 /// `ON DUPLICATE KEY UPDATE` that leaves the row as is.
3533 ///
3534 /// Returns `Ok(true)` when a row was inserted,
3535 /// `Ok(false)` when a conflict caused the INSERT to
3536 /// silently skip.
3537 ///
3538 /// Use for "create-if-absent" patterns where you don't
3539 /// need the row back. For "find-or-create with the row
3540 /// returned", use the queryset-level
3541 /// `crate::sql::get_or_create` free function.
3542 ///
3543 /// # Errors
3544 /// As [`Self::insert`], plus any dialect-specific
3545 /// translation error from the ConflictClause writer.
3546 pub async fn insert_or_ignore(
3547 &self,
3548 pool: &#root::sql::Pool,
3549 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3550 let _query = #root::core::InsertQuery::new(
3551 <Self as #root::core::Model>::SCHEMA,
3552 ::std::vec![ #( #insert_columns ),* ],
3553 ::std::vec![ #( #insert_values ),* ],
3554 );
3555 #root::sql::insert_or_ignore(pool, &_query).await
3556 }
3557 }
3558 };
3559
3560 // The tracked (column, JSON value) pairs, spliced only into
3561 // `__rustango_audit_pairs`. Empty when the model isn't audited.
3562 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3563 .map(|tracked| {
3564 tracked
3565 .iter()
3566 .map(|c| {
3567 let column_lit = c.column.as_str();
3568 let ident = &c.ident;
3569 quote! {
3570 (
3571 #column_lit,
3572 #root::core::__json_value(&self.#ident),
3573 )
3574 }
3575 })
3576 .collect()
3577 })
3578 .unwrap_or_default();
3579 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3580 if fields.pk_is_auto {
3581 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3582 } else {
3583 quote!(::std::format!("{}", &self.#pk_ident))
3584 }
3585 } else {
3586 quote!(::std::string::String::new())
3587 };
3588 // `__rustango_audit_entry`: the one place a snapshot `PendingEntry`
3589 // is built, so every audited write path records the same shape.
3590 let delete_audited_body = if primary_key.is_some() {
3591 quote! {
3592 ::std::boxed::Box::pin(#root::audit::delete_many_with_audit::<Self>(
3593 pool,
3594 query,
3595 |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3596 ))
3597 }
3598 } else {
3599 quote! {
3600 let _ = (pool, query);
3601 ::std::boxed::Box::pin(async {
3602 ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3603 table: <Self as #root::core::Model>::SCHEMA.table,
3604 })
3605 })
3606 }
3607 };
3608 let cascade_audited_body = if primary_key.is_some() {
3609 quote! {
3610 ::std::boxed::Box::pin(#root::audit::cascade_rows::<Self>(
3611 tx,
3612 <Self as #root::core::Model>::SCHEMA,
3613 rows,
3614 |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Delete),
3615 ))
3616 }
3617 } else {
3618 quote! {
3619 let _ = (tx, rows);
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 update_audited_body = if primary_key.is_some() {
3628 quote! {
3629 ::std::boxed::Box::pin(#root::audit::update_many_with_audit::<Self>(
3630 pool,
3631 query,
3632 move |_r: &Self| _r.__rustango_audit_entry(op),
3633 ))
3634 }
3635 } else {
3636 quote! {
3637 let _ = (pool, query, op);
3638 ::std::boxed::Box::pin(async {
3639 ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
3640 table: <Self as #root::core::Model>::SCHEMA.table,
3641 })
3642 })
3643 }
3644 };
3645 let audit_entry_method = if audited_fields.is_some() {
3646 let pairs = audit_pair_tokens.iter();
3647 let pk_str = audit_pk_to_string.clone();
3648 quote! {
3649 /// This row's tracked `(column, value)` pairs.
3650 #[doc(hidden)]
3651 pub fn __rustango_audit_pairs(
3652 &self,
3653 ) -> ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> {
3654 ::std::vec![ #( #pairs ),* ]
3655 }
3656
3657 /// This row's audit PK.
3658 #[doc(hidden)]
3659 pub fn __rustango_audit_pk(&self) -> ::std::string::String {
3660 #pk_str
3661 }
3662
3663 /// Snapshot audit entry for `operation` on this row.
3664 #[doc(hidden)]
3665 pub fn __rustango_audit_entry(
3666 &self,
3667 operation: #root::audit::AuditOp,
3668 ) -> #root::audit::PendingEntry {
3669 #root::audit::PendingEntry {
3670 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3671 entity_pk: self.__rustango_audit_pk(),
3672 operation,
3673 source: #root::audit::current_source(),
3674 changes: #root::audit::snapshot_changes(&self.__rustango_audit_pairs()),
3675 }
3676 }
3677
3678 /// Audited bulk `UPDATE`, behind `Model::__rustango_audited_update`.
3679 #[doc(hidden)]
3680 pub fn __rustango_update_audited<'a>(
3681 pool: &'a #root::sql::Pool,
3682 query: &'a #root::core::UpdateQuery,
3683 op: #root::audit::AuditOp,
3684 ) -> ::std::pin::Pin<::std::boxed::Box<
3685 dyn ::core::future::Future<
3686 Output = ::core::result::Result<u64, #root::sql::ExecError>,
3687 > + ::core::marker::Send + 'a,
3688 >> {
3689 #update_audited_body
3690 }
3691
3692 /// Audited `create` entry for a new row, behind `Model::__rustango_audited_create`.
3693 #[doc(hidden)]
3694 pub fn __rustango_create_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_create::<Self>(
3703 tx,
3704 <Self as #root::core::Model>::SCHEMA,
3705 pk,
3706 |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Create),
3707 ))
3708 }
3709
3710 /// Audited one-row `update` entry, behind `Model::__rustango_audited_update_record`.
3711 #[doc(hidden)]
3712 pub fn __rustango_update_record_audited<'a, 't>(
3713 tx: &'a mut #root::sql::PoolTx<'t>,
3714 pk: #root::core::SqlValue,
3715 ) -> ::std::pin::Pin<::std::boxed::Box<
3716 dyn ::core::future::Future<
3717 Output = ::core::result::Result<(), #root::sql::ExecError>,
3718 > + ::core::marker::Send + 'a,
3719 >> {
3720 ::std::boxed::Box::pin(#root::audit::record_update::<Self>(
3721 tx,
3722 <Self as #root::core::Model>::SCHEMA,
3723 pk,
3724 |_r: &Self| _r.__rustango_audit_entry(#root::audit::AuditOp::Update),
3725 ))
3726 }
3727
3728 /// Rows an FK cascade will delete, behind `Model::__rustango_audited_cascade`.
3729 #[doc(hidden)]
3730 pub fn __rustango_cascade_audited<'a, 't>(
3731 tx: &'a mut #root::sql::PoolTx<'t>,
3732 rows: #root::core::WhereExpr,
3733 ) -> ::std::pin::Pin<::std::boxed::Box<
3734 dyn ::core::future::Future<
3735 Output = ::core::result::Result<
3736 ::std::vec::Vec<(#root::core::SqlValue, #root::audit::PendingEntry)>,
3737 #root::sql::ExecError,
3738 >,
3739 > + ::core::marker::Send + 'a,
3740 >> {
3741 #cascade_audited_body
3742 }
3743
3744 /// Audited bulk `DELETE`, behind `Model::__rustango_audited_delete`.
3745 #[doc(hidden)]
3746 pub fn __rustango_delete_audited<'a>(
3747 pool: &'a #root::sql::Pool,
3748 query: &'a #root::core::DeleteQuery,
3749 ) -> ::std::pin::Pin<::std::boxed::Box<
3750 dyn ::core::future::Future<
3751 Output = ::core::result::Result<u64, #root::sql::ExecError>,
3752 > + ::core::marker::Send + 'a,
3753 >> {
3754 #delete_audited_body
3755 }
3756 }
3757 } else {
3758 quote!()
3759 };
3760 // Builds `_audit_entry`; `written` is the soft-delete column's new
3761 // value, recorded in place of the stale `self` field.
3762 let make_entry = |op_path: TokenStream2, written: Option<TokenStream2>| -> TokenStream2 {
3763 if let (Some(v), Some(col)) = (written, fields.soft_delete_column.as_deref()) {
3764 quote! {
3765 let mut _audit_entry = self.__rustango_audit_entry(#op_path);
3766 _audit_entry.set_tracked(#col, #v);
3767 }
3768 } else {
3769 quote!(let _audit_entry = self.__rustango_audit_entry(#op_path);)
3770 }
3771 };
3772 let soft_deleted_json = quote!(#root::__serde_json::to_value(&_now)
3773 .unwrap_or(#root::__serde_json::Value::Null));
3774 let restored_json = quote!(#root::__serde_json::Value::Null);
3775 // `_on` emit; `affected` paths skip the row when nothing changed.
3776 let make_op_emit =
3777 |op_path: TokenStream2, written: Option<TokenStream2>, affected: bool| -> TokenStream2 {
3778 if audited_fields.is_none() {
3779 return quote!();
3780 }
3781 let entry = make_entry(op_path, written);
3782 let emit = quote! {
3783 #entry
3784 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3785 };
3786 if affected {
3787 quote!(if _affected > 0 { #emit })
3788 } else {
3789 emit
3790 }
3791 };
3792 let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create), None, false);
3793 let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete), None, true);
3794 let audit_softdelete_emit = make_op_emit(
3795 quote!(#root::audit::AuditOp::SoftDelete),
3796 Some(soft_deleted_json.clone()),
3797 true,
3798 );
3799 let audit_restore_emit = make_op_emit(
3800 quote!(#root::audit::AuditOp::Restore),
3801 Some(restored_json.clone()),
3802 true,
3803 );
3804 // `&Pool` update runner for soft_delete / restore: one tx with the
3805 // audit row when audited, a plain UPDATE otherwise.
3806 let make_pool_update = |op_path: TokenStream2, written: TokenStream2| -> TokenStream2 {
3807 if audited_fields.is_some() {
3808 let entry = make_entry(op_path, Some(written));
3809 quote! {
3810 #entry
3811 #root::audit::save_one_with_audit(pool, &_query, &_audit_entry).await
3812 }
3813 } else {
3814 quote!(#root::sql::update_pool(pool, &_query).await)
3815 }
3816 };
3817 let pool_softdelete_run =
3818 make_pool_update(quote!(#root::audit::AuditOp::SoftDelete), soft_deleted_json);
3819 let pool_restore_run = make_pool_update(quote!(#root::audit::AuditOp::Restore), restored_json);
3820
3821 // `save_pool(&Pool)` — emitted for every model with a PK. The
3822 // audited `save_pool` is built below on the diff path; both share
3823 // `save_partial_methods`, which differ only in how they run `_query`.
3824 let save_ret = quote!(::core::result::Result<u64, #root::sql::ExecError>);
3825 let save_keep = quote!(matches!(_saved, ::core::result::Result::Ok(n) if n > 0));
3826 let save_partial_methods = |pk_ident: &syn::Ident,
3827 pk_column_lit: &str,
3828 run: TokenStream2|
3829 -> TokenStream2 {
3830 let assignments = &fields.update_assignments;
3831 // Pushed in step with `update_assignments`: one column each.
3832 let assignment_cols = &fields.upsert_update_columns;
3833 let write = restore_auto_now_unless(
3834 &fields.auto_now_idents,
3835 &save_ret,
3836 &save_keep,
3837 "e! {
3838 // Build only the wanted assignments, so an unnamed
3839 // `auto_now` field is not stamped.
3840 let mut _filtered: ::std::vec::Vec<#root::core::Assignment> =
3841 ::std::vec::Vec::with_capacity(_wanted_cols.len());
3842 #(
3843 if _wanted_cols.contains(#assignment_cols) {
3844 _filtered.push(#assignments);
3845 }
3846 )*
3847 if _filtered.is_empty() {
3848 // All field names valid, but they all map to
3849 // non-assignable slots (PK column, computed/
3850 // virtual fields, relations without an
3851 // assignment). Same no-op as an empty list.
3852 #root::__tracing::warn!(
3853 target: "rustango::save_partial",
3854 model = _schema.name,
3855 "save_partial: every named field maps to a non-assignable column — no-op"
3856 );
3857 return ::core::result::Result::Ok(0);
3858 }
3859 let _query = #root::core::UpdateQuery::new(
3860 _schema,
3861 _filtered,
3862 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
3863 #pk_column_lit,
3864 #root::core::Op::Eq,
3865 ::core::convert::Into::<#root::core::SqlValue>::into(
3866 ::core::clone::Clone::clone(&self.#pk_ident)
3867 ),
3868 )),
3869 );
3870 #run
3871 },
3872 );
3873 quote! {
3874 /// Save (UPDATE) only the listed Rust-side fields,
3875 /// leaving every other column untouched. Issue #66.
3876 ///
3877 /// `fields` are Rust-side struct field names; the macro
3878 /// resolves each to its SQL column. Unknown field
3879 /// names return [`#root::core::QueryError::UnknownField`]
3880 /// wrapped in `ExecError::Query`. An empty list is a
3881 /// no-op (returns `Ok(())` and logs a `tracing::warn!`).
3882 /// On an audited model the audit diff covers only these fields.
3883 ///
3884 /// Use this when:
3885 /// * you only mutated a couple of fields on a wide row
3886 /// (avoid re-writing every column on every save), or
3887 /// * two writers diverged after their initial read and
3888 /// you want to preserve the other writer's changes to
3889 /// columns you didn't touch.
3890 ///
3891 /// Auto-PK models with an unset PK return
3892 /// [`#root::core::QueryError::UnknownField`] with
3893 /// field name `<pk>` — `save_partial` is an
3894 /// UPDATE-only path. Call [`Self::insert_pool`]
3895 /// (or [`Self::save_pool`] which dispatches based on
3896 /// PK state) for the INSERT case.
3897 ///
3898 /// # Errors
3899 /// As [`Self::save_pool`], plus `UnknownField` for
3900 /// unknown / empty / Auto-Unset cases.
3901 pub async fn save_partial(
3902 &mut self,
3903 fields: &[&str],
3904 pool: &#root::sql::Pool,
3905 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3906 if fields.is_empty() {
3907 #root::__tracing::warn!(
3908 target: "rustango::save_partial",
3909 model = <Self as #root::core::Model>::SCHEMA.name,
3910 "save_partial called with empty field list — no-op"
3911 );
3912 return ::core::result::Result::Ok(0);
3913 }
3914 let _schema = <Self as #root::core::Model>::SCHEMA;
3915 // Validate field names against the schema.
3916 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3917 ::std::collections::HashSet::with_capacity(fields.len());
3918 for f in fields {
3919 match _schema.field(f) {
3920 ::core::option::Option::Some(fs) => {
3921 _wanted_cols.insert(fs.column);
3922 }
3923 ::core::option::Option::None => {
3924 return ::core::result::Result::Err(
3925 #root::sql::ExecError::Query(
3926 #root::core::QueryError::UnknownField {
3927 model: _schema.name,
3928 field: (*f).to_owned(),
3929 }
3930 )
3931 );
3932 }
3933 }
3934 }
3935 #write
3936 }
3937
3938 /// Typed-column counterpart of [`Self::save_partial`] —
3939 /// issue #67. `fields` is a tuple of [`Column`]
3940 /// constants whose `Model` matches `Self`; typos and
3941 /// model mismatches surface at *compile time*
3942 /// (`Author::name` inside a `Post::save_partial_typed`
3943 /// call is a type error, no runtime check).
3944 ///
3945 /// ```ignore
3946 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3947 /// ```
3948 ///
3949 /// Lowers to [`Self::save_partial`] under the hood — the
3950 /// tuple is reduced to a `&[&str]` slice of Rust-side
3951 /// field names and forwarded.
3952 ///
3953 /// [`Column`]: #root::core::Column
3954 ///
3955 /// # Errors
3956 /// As [`Self::save_partial`].
3957 pub async fn save_partial_typed<
3958 L: #root::core::TypedFieldList<Self>,
3959 >(
3960 &mut self,
3961 fields: L,
3962 pool: &#root::sql::Pool,
3963 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3964 let _names = fields.rust_field_names();
3965 let _refs: ::std::vec::Vec<&str> =
3966 _names.iter().copied().collect();
3967 self.save_partial(&_refs, pool).await
3968 }
3969 }
3970 };
3971 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3972 let pk_column_lit = pk_col.as_str();
3973 let assignments = &fields.update_assignments;
3974 if audited_fields.is_some() {
3975 quote!()
3976 } else {
3977 let partial = save_partial_methods(
3978 pk_ident,
3979 pk_column_lit,
3980 quote!(#root::sql::update_pool(pool, &_query).await),
3981 );
3982 let dispatch_unset = if fields.pk_is_auto {
3983 quote! {
3984 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3985 return self.insert_pool(pool).await.map(|()| 1u64);
3986 }
3987 }
3988 } else {
3989 quote!()
3990 };
3991 let write = restore_auto_now_unless(
3992 &fields.auto_now_idents,
3993 &save_ret,
3994 &save_keep,
3995 "e! {
3996 let _query = #root::core::UpdateQuery::new(
3997 <Self as #root::core::Model>::SCHEMA,
3998 ::std::vec![ #( #assignments ),* ],
3999 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
4000 #pk_column_lit,
4001 #root::core::Op::Eq,
4002 ::core::convert::Into::<#root::core::SqlValue>::into(
4003 ::core::clone::Clone::clone(&self.#pk_ident)
4004 ),
4005 )),
4006 );
4007 let _affected = #root::sql::update_pool(pool, &_query).await?;
4008 ::core::result::Result::Ok(_affected)
4009 },
4010 );
4011 quote! {
4012 /// Save this row to its table against either backend.
4013 /// `INSERT` when the `Auto<T>` PK is `Unset`, else
4014 /// `UPDATE` keyed on the PK; global scopes do not apply.
4015 ///
4016 /// # Errors
4017 /// As [`Self::save`].
4018 pub async fn save_pool(
4019 &mut self,
4020 pool: &#root::sql::Pool,
4021 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4022 #dispatch_unset
4023 #write
4024 }
4025
4026 #partial
4027 }
4028 }
4029 } else {
4030 quote!()
4031 };
4032
4033 // Audited `insert_pool` (overrides the placeholder set higher up
4034 // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
4035 // audited models get insert_pool routing through
4036 // audit::insert_one_with_audit (per-backend tx wraps INSERT
4037 // + auto-PK readback + audit emit). Snapshot-style audit (the
4038 // PendingEntry's `changes` carries post-write field values).
4039 let pool_insert_method = if audited_fields.is_some() {
4040 if let Some(_) = primary_key {
4041 let pushes = if fields.has_auto {
4042 fields.insert_pushes.clone()
4043 } else {
4044 // For non-Auto-PK models, the macro normally builds
4045 // {columns, values} from fields.insert_columns +
4046 // fields.insert_values rather than insert_pushes.
4047 // Map those into the pushes shape.
4048 fields
4049 .insert_columns
4050 .iter()
4051 .zip(&fields.insert_values)
4052 .map(|(col, val)| {
4053 quote! {
4054 _columns.push(#col);
4055 _values.push(#val);
4056 }
4057 })
4058 .collect()
4059 };
4060 // Rust-filled Auto PKs (`default_uuid_v7`) leave the list empty (#1934).
4061 let returning_cols: Vec<proc_macro2::TokenStream> = if !fields.returning_cols.is_empty()
4062 {
4063 fields.returning_cols.clone()
4064 } else {
4065 // Non-Auto-PK: still need RETURNING something for the
4066 // audit helper's contract (it errors on empty
4067 // returning). Return the PK column so the audit row
4068 // can carry the assigned PK back. Some non-Auto PKs
4069 // are server-side-default (e.g. UUIDv4 default), so
4070 // RETURNING is genuinely useful.
4071 primary_key
4072 .map(|(_, col)| {
4073 let lit = col.as_str();
4074 vec![quote!(#lit)]
4075 })
4076 .unwrap_or_default()
4077 };
4078 quote! {
4079 /// Insert this row against either backend with audit
4080 /// emission inside the same transaction. Bi-dialect
4081 /// counterpart of [`Self::insert`] for audited models.
4082 ///
4083 /// Snapshot-style audit (post-write field values).
4084 ///
4085 /// # Errors
4086 /// As [`Self::insert`].
4087 pub async fn insert_pool(
4088 &mut self,
4089 pool: &#root::sql::Pool,
4090 ) -> ::core::result::Result<(), #root::sql::ExecError> {
4091 let mut _columns: ::std::vec::Vec<&'static str> =
4092 ::std::vec::Vec::new();
4093 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
4094 ::std::vec::Vec::new();
4095 #( #pushes )*
4096 let _query = #root::core::InsertQuery::new(
4097 <Self as #root::core::Model>::SCHEMA,
4098 _columns,
4099 _values,
4100 )
4101 .returning(::std::vec![ #( #returning_cols ),* ]);
4102 #root::audit::insert_one_with_audit(pool, &_query, self, |_m: &Self| {
4103 _m.__rustango_audit_entry(#root::audit::AuditOp::Create)
4104 })
4105 .await
4106 }
4107 }
4108 } else {
4109 quote!()
4110 }
4111 } else {
4112 // Keep the non-audited pool_insert_method we built earlier.
4113 pool_insert_method
4114 };
4115
4116 // Update audited save_pool: now that insert_pool is wired for
4117 // audited Auto-PK models, save_pool can dispatch Auto::Unset →
4118 // insert_pool. Non-audited save_pool already does this.
4119 // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
4120 // Replaces the snapshot-only emission with a per-backend transaction
4121 // body that:
4122 // 1. SELECTs the tracked columns by PK (typed Row::try_get per
4123 // column), capturing BEFORE values
4124 // 2. compiles the UPDATE via pool.dialect() and runs it on the tx
4125 // 3. builds AFTER pairs from &self
4126 // 4. diffs BEFORE/AFTER, emits one PendingEntry with
4127 // AuditOp::Update + diff_changes(...) on the same tx connection
4128 // 5. commits
4129 //
4130 // Per-backend arms inline the SQL string + placeholder shape, then
4131 // share the `audit_before_pair_tokens` decoder block (Row::try_get
4132 // is polymorphic over Row type — the same tokens work against
4133 // PgRow and MySqlRow as long as the field's Rust type implements
4134 // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
4135 // primitives + chrono/uuid/serde_json::Value all do).
4136 let pool_save_method = if let Some(tracked) = audited_fields {
4137 if let Some((pk_ident, pk_col)) = primary_key {
4138 let pk_column_lit = pk_col.as_str();
4139 let partial = save_partial_methods(
4140 pk_ident,
4141 pk_column_lit,
4142 quote! {
4143 self.__rustango_save_with_diff(
4144 pool,
4145 &_query,
4146 ::core::option::Option::Some(&_wanted_cols),
4147 )
4148 .await
4149 },
4150 );
4151 // Per-tracked-column BEFORE-pair token list. Each entry
4152 // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
4153 // The Row alias resolves to PgRow / MySqlRow per call site,
4154 // so the same template generates both the PG and MySQL bodies.
4155 let mk_before_pairs =
4156 |getter: &dyn Fn(&ColumnEntry) -> TokenStream2| -> Vec<TokenStream2> {
4157 tracked
4158 .iter()
4159 .map(|c| {
4160 let getter = getter(c);
4161 let column_lit = c.column.as_str();
4162 let value_ty = &c.value_ty;
4163 quote! {
4164 (
4165 #column_lit,
4166 match #getter::<#value_ty>(
4167 _audit_before_row, #column_lit,
4168 ) {
4169 ::core::result::Result::Ok(v) => {
4170 #root::core::__json_value(&v)
4171 }
4172 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
4173 },
4174 )
4175 }
4176 })
4177 .collect()
4178 };
4179 let before_pairs_pg = mk_before_pairs(&|_| quote!(#root::sql::try_get_returning));
4180 let before_pairs_my = mk_before_pairs(&|c| {
4181 if c.uuid_column {
4182 quote!(#root::sql::try_get_flat_my)
4183 } else {
4184 quote!(#root::sql::try_get_returning_my)
4185 }
4186 });
4187 let before_pairs_sqlite =
4188 mk_before_pairs(&|_| quote!(#root::sql::try_get_returning_sqlite));
4189 let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
4190 let pk_value_for_bind = if fields.pk_is_auto {
4191 quote!(self.#pk_ident.get().copied().unwrap_or_default())
4192 } else {
4193 quote!(::core::clone::Clone::clone(&self.#pk_ident))
4194 };
4195 let assignments = &fields.update_assignments;
4196 let unset_dispatch = if fields.has_auto {
4197 quote! {
4198 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
4199 return self.insert_pool(pool).await.map(|()| 1u64);
4200 }
4201 }
4202 } else {
4203 quote!()
4204 };
4205 let write = restore_auto_now_unless(
4206 &fields.auto_now_idents,
4207 &save_ret,
4208 &save_keep,
4209 "e! {
4210 let _query = #root::core::UpdateQuery::new(
4211 <Self as #root::core::Model>::SCHEMA,
4212 ::std::vec![ #( #assignments ),* ],
4213 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
4214 #pk_column_lit,
4215 #root::core::Op::Eq,
4216 ::core::convert::Into::<#root::core::SqlValue>::into(
4217 ::core::clone::Clone::clone(&self.#pk_ident)
4218 ),
4219 )),
4220 );
4221 self.__rustango_save_with_diff(pool, &_query, ::core::option::Option::None)
4222 .await
4223 },
4224 );
4225 quote! {
4226 /// Save this row against either backend with audit
4227 /// emission (diff-style: BEFORE+AFTER) inside the
4228 /// same transaction. Auto::Unset PK routes to
4229 /// insert_pool. Bi-dialect counterpart of
4230 /// [`Self::save`] for audited models.
4231 ///
4232 /// The audit row's `changes` JSON contains one
4233 /// `{ "field": { "before": …, "after": … } }` entry
4234 /// per tracked column whose value actually changed
4235 /// — same shape as the existing &PgPool save() emits.
4236 /// Keyed on the PK; global scopes do not apply.
4237 ///
4238 /// # Errors
4239 /// As [`Self::save`].
4240 pub async fn save_pool(
4241 &mut self,
4242 pool: &#root::sql::Pool,
4243 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4244 #unset_dispatch
4245 #write
4246 }
4247
4248 /// Run `query` and write its diff entry, limited to `only` columns.
4249 #[doc(hidden)]
4250 async fn __rustango_save_with_diff(
4251 &self,
4252 pool: &#root::sql::Pool,
4253 query: &#root::core::UpdateQuery,
4254 only: ::core::option::Option<&::std::collections::HashSet<&'static str>>,
4255 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4256 let mut _after_pairs = self.__rustango_audit_pairs();
4257 if let ::core::option::Option::Some(only) = only {
4258 _after_pairs.retain(|(col, _)| only.contains(col));
4259 }
4260 let _before_query = #root::audit::before_image_query(
4261 <Self as #root::core::Model>::SCHEMA,
4262 #pk_column_lit,
4263 ::core::convert::Into::<#root::core::SqlValue>::into(
4264 #pk_value_for_bind,
4265 ),
4266 &[ #( #tracked_cols ),* ],
4267 );
4268 #root::audit::save_one_with_diff(
4269 pool,
4270 query,
4271 &_before_query,
4272 <Self as #root::core::Model>::SCHEMA.table,
4273 self.__rustango_audit_pk(),
4274 _after_pairs,
4275 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
4276 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
4277 |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
4278 ).await
4279 }
4280
4281 #partial
4282 }
4283 } else {
4284 quote!()
4285 }
4286 } else {
4287 pool_save_method
4288 };
4289
4290 // `delete_pool(&Pool)` — emitted for every model with a PK. Two
4291 // body shapes:
4292 // - non-audited: simple dispatch through `sql::delete_pool`
4293 // - audited: routes through `audit::delete_one_with_audit`,
4294 // which opens a per-backend transaction wrapping DELETE +
4295 // audit emit so the data write and audit row commit atomically.
4296 let pool_delete_method = {
4297 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
4298 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
4299 if let Some(pk_ident) = pk_ident_for_pool {
4300 if audited_fields.is_some() {
4301 quote! {
4302 /// Delete this row against either backend with audit
4303 /// emission inside the same transaction. Bi-dialect
4304 /// counterpart of [`Self::delete`] for audited models.
4305 /// Keyed on the PK; global scopes do not apply.
4306 ///
4307 /// # Errors
4308 /// As [`Self::delete`].
4309 pub async fn delete_pool(
4310 &self,
4311 pool: &#root::sql::Pool,
4312 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4313 let _query = #root::core::DeleteQuery::by_pk(
4314 <Self as #root::core::Model>::SCHEMA,
4315 #pk_column_lit,
4316 ::core::convert::Into::<#root::core::SqlValue>::into(
4317 ::core::clone::Clone::clone(&self.#pk_ident)
4318 ),
4319 );
4320 let _audit_entry =
4321 self.__rustango_audit_entry(#root::audit::AuditOp::Delete);
4322 #root::audit::delete_one_with_audit(
4323 pool, &_query, &_audit_entry,
4324 ).await
4325 }
4326 }
4327 } else {
4328 quote! {
4329 /// Delete the row identified by this instance's primary key
4330 /// against either backend. Equivalent to [`Self::delete`] but
4331 /// takes [`#root::sql::Pool`] and dispatches per backend.
4332 /// Global scopes do not apply.
4333 ///
4334 /// # Errors
4335 /// As [`Self::delete`].
4336 pub async fn delete_pool(
4337 &self,
4338 pool: &#root::sql::Pool,
4339 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4340 let _query = #root::core::DeleteQuery::by_pk(
4341 <Self as #root::core::Model>::SCHEMA,
4342 #pk_column_lit,
4343 ::core::convert::Into::<#root::core::SqlValue>::into(
4344 ::core::clone::Clone::clone(&self.#pk_ident)
4345 ),
4346 );
4347 #root::sql::delete_pool(pool, &_query).await
4348 }
4349 }
4350 }
4351 } else {
4352 quote!()
4353 }
4354 };
4355
4356 // Bulk writers on audited models write one audit row per affected
4357 // row, in the write's transaction (#1747).
4358 let (bulk_delete_run, destroy_run, bulk_update_run, truncate_run) = if audited_fields.is_some()
4359 {
4360 (
4361 quote!(#root::audit::delete_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4362 _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4363 })
4364 .await),
4365 quote!(#root::audit::delete_each_with_audit::<Self>(pool, &_queries, |_r: &Self| {
4366 _r.__rustango_audit_entry(#root::audit::AuditOp::Delete)
4367 })
4368 .await),
4369 quote!(#root::audit::update_many_with_audit::<Self>(pool, &_query, |_r: &Self| {
4370 _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
4371 })
4372 .await),
4373 quote! {
4374 let _table = <Self as #root::core::Model>::SCHEMA.table;
4375 let _sql = #root::sql::truncate_sql(pool.dialect(), _table);
4376 #root::audit::truncate_with_audit(pool, _table, &_sql).await
4377 },
4378 )
4379 } else {
4380 (
4381 quote!(#root::sql::delete_pool(pool, &_query).await),
4382 quote!(#root::sql::__rustango_delete_all_or_none(pool, _queries).await),
4383 quote!(#root::sql::update_pool(pool, &_query).await),
4384 quote!(#root::sql::truncate(pool, <Self as #root::core::Model>::SCHEMA).await),
4385 )
4386 };
4387
4388 // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
4389 // matching this instance's PK and overwrite the in-memory state
4390 // with the freshly-fetched columns.
4391 // Issue #825. Only emitted when the model declares a PK; non-PK
4392 // models can't address a specific row.
4393 //
4394 // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
4395 // every field from `self`; resets the PK to `Auto::Unset` when
4396 // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
4397 // a fresh autoincrement value. Non-Auto PKs preserve the source
4398 // PK — the caller must overwrite before insert. Pure-Rust, no
4399 // I/O, no dialect surface.
4400 let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
4401 let other_field_clones: Vec<TokenStream2> = fields
4402 .column_entries
4403 .iter()
4404 .filter(|c| &c.ident != pk_ident)
4405 .map(|c| {
4406 let ident = &c.ident;
4407 quote! {
4408 #ident: ::core::clone::Clone::clone(&self.#ident)
4409 }
4410 })
4411 .collect();
4412 let pk_clone_token = if fields.pk_is_auto {
4413 quote! { #pk_ident: #root::sql::Auto::Unset }
4414 } else {
4415 quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
4416 };
4417 let replicate_doc = if fields.pk_is_auto {
4418 quote! {
4419 /// Eloquent-style `replicate()` — return a clone of this
4420 /// row with the primary key reset to [`Auto::Unset`] so
4421 /// the copy is ready for `insert_pool` / `save_pool` to
4422 /// allocate a fresh autoincrement value. Every other
4423 /// field is `Clone`d verbatim. Issue #825.
4424 ///
4425 /// `auto_now_add` / `auto_now` timestamp fields are
4426 /// **not** reset (Eloquent's `replicate` doesn't reset
4427 /// them either) — pass them through the normal insert
4428 /// path if you want fresh values, or assign them
4429 /// explicitly after the call.
4430 }
4431 } else {
4432 quote! {
4433 /// Eloquent-style `replicate()` — clone this row
4434 /// verbatim. Because the primary key is **not** an
4435 /// `Auto<T>`, the clone keeps the source PK; the
4436 /// caller must overwrite `copy.<pk>` before inserting
4437 /// to avoid a unique-key violation. Issue #825.
4438 }
4439 };
4440 // 2026-06-07 — field-name / shortcut collision guard.
4441 //
4442 // The macro emits both `pub const <field>: <field>_col = ...`
4443 // (per-field typed-column const, used by the typed-builder
4444 // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
4445 // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
4446 // model has a field named e.g. `count`, both items would land
4447 // in the same inherent impl with the same name, and the
4448 // compiler rejects the derive with "duplicate definitions".
4449 //
4450 // Drop the conflicting shortcut for that model. Callers can
4451 // still reach the same behavior via
4452 // `QuerySet::<Model>::default().count(&pool)`.
4453 let column_names: ::std::collections::HashSet<String> = fields
4454 .column_entries
4455 .iter()
4456 .map(|c| ident_name(&c.ident))
4457 .collect();
4458 let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
4459 if column_names.contains(name) {
4460 quote! {}
4461 } else {
4462 tokens
4463 }
4464 };
4465 let count_method = emit_if_no_field_collision(
4466 "count",
4467 quote! {
4468 /// Count rows of this model — `SELECT COUNT(*) FROM
4469 /// <table>`. Eloquent `Model::count()` parity.
4470 ///
4471 /// Skipped on models that already declare a field named
4472 /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
4473 /// in that case.
4474 ///
4475 /// # Errors
4476 /// As [`CounterPool::count`].
4477 ///
4478 /// [`CounterPool::count`]: rustango::sql::CounterPool::count
4479 pub async fn count(
4480 pool: &#root::sql::Pool,
4481 ) -> ::core::result::Result<i64, #root::sql::ExecError> {
4482 use #root::sql::CounterPool as _;
4483 #root::query::QuerySet::<Self>::default()
4484 .count(pool)
4485 .await
4486 }
4487 },
4488 );
4489 let value_method = emit_if_no_field_collision(
4490 "value",
4491 quote! {
4492 /// Pluck a single scalar from the first row.
4493 /// Eloquent `Model::query()->value($col)` parity.
4494 ///
4495 /// Skipped on models that already declare a field named
4496 /// `value`. Drop into
4497 /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4498 ///
4499 /// # Errors
4500 /// As `ValuesFlatQuerySet::first`.
4501 pub async fn value<U>(
4502 col: &str,
4503 pool: &#root::sql::Pool,
4504 ) -> ::core::result::Result<
4505 ::core::option::Option<U>,
4506 #root::sql::ExecError,
4507 >
4508 where
4509 U: #root::sql::FlatScalar,
4510 {
4511 let _col_static: &'static str = Self::__resolve_col(col)?;
4512 #root::query::QuerySet::<Self>::default()
4513 .values_list_flat(_col_static)
4514 .first::<U>(pool)
4515 .await
4516 }
4517 },
4518 );
4519 let sum_method = emit_if_no_field_collision(
4520 "sum",
4521 quote! {
4522 /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4523 /// Skipped on models that already declare a field named
4524 /// `sum`.
4525 ///
4526 /// # Errors
4527 /// As [`#root::sql::fetch_aggregate_pool`].
4528 pub async fn sum<U>(
4529 col: &str,
4530 pool: &#root::sql::Pool,
4531 ) -> ::core::result::Result<
4532 ::core::option::Option<U>,
4533 #root::sql::ExecError,
4534 >
4535 where
4536 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4537 + #root::sql::MaybeMyFromRow
4538 + #root::sql::MaybeSqliteFromRow
4539 + ::core::marker::Send
4540 + ::core::marker::Unpin,
4541 {
4542 #root::query::QuerySet::<Self>::default()
4543 .sum::<U>(col, pool)
4544 .await
4545 }
4546 },
4547 );
4548 let avg_method = emit_if_no_field_collision(
4549 "avg",
4550 quote! {
4551 /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4552 /// models that already declare a field named `avg`.
4553 ///
4554 /// # Errors
4555 /// As [`#root::sql::fetch_aggregate_pool`].
4556 pub async fn avg<U>(
4557 col: &str,
4558 pool: &#root::sql::Pool,
4559 ) -> ::core::result::Result<
4560 ::core::option::Option<U>,
4561 #root::sql::ExecError,
4562 >
4563 where
4564 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4565 + #root::sql::MaybeMyFromRow
4566 + #root::sql::MaybeSqliteFromRow
4567 + ::core::marker::Send
4568 + ::core::marker::Unpin,
4569 {
4570 #root::query::QuerySet::<Self>::default()
4571 .avg::<U>(col, pool)
4572 .await
4573 }
4574 },
4575 );
4576 let min_method = emit_if_no_field_collision(
4577 "min",
4578 quote! {
4579 /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4580 /// models that already declare a field named `min`.
4581 ///
4582 /// # Errors
4583 /// As [`#root::sql::fetch_aggregate_pool`].
4584 pub async fn min<U>(
4585 col: &str,
4586 pool: &#root::sql::Pool,
4587 ) -> ::core::result::Result<
4588 ::core::option::Option<U>,
4589 #root::sql::ExecError,
4590 >
4591 where
4592 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4593 + #root::sql::MaybeMyFromRow
4594 + #root::sql::MaybeSqliteFromRow
4595 + ::core::marker::Send
4596 + ::core::marker::Unpin,
4597 {
4598 #root::query::QuerySet::<Self>::default()
4599 .min::<U>(col, pool)
4600 .await
4601 }
4602 },
4603 );
4604 let max_method = emit_if_no_field_collision(
4605 "max",
4606 quote! {
4607 /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4608 /// models that already declare a field named `max`.
4609 ///
4610 /// # Errors
4611 /// As [`#root::sql::fetch_aggregate_pool`].
4612 pub async fn max<U>(
4613 col: &str,
4614 pool: &#root::sql::Pool,
4615 ) -> ::core::result::Result<
4616 ::core::option::Option<U>,
4617 #root::sql::ExecError,
4618 >
4619 where
4620 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4621 + #root::sql::MaybeMyFromRow
4622 + #root::sql::MaybeSqliteFromRow
4623 + ::core::marker::Send
4624 + ::core::marker::Unpin,
4625 {
4626 #root::query::QuerySet::<Self>::default()
4627 .max::<U>(col, pool)
4628 .await
4629 }
4630 },
4631 );
4632 let first_method = emit_if_no_field_collision(
4633 "first",
4634 quote! {
4635 /// First row of this model. Eloquent `Model::first()`.
4636 /// Skipped on models that already declare a field named
4637 /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4638 ///
4639 /// # Errors
4640 /// As `QuerySet::first`.
4641 pub async fn first(
4642 pool: &#root::sql::Pool,
4643 ) -> ::core::result::Result<
4644 ::core::option::Option<Self>,
4645 #root::sql::ExecError,
4646 > {
4647 #root::query::QuerySet::<Self>::default()
4648 .first(pool)
4649 .await
4650 }
4651 },
4652 );
4653 let last_method = emit_if_no_field_collision(
4654 "last",
4655 quote! {
4656 /// Last row of this model by primary-key DESC.
4657 /// Eloquent `Model::query()->latest('id')->first()`
4658 /// parity — fetches the highest-PK row without
4659 /// requiring the caller to spell the PK column.
4660 /// Returns `None` on an empty table.
4661 ///
4662 /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4663 /// Skipped on models that already declare a field
4664 /// named `last`.
4665 ///
4666 /// # Errors
4667 /// As `QuerySet::last`.
4668 pub async fn last(
4669 pool: &#root::sql::Pool,
4670 ) -> ::core::result::Result<
4671 ::core::option::Option<Self>,
4672 #root::sql::ExecError,
4673 > {
4674 #root::query::QuerySet::<Self>::default()
4675 .last(pool)
4676 .await
4677 }
4678 },
4679 );
4680 quote! {
4681 /// Re-SELECT this row by its primary key and overwrite
4682 /// every in-memory field with the freshly-fetched value.
4683 /// Issue #825.
4684 ///
4685 /// Use this when the row may have been modified by another
4686 /// process / connection / job since you read it — e.g. after
4687 /// a queued task callback, or to re-sync stale UI state
4688 /// before re-saving.
4689 ///
4690 /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4691 /// when the primary key no longer matches any row (e.g.
4692 /// the row was deleted concurrently).
4693 ///
4694 /// # Errors
4695 /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4696 /// the PK no longer exists.
4697 ///
4698 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4699 pub async fn refresh_from_db(
4700 &mut self,
4701 pool: &#root::sql::Pool,
4702 ) -> ::core::result::Result<(), #root::sql::ExecError> {
4703 use #root::sql::FetcherPool as _;
4704 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4705 ::core::clone::Clone::clone(&self.#pk_ident),
4706 );
4707 let mut _rows: ::std::vec::Vec<Self> =
4708 #root::query::QuerySet::<Self>::default()
4709 .filter(::core::stringify!(#pk_ident), _pk_val)
4710 .limit(1)
4711 .fetch(pool)
4712 .await?;
4713 match _rows.into_iter().next() {
4714 ::core::option::Option::Some(_fresh) => {
4715 *self = _fresh;
4716 ::core::result::Result::Ok(())
4717 }
4718 ::core::option::Option::None => ::core::result::Result::Err(
4719 #root::sql::ExecError::Driver(
4720 #root::sql::sqlx::Error::RowNotFound,
4721 ),
4722 ),
4723 }
4724 }
4725
4726 /// Atomically increment the integer column `col` by
4727 /// `by` for this row. Equivalent to
4728 /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4729 ///
4730 /// **Doesn't mutate `self`** — the in-memory copy is now
4731 /// stale; call [`Self::refresh_from_db`] /
4732 /// [`Self::fresh`] to re-sync. Returns the rows-
4733 /// affected count (0 when the PK doesn't match any row,
4734 /// 1 on success).
4735 ///
4736 /// `col` is the Rust field name as a string; unknown
4737 /// fields surface as `UnknownField` at runtime. Negative
4738 /// `by` values atomically decrement (see also
4739 /// [`Self::decrement`]).
4740 ///
4741 /// # Errors
4742 /// As [`UpdaterPool::execute_pool`].
4743 ///
4744 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4745 pub async fn increment(
4746 &self,
4747 col: &str,
4748 by: i64,
4749 pool: &#root::sql::Pool,
4750 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4751 Self::__increment_one(self, col, by, pool).await
4752 }
4753
4754 /// Sibling of [`Self::increment`] — atomically
4755 /// decrement this row's `col` by `by`. Eloquent
4756 /// `$model->decrement($col, $by)` parity. Equivalent to
4757 /// `self.increment(col, -by, &pool)`; the separate name
4758 /// keeps call sites readable.
4759 ///
4760 /// # Errors
4761 /// As [`Self::increment`].
4762 pub async fn decrement(
4763 &self,
4764 col: &str,
4765 by: i64,
4766 pool: &#root::sql::Pool,
4767 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4768 Self::__increment_one(self, col, -by, pool).await
4769 }
4770
4771 /// Bulk-increment: add `by` to `col` on every row of the
4772 /// table. Eloquent `Model::query()->increment($col, $by)`
4773 /// parity. Use for counters, score adjustments, view
4774 /// rollups.
4775 ///
4776 /// # Errors
4777 /// As [`UpdaterPool::execute_pool`].
4778 ///
4779 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4780 pub async fn increment_each(
4781 col: &str,
4782 by: i64,
4783 pool: &#root::sql::Pool,
4784 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4785 Self::__increment_all(col, by, pool).await
4786 }
4787
4788 /// Sibling of [`Self::increment_each`] — bulk-decrement.
4789 ///
4790 /// # Errors
4791 /// As [`Self::increment_each`].
4792 pub async fn decrement_each(
4793 col: &str,
4794 by: i64,
4795 pool: &#root::sql::Pool,
4796 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4797 Self::__increment_all(col, -by, pool).await
4798 }
4799
4800 /// Internal: forward to
4801 /// [`#root::sql::model_shortcuts::increment_one_pool`].
4802 /// One-line wrapper kept as a per-Model method so the
4803 /// macro's emitted `increment` / `decrement` instance
4804 /// calls don't have to thread `Self` through manually.
4805 #[doc(hidden)]
4806 pub async fn __increment_one(
4807 this: &Self,
4808 col: &str,
4809 by: i64,
4810 pool: &#root::sql::Pool,
4811 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4812 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4813 ::core::clone::Clone::clone(&this.#pk_ident),
4814 );
4815 #root::sql::model_shortcuts::increment_one_pool::<Self>(
4816 ::core::stringify!(#pk_ident),
4817 _pk_val,
4818 col,
4819 by,
4820 pool,
4821 )
4822 .await
4823 }
4824
4825 /// Internal: run
4826 /// [`#root::sql::model_shortcuts::increment_all_query`].
4827 #[doc(hidden)]
4828 pub async fn __increment_all(
4829 col: &str,
4830 by: i64,
4831 pool: &#root::sql::Pool,
4832 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4833 let _query =
4834 #root::sql::model_shortcuts::increment_all_query::<Self>(col, by)?;
4835 #bulk_update_run
4836 }
4837
4838 /// Internal: forward to
4839 /// [`#root::sql::model_shortcuts::resolve_col`].
4840 #[doc(hidden)]
4841 pub fn __resolve_col(
4842 col: &str,
4843 ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4844 #root::sql::model_shortcuts::resolve_col::<Self>(col)
4845 }
4846
4847 /// Internal: forward to
4848 /// [`#root::sql::model_shortcuts::add_signed_expr`].
4849 #[doc(hidden)]
4850 #[must_use]
4851 pub fn __add_signed_expr(
4852 col_static: &'static str,
4853 signed_by: i64,
4854 ) -> #root::core::Expr {
4855 #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4856 }
4857
4858 /// Re-SELECT this row by its primary key and return a
4859 /// **new** instance with the freshly-fetched fields.
4860 /// Eloquent `Model::fresh()` parity — non-mutating
4861 /// counterpart of [`Self::refresh_from_db`].
4862 ///
4863 /// Returns `Ok(None)` when the row was deleted
4864 /// concurrently — vs [`Self::refresh_from_db`]
4865 /// which surfaces that as `RowNotFound` because
4866 /// in-place mutation has nothing to write to.
4867 ///
4868 /// Useful when you want to compare the in-memory
4869 /// instance against the persisted state (audit-style
4870 /// diffs, conflict detection) without mutating the
4871 /// reference you already hold.
4872 ///
4873 /// # Errors
4874 /// As [`FetcherPool::fetch`].
4875 ///
4876 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4877 pub async fn fresh(
4878 &self,
4879 pool: &#root::sql::Pool,
4880 ) -> ::core::result::Result<
4881 ::core::option::Option<Self>,
4882 #root::sql::ExecError,
4883 > {
4884 use #root::sql::FetcherPool as _;
4885 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4886 ::core::clone::Clone::clone(&self.#pk_ident),
4887 );
4888 let _rows: ::std::vec::Vec<Self> =
4889 #root::query::QuerySet::<Self>::default()
4890 .filter(::core::stringify!(#pk_ident), _pk_val)
4891 .limit(1)
4892 .fetch(pool)
4893 .await?;
4894 ::core::result::Result::Ok(_rows.into_iter().next())
4895 }
4896
4897 #replicate_doc
4898 #[must_use]
4899 pub fn replicate(&self) -> Self {
4900 Self {
4901 #pk_clone_token,
4902 #( #other_field_clones, )*
4903 }
4904 }
4905
4906 #first_method
4907
4908 #last_method
4909
4910 /// Throwing counterpart of [`Self::first`] —
4911 /// errors with `RowNotFound` when the table is empty.
4912 /// Eloquent `Model::firstOrFail()` parity.
4913 ///
4914 /// # Errors
4915 /// As [`Self::first`]; additionally
4916 /// [`sqlx::Error::RowNotFound`] on empty tables.
4917 ///
4918 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4919 pub async fn first_or_fail(
4920 pool: &#root::sql::Pool,
4921 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4922 // Route through the queryset rather than `Self::first`,
4923 // which is suppressed on models with a field named
4924 // `first` (field/shortcut collision guard).
4925 match #root::query::QuerySet::<Self>::default().first(pool).await? {
4926 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4927 ::core::option::Option::None => ::core::result::Result::Err(
4928 #root::sql::ExecError::Driver(
4929 #root::sql::sqlx::Error::RowNotFound,
4930 ),
4931 ),
4932 }
4933 }
4934
4935 /// Single-column projection — `SELECT <col> FROM
4936 /// <table>`. Returns `Vec<U>` where each element is the
4937 /// decoded value of the column. Eloquent
4938 /// `Model::pluck($column)` parity.
4939 ///
4940 /// Thin wrapper over `QuerySet::<Self>::default()
4941 /// .values_list_flat(col).fetch::<U>(pool)`. `U` is a
4942 /// `FlatScalar`; use `Option<_>` for a nullable column.
4943 ///
4944 /// # Errors
4945 /// As `ValuesFlatQuerySet::fetch`.
4946 pub async fn pluck<U>(
4947 col: &'static str,
4948 pool: &#root::sql::Pool,
4949 ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4950 where
4951 U: #root::sql::FlatScalar,
4952 {
4953 #root::query::QuerySet::<Self>::default()
4954 .values_list_flat(col)
4955 .fetch::<U>(pool)
4956 .await
4957 }
4958
4959 /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4960 /// stream every row of this model in batches of `n`,
4961 /// invoking the callback once per batch. Stable PK-ASC
4962 /// ordering so the LIMIT/OFFSET pagination is
4963 /// deterministic across drivers.
4964 ///
4965 /// The callback is async — it can do further DB work
4966 /// (writes, related-row lookups, queue dispatch) per
4967 /// batch. Return `Err(...)` from the callback to abort
4968 /// the iteration early; the error bubbles up.
4969 ///
4970 /// **When to use**: bulk processing flows that can't
4971 /// fit the whole table in memory — sending newsletters,
4972 /// running data migrations, computing summary
4973 /// statistics. For small / known-bounded tables, plain
4974 /// `Self::all(&pool)` is simpler.
4975 ///
4976 /// **Caveat**: ascending OFFSET pagination is O(N²) on
4977 /// large tables. For multi-million-row scans prefer
4978 /// keyset-by-PK (the standard "WHERE id > last_seen"
4979 /// shape) over `chunk(...)`.
4980 ///
4981 /// Skipped on models without a primary key — chunking
4982 /// needs a stable order to avoid skipping / repeating
4983 /// rows across batches.
4984 pub async fn chunk<F, Fut>(
4985 n: i64,
4986 pool: &#root::sql::Pool,
4987 mut cb: F,
4988 ) -> ::core::result::Result<(), #root::sql::ExecError>
4989 where
4990 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4991 Fut: ::core::future::Future<
4992 Output = ::core::result::Result<(), #root::sql::ExecError>,
4993 >,
4994 {
4995 use #root::sql::FetcherPool as _;
4996 // The field name: `filter` / `order_by` take fields, not columns.
4997 let pk_col = match <Self as #root::core::Model>::SCHEMA.primary_key() {
4998 ::core::option::Option::Some(f) => f.name,
4999 ::core::option::Option::None => {
5000 return ::core::result::Result::Ok(());
5001 }
5002 };
5003 let mut offset: i64 = 0;
5004 loop {
5005 let rows: ::std::vec::Vec<Self> =
5006 #root::query::QuerySet::<Self>::default()
5007 .order_by(&[(pk_col, false)])
5008 .limit(n)
5009 .offset(offset)
5010 .fetch(pool)
5011 .await?;
5012 if rows.is_empty() {
5013 return ::core::result::Result::Ok(());
5014 }
5015 let len = rows.len() as i64;
5016 cb(rows).await?;
5017 if len < n {
5018 return ::core::result::Result::Ok(());
5019 }
5020 offset += n;
5021 }
5022 }
5023
5024 /// Eloquent `Model::chunkById($n, fn (...))` — same
5025 /// per-batch callback shape as [`Self::chunk`], but uses
5026 /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
5027 /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
5028 /// — the right choice for multi-million-row sweeps.
5029 ///
5030 /// The keyset compares the primary key in its own type,
5031 /// so any orderable PK works (`i32`, `i64`, UUID, string).
5032 ///
5033 /// Callback errors abort iteration; the error bubbles up
5034 /// unchanged. Empty table → callback invoked zero times.
5035 pub async fn chunk_by_id<F, Fut>(
5036 n: i64,
5037 pool: &#root::sql::Pool,
5038 mut cb: F,
5039 ) -> ::core::result::Result<(), #root::sql::ExecError>
5040 where
5041 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
5042 Fut: ::core::future::Future<
5043 Output = ::core::result::Result<(), #root::sql::ExecError>,
5044 >,
5045 {
5046 use #root::sql::FetcherPool as _;
5047 // The field name: `filter` / `order_by` take fields, not columns.
5048 let pk_col = match <Self as #root::core::Model>::SCHEMA.primary_key() {
5049 ::core::option::Option::Some(f) => f.name,
5050 ::core::option::Option::None => {
5051 return ::core::result::Result::Ok(());
5052 }
5053 };
5054 // The largest PK seen, in the PK's own type; none before
5055 // the first batch.
5056 let key = ::std::format!("{}__gt", pk_col);
5057 let mut last_seen: ::core::option::Option<#root::core::SqlValue> =
5058 ::core::option::Option::None;
5059 loop {
5060 let mut qs = #root::query::QuerySet::<Self>::default();
5061 if let ::core::option::Option::Some(v) = last_seen.take() {
5062 qs = qs.filter(key.as_str(), v);
5063 }
5064 let rows: ::std::vec::Vec<Self> = qs
5065 .order_by(&[(pk_col, false)])
5066 .limit(n)
5067 .fetch(pool)
5068 .await?;
5069 let len = rows.len() as i64;
5070 // Capture the last row's PK BEFORE moving rows
5071 // into the callback.
5072 let ::core::option::Option::Some(max_pk) =
5073 rows.last().map(|r| r.__rustango_pk_value())
5074 else {
5075 return ::core::result::Result::Ok(());
5076 };
5077 cb(rows).await?;
5078 if len < n {
5079 return ::core::result::Result::Ok(());
5080 }
5081 last_seen = ::core::option::Option::Some(max_pk);
5082 }
5083 }
5084
5085 /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
5086 /// per-row callback companion to [`Self::chunk`].
5087 /// Streams every row in keyset-paginated batches of
5088 /// `batch` size, calling `cb` once per row.
5089 ///
5090 /// Inherits the keyset-paginated scan of
5091 /// [`Self::chunk_by_id`] (O(N) total).
5092 ///
5093 /// ```ignore
5094 /// Post::each(500, &pool, |p| async move {
5095 /// reindex(p).await?;
5096 /// Ok(())
5097 /// }).await?;
5098 /// ```
5099 ///
5100 /// Return `Err(...)` from the callback to abort
5101 /// iteration; the error bubbles up unchanged.
5102 pub async fn each<F, Fut>(
5103 batch: i64,
5104 pool: &#root::sql::Pool,
5105 mut cb: F,
5106 ) -> ::core::result::Result<(), #root::sql::ExecError>
5107 where
5108 F: ::core::ops::FnMut(Self) -> Fut,
5109 Fut: ::core::future::Future<
5110 Output = ::core::result::Result<(), #root::sql::ExecError>,
5111 >,
5112 {
5113 use #root::sql::FetcherPool as _;
5114 // The field name: `filter` / `order_by` take fields, not columns.
5115 let pk_col = match <Self as #root::core::Model>::SCHEMA.primary_key() {
5116 ::core::option::Option::Some(f) => f.name,
5117 ::core::option::Option::None => {
5118 return ::core::result::Result::Ok(());
5119 }
5120 };
5121 let key = ::std::format!("{}__gt", pk_col);
5122 let mut last_seen: ::core::option::Option<#root::core::SqlValue> =
5123 ::core::option::Option::None;
5124 loop {
5125 let mut qs = #root::query::QuerySet::<Self>::default();
5126 if let ::core::option::Option::Some(v) = last_seen.take() {
5127 qs = qs.filter(key.as_str(), v);
5128 }
5129 let rows: ::std::vec::Vec<Self> = qs
5130 .order_by(&[(pk_col, false)])
5131 .limit(batch)
5132 .fetch(pool)
5133 .await?;
5134 let len = rows.len() as i64;
5135 let ::core::option::Option::Some(max_pk) =
5136 rows.last().map(|r| r.__rustango_pk_value())
5137 else {
5138 return ::core::result::Result::Ok(());
5139 };
5140 for row in rows {
5141 cb(row).await?;
5142 }
5143 if len < batch {
5144 return ::core::result::Result::Ok(());
5145 }
5146 last_seen = ::core::option::Option::Some(max_pk);
5147 }
5148 }
5149
5150 /// Delete every row of this model — `TRUNCATE TABLE
5151 /// <table> RESTART IDENTITY CASCADE` on Postgres,
5152 /// `DELETE FROM <table>` on MySQL / SQLite (which don't
5153 /// support `TRUNCATE` inside foreign-key constraints
5154 /// or — for SQLite — at all). Eloquent `Model::truncate()`
5155 /// parity.
5156 ///
5157 /// **Use only in tests / fixture-reset flows.** Production
5158 /// writes through this would silently bypass the
5159 /// `pre_delete` / `post_delete` signals (no per-row hooks
5160 /// fire on a TRUNCATE / bulk DELETE FROM). An audited model
5161 /// gets one bulk audit entry, not one per row. Global scopes
5162 /// do not apply: every row goes, generic M2M links too.
5163 ///
5164 /// # Errors
5165 /// As [`truncate`].
5166 ///
5167 /// [`truncate`]: rustango::sql::truncate
5168 pub async fn truncate(
5169 pool: &#root::sql::Pool,
5170 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5171 #truncate_run
5172 }
5173
5174 /// Bulk-delete every row whose primary key is in
5175 /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
5176 /// Returns the affected row count.
5177 ///
5178 /// Eloquent `Model::destroy([1, 2, 3])` parity.
5179 /// Empty `pks` is a no-op (returns 0). All or none: past
5180 /// the bind cap the batches share one transaction.
5181 ///
5182 /// Accepts any iterable whose elements are
5183 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
5184 /// `[i64; N]`, etc.
5185 ///
5186 /// # Errors
5187 /// As `delete_pool`.
5188 pub async fn destroy<V>(
5189 pks: impl ::core::iter::IntoIterator<Item = V>,
5190 pool: &#root::sql::Pool,
5191 ) -> ::core::result::Result<u64, #root::sql::ExecError>
5192 where
5193 V: ::core::convert::Into<#root::core::SqlValue>,
5194 {
5195 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5196 pks.into_iter().map(::core::convert::Into::into).collect();
5197 if _values.is_empty() {
5198 return ::core::result::Result::Ok(0);
5199 }
5200 let _pk = <Self as #root::core::Model>::SCHEMA
5201 .primary_key()
5202 .ok_or_else(|| {
5203 #root::sql::ExecError::Sql(#root::sql::SqlError::MissingPrimaryKey)
5204 })?;
5205 // One statement per `IN` list the dialect can bind, all or none.
5206 let mut _queries = ::std::vec::Vec::new();
5207 for _keys in #root::sql::__rustango_in_batches::<Self>(pool, _values)? {
5208 _queries.push(
5209 #root::query::QuerySet::<Self>::default()
5210 .filter_op(
5211 _pk.name,
5212 #root::core::Op::In,
5213 #root::core::SqlValue::List(_keys),
5214 )
5215 .compile_delete()?,
5216 );
5217 }
5218 #destroy_run
5219 }
5220
5221 /// Fetch every row where `<col> = <val>`. Eloquent
5222 /// `Model::where($col, $val)->get()` parity.
5223 ///
5224 /// Thin wrapper over `QuerySet::<Self>::default()
5225 /// .filter(col, val).fetch(pool)`. For one row,
5226 /// use [`Self::first_where`]; for a chain that
5227 /// needs further `.filter()` / `.order_by()` /
5228 /// `.limit()`, drop down to `Self::query().filter(...)`
5229 /// directly.
5230 ///
5231 /// `val` accepts any value `Into<SqlValue>` so plain
5232 /// strings, ints, UUIDs, etc. all work.
5233 ///
5234 /// # Errors
5235 /// As [`FetcherPool::fetch`].
5236 ///
5237 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5238 pub async fn where_(
5239 col: &str,
5240 val: impl ::core::convert::Into<#root::core::SqlValue>,
5241 pool: &#root::sql::Pool,
5242 ) -> ::core::result::Result<
5243 ::std::vec::Vec<Self>,
5244 #root::sql::ExecError,
5245 > {
5246 use #root::sql::FetcherPool as _;
5247 #root::query::QuerySet::<Self>::default()
5248 .filter(col, val)
5249 .fetch(pool)
5250 .await
5251 }
5252
5253 /// Fetch every row where `<col> IN (vals)`. Eloquent
5254 /// `Model::whereIn($col, $vals)->get()` parity. Empty
5255 /// `vals` returns no rows (matches SQL's empty-IN
5256 /// semantics).
5257 ///
5258 /// `vals` accepts any iterable whose items are
5259 /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
5260 /// etc.
5261 ///
5262 /// # Errors
5263 /// As [`FetcherPool::fetch`].
5264 ///
5265 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5266 pub async fn where_in<V>(
5267 col: &str,
5268 vals: impl ::core::iter::IntoIterator<Item = V>,
5269 pool: &#root::sql::Pool,
5270 ) -> ::core::result::Result<
5271 ::std::vec::Vec<Self>,
5272 #root::sql::ExecError,
5273 >
5274 where
5275 V: ::core::convert::Into<#root::core::SqlValue>,
5276 {
5277 use #root::sql::FetcherPool as _;
5278 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5279 vals.into_iter().map(::core::convert::Into::into).collect();
5280 if _values.is_empty() {
5281 return ::core::result::Result::Ok(::std::vec::Vec::new());
5282 }
5283 let _key = ::std::format!("{}__in", col);
5284 let mut _rows = ::std::vec::Vec::new();
5285 for _keys in #root::sql::__rustango_in_batches::<Self>(pool, _values)? {
5286 _rows.extend(
5287 #root::query::QuerySet::<Self>::default()
5288 .filter(&_key, #root::core::SqlValue::List(_keys))
5289 .fetch(pool)
5290 .await?,
5291 );
5292 }
5293 ::core::result::Result::Ok(_rows)
5294 }
5295
5296 /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
5297 /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
5298 /// `vals` returns every row (matches SQL's empty-NOT-IN
5299 /// semantics — vacuously true for every row).
5300 ///
5301 /// # Errors
5302 /// As [`FetcherPool::fetch`].
5303 ///
5304 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5305 pub async fn where_not_in<V>(
5306 col: &str,
5307 vals: impl ::core::iter::IntoIterator<Item = V>,
5308 pool: &#root::sql::Pool,
5309 ) -> ::core::result::Result<
5310 ::std::vec::Vec<Self>,
5311 #root::sql::ExecError,
5312 >
5313 where
5314 V: ::core::convert::Into<#root::core::SqlValue>,
5315 {
5316 use #root::sql::FetcherPool as _;
5317 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5318 vals.into_iter().map(::core::convert::Into::into).collect();
5319 if _values.is_empty() {
5320 return #root::query::QuerySet::<Self>::default()
5321 .fetch(pool)
5322 .await;
5323 }
5324 let _key = ::std::format!("{}__not_in", col);
5325 #root::query::QuerySet::<Self>::default()
5326 .filter(&_key, #root::core::SqlValue::List(_values))
5327 .fetch(pool)
5328 .await
5329 }
5330
5331 /// Fetch every row where `<col> IS NULL`. Eloquent
5332 /// `Model::whereNull($col)->get()` parity.
5333 ///
5334 /// # Errors
5335 /// As [`FetcherPool::fetch`].
5336 ///
5337 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5338 pub async fn where_null(
5339 col: &str,
5340 pool: &#root::sql::Pool,
5341 ) -> ::core::result::Result<
5342 ::std::vec::Vec<Self>,
5343 #root::sql::ExecError,
5344 > {
5345 use #root::sql::FetcherPool as _;
5346 let _key = ::std::format!("{}__isnull", col);
5347 #root::query::QuerySet::<Self>::default()
5348 .filter(&_key, #root::core::SqlValue::Bool(true))
5349 .fetch(pool)
5350 .await
5351 }
5352
5353 /// Fetch every row where `<col> IS NOT NULL`. Eloquent
5354 /// `Model::whereNotNull($col)->get()` parity.
5355 ///
5356 /// # Errors
5357 /// As [`FetcherPool::fetch`].
5358 ///
5359 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5360 pub async fn where_not_null(
5361 col: &str,
5362 pool: &#root::sql::Pool,
5363 ) -> ::core::result::Result<
5364 ::std::vec::Vec<Self>,
5365 #root::sql::ExecError,
5366 > {
5367 use #root::sql::FetcherPool as _;
5368 let _key = ::std::format!("{}__isnull", col);
5369 #root::query::QuerySet::<Self>::default()
5370 .filter(&_key, #root::core::SqlValue::Bool(false))
5371 .fetch(pool)
5372 .await
5373 }
5374
5375 /// Fetch up to `n` rows in random order. Eloquent
5376 /// `Model::inRandomOrder()->limit($n)->get()` /
5377 /// `Model::query()->inRandomOrder()->get()->take($n)`
5378 /// parity. **Performance caveat**: random ordering
5379 /// forces a full table scan + per-row random key sort;
5380 /// the optimizer cannot use an index. Prefer a
5381 /// `pk >= rand_offset LIMIT N` walk for huge tables.
5382 ///
5383 /// # Errors
5384 /// As [`FetcherPool::fetch`].
5385 ///
5386 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5387 pub async fn random_n(
5388 n: i64,
5389 pool: &#root::sql::Pool,
5390 ) -> ::core::result::Result<
5391 ::std::vec::Vec<Self>,
5392 #root::sql::ExecError,
5393 > {
5394 use #root::sql::FetcherPool as _;
5395 #root::query::QuerySet::<Self>::default()
5396 .order_random()
5397 .limit(n)
5398 .fetch(pool)
5399 .await
5400 }
5401
5402 /// Fetch one row in random order. Eloquent
5403 /// `Model::inRandomOrder()->first()` parity. Same
5404 /// performance caveat as [`Self::random_n`].
5405 ///
5406 /// # Errors
5407 /// As [`FetcherPool::fetch`].
5408 ///
5409 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5410 pub async fn random(
5411 pool: &#root::sql::Pool,
5412 ) -> ::core::result::Result<
5413 ::core::option::Option<Self>,
5414 #root::sql::ExecError,
5415 > {
5416 ::core::result::Result::Ok(
5417 Self::random_n(1, pool).await?.into_iter().next(),
5418 )
5419 }
5420
5421 /// Fetch every row ordered ASC by `field`. Eloquent
5422 /// `Model::oldest($field)->get()` parity — the multi-row
5423 /// counterpart of [`Self::earliest`].
5424 ///
5425 /// # Errors
5426 /// As [`FetcherPool::fetch`].
5427 ///
5428 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5429 pub async fn oldest(
5430 field: &str,
5431 pool: &#root::sql::Pool,
5432 ) -> ::core::result::Result<
5433 ::std::vec::Vec<Self>,
5434 #root::sql::ExecError,
5435 > {
5436 use #root::sql::FetcherPool as _;
5437 #root::query::QuerySet::<Self>::default()
5438 .order_by(&[(field, false)])
5439 .fetch(pool)
5440 .await
5441 }
5442
5443 /// Fetch every row ordered DESC by `field`. Eloquent
5444 /// `Model::latest($field)->get()` parity — the multi-row
5445 /// counterpart of [`Self::latest`].
5446 ///
5447 /// # Errors
5448 /// As [`FetcherPool::fetch`].
5449 ///
5450 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5451 pub async fn newest(
5452 field: &str,
5453 pool: &#root::sql::Pool,
5454 ) -> ::core::result::Result<
5455 ::std::vec::Vec<Self>,
5456 #root::sql::ExecError,
5457 > {
5458 use #root::sql::FetcherPool as _;
5459 #root::query::QuerySet::<Self>::default()
5460 .order_by(&[(field, true)])
5461 .fetch(pool)
5462 .await
5463 }
5464
5465 /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5466 /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5467 /// Routes through the existing `__year` lookup suffix
5468 /// (issue #829).
5469 ///
5470 /// # Errors
5471 /// As [`FetcherPool::fetch`].
5472 ///
5473 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5474 pub async fn where_year(
5475 col: &str,
5476 year: i64,
5477 pool: &#root::sql::Pool,
5478 ) -> ::core::result::Result<
5479 ::std::vec::Vec<Self>,
5480 #root::sql::ExecError,
5481 > {
5482 use #root::sql::FetcherPool as _;
5483 let _key = ::std::format!("{}__year", col);
5484 #root::query::QuerySet::<Self>::default()
5485 .filter(&_key, #root::core::SqlValue::I64(year))
5486 .fetch(pool)
5487 .await
5488 }
5489
5490 /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5491 /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5492 /// `month` is 1–12.
5493 ///
5494 /// # Errors
5495 /// As [`FetcherPool::fetch`].
5496 ///
5497 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5498 pub async fn where_month(
5499 col: &str,
5500 month: i64,
5501 pool: &#root::sql::Pool,
5502 ) -> ::core::result::Result<
5503 ::std::vec::Vec<Self>,
5504 #root::sql::ExecError,
5505 > {
5506 use #root::sql::FetcherPool as _;
5507 let _key = ::std::format!("{}__month", col);
5508 #root::query::QuerySet::<Self>::default()
5509 .filter(&_key, #root::core::SqlValue::I64(month))
5510 .fetch(pool)
5511 .await
5512 }
5513
5514 /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5515 /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5516 /// `day` is 1–31.
5517 ///
5518 /// # Errors
5519 /// As [`FetcherPool::fetch`].
5520 ///
5521 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5522 pub async fn where_day(
5523 col: &str,
5524 day: i64,
5525 pool: &#root::sql::Pool,
5526 ) -> ::core::result::Result<
5527 ::std::vec::Vec<Self>,
5528 #root::sql::ExecError,
5529 > {
5530 use #root::sql::FetcherPool as _;
5531 let _key = ::std::format!("{}__day", col);
5532 #root::query::QuerySet::<Self>::default()
5533 .filter(&_key, #root::core::SqlValue::I64(day))
5534 .fetch(pool)
5535 .await
5536 }
5537
5538 /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5539 /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5540 /// `hour` is 0–23.
5541 ///
5542 /// # Errors
5543 /// As [`FetcherPool::fetch`].
5544 ///
5545 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5546 pub async fn where_hour(
5547 col: &str,
5548 hour: i64,
5549 pool: &#root::sql::Pool,
5550 ) -> ::core::result::Result<
5551 ::std::vec::Vec<Self>,
5552 #root::sql::ExecError,
5553 > {
5554 use #root::sql::FetcherPool as _;
5555 let _key = ::std::format!("{}__hour", col);
5556 #root::query::QuerySet::<Self>::default()
5557 .filter(&_key, #root::core::SqlValue::I64(hour))
5558 .fetch(pool)
5559 .await
5560 }
5561
5562 /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5563 /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5564 /// `minute` is 0–59.
5565 ///
5566 /// # Errors
5567 /// As [`FetcherPool::fetch`].
5568 ///
5569 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5570 pub async fn where_minute(
5571 col: &str,
5572 minute: i64,
5573 pool: &#root::sql::Pool,
5574 ) -> ::core::result::Result<
5575 ::std::vec::Vec<Self>,
5576 #root::sql::ExecError,
5577 > {
5578 use #root::sql::FetcherPool as _;
5579 let _key = ::std::format!("{}__minute", col);
5580 #root::query::QuerySet::<Self>::default()
5581 .filter(&_key, #root::core::SqlValue::I64(minute))
5582 .fetch(pool)
5583 .await
5584 }
5585
5586 /// Fetch every row where `<col> LIKE <pattern>` —
5587 /// caller-supplied pattern (must include `%` / `_`
5588 /// wildcards manually). Eloquent `Model::whereLike`
5589 /// parity.
5590 ///
5591 /// # Errors
5592 /// As [`FetcherPool::fetch`].
5593 ///
5594 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5595 pub async fn where_like(
5596 col: &str,
5597 pattern: impl ::core::convert::Into<::std::string::String>,
5598 pool: &#root::sql::Pool,
5599 ) -> ::core::result::Result<
5600 ::std::vec::Vec<Self>,
5601 #root::sql::ExecError,
5602 > {
5603 use #root::sql::FetcherPool as _;
5604 let _key = ::std::format!("{}__like", col);
5605 #root::query::QuerySet::<Self>::default()
5606 .filter(
5607 &_key,
5608 #root::core::SqlValue::String(pattern.into()),
5609 )
5610 .fetch(pool)
5611 .await
5612 }
5613
5614 /// Fetch every row where `<col> ILIKE <pattern>` —
5615 /// case-insensitive LIKE (PG native, MySQL/SQLite
5616 /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5617 ///
5618 /// # Errors
5619 /// As [`FetcherPool::fetch`].
5620 ///
5621 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5622 pub async fn where_ilike(
5623 col: &str,
5624 pattern: impl ::core::convert::Into<::std::string::String>,
5625 pool: &#root::sql::Pool,
5626 ) -> ::core::result::Result<
5627 ::std::vec::Vec<Self>,
5628 #root::sql::ExecError,
5629 > {
5630 use #root::sql::FetcherPool as _;
5631 let _key = ::std::format!("{}__ilike", col);
5632 #root::query::QuerySet::<Self>::default()
5633 .filter(
5634 &_key,
5635 #root::core::SqlValue::String(pattern.into()),
5636 )
5637 .fetch(pool)
5638 .await
5639 }
5640
5641 /// Fetch every row where `<col>` starts with `prefix`
5642 /// (auto-appends `%`). Eloquent
5643 /// `whereLike("col", "$prefix%")` parity.
5644 ///
5645 /// # Errors
5646 /// As [`FetcherPool::fetch`].
5647 ///
5648 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5649 pub async fn where_starts_with(
5650 col: &str,
5651 prefix: impl ::core::convert::Into<::std::string::String>,
5652 pool: &#root::sql::Pool,
5653 ) -> ::core::result::Result<
5654 ::std::vec::Vec<Self>,
5655 #root::sql::ExecError,
5656 > {
5657 use #root::sql::FetcherPool as _;
5658 let _key = ::std::format!("{}__startswith", col);
5659 #root::query::QuerySet::<Self>::default()
5660 .filter(
5661 &_key,
5662 #root::core::SqlValue::String(prefix.into()),
5663 )
5664 .fetch(pool)
5665 .await
5666 }
5667
5668 /// Fetch every row where `<col>` ends with `suffix`
5669 /// (auto-prepends `%`). Eloquent
5670 /// `whereLike("col", "%$suffix")` parity.
5671 ///
5672 /// # Errors
5673 /// As [`FetcherPool::fetch`].
5674 ///
5675 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5676 pub async fn where_ends_with(
5677 col: &str,
5678 suffix: impl ::core::convert::Into<::std::string::String>,
5679 pool: &#root::sql::Pool,
5680 ) -> ::core::result::Result<
5681 ::std::vec::Vec<Self>,
5682 #root::sql::ExecError,
5683 > {
5684 use #root::sql::FetcherPool as _;
5685 let _key = ::std::format!("{}__endswith", col);
5686 #root::query::QuerySet::<Self>::default()
5687 .filter(
5688 &_key,
5689 #root::core::SqlValue::String(suffix.into()),
5690 )
5691 .fetch(pool)
5692 .await
5693 }
5694
5695 /// Fetch every row where `<col>` contains `substr`
5696 /// (auto-wraps with `%`).
5697 /// Eloquent `whereLike("col", "%$substr%")` parity.
5698 ///
5699 /// # Errors
5700 /// As [`FetcherPool::fetch`].
5701 ///
5702 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5703 pub async fn where_contains(
5704 col: &str,
5705 substr: impl ::core::convert::Into<::std::string::String>,
5706 pool: &#root::sql::Pool,
5707 ) -> ::core::result::Result<
5708 ::std::vec::Vec<Self>,
5709 #root::sql::ExecError,
5710 > {
5711 use #root::sql::FetcherPool as _;
5712 let _key = ::std::format!("{}__contains", col);
5713 #root::query::QuerySet::<Self>::default()
5714 .filter(
5715 &_key,
5716 #root::core::SqlValue::String(substr.into()),
5717 )
5718 .fetch(pool)
5719 .await
5720 }
5721
5722 /// Fetch every row where `<col> > val`. Eloquent
5723 /// `Model::where($col, ">", $val)->get()` parity.
5724 ///
5725 /// # Errors
5726 /// As [`FetcherPool::fetch`].
5727 ///
5728 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5729 pub async fn where_gt(
5730 col: &str,
5731 val: impl ::core::convert::Into<#root::core::SqlValue>,
5732 pool: &#root::sql::Pool,
5733 ) -> ::core::result::Result<
5734 ::std::vec::Vec<Self>,
5735 #root::sql::ExecError,
5736 > {
5737 use #root::sql::FetcherPool as _;
5738 let _key = ::std::format!("{}__gt", col);
5739 #root::query::QuerySet::<Self>::default()
5740 .filter(&_key, val)
5741 .fetch(pool)
5742 .await
5743 }
5744
5745 /// Fetch every row where `<col> >= val`. Eloquent
5746 /// `Model::where($col, ">=", $val)->get()` parity.
5747 ///
5748 /// # Errors
5749 /// As [`FetcherPool::fetch`].
5750 ///
5751 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5752 pub async fn where_gte(
5753 col: &str,
5754 val: impl ::core::convert::Into<#root::core::SqlValue>,
5755 pool: &#root::sql::Pool,
5756 ) -> ::core::result::Result<
5757 ::std::vec::Vec<Self>,
5758 #root::sql::ExecError,
5759 > {
5760 use #root::sql::FetcherPool as _;
5761 let _key = ::std::format!("{}__gte", col);
5762 #root::query::QuerySet::<Self>::default()
5763 .filter(&_key, val)
5764 .fetch(pool)
5765 .await
5766 }
5767
5768 /// Fetch every row where `<col> < val`. Eloquent
5769 /// `Model::where($col, "<", $val)->get()` parity.
5770 ///
5771 /// # Errors
5772 /// As [`FetcherPool::fetch`].
5773 ///
5774 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5775 pub async fn where_lt(
5776 col: &str,
5777 val: impl ::core::convert::Into<#root::core::SqlValue>,
5778 pool: &#root::sql::Pool,
5779 ) -> ::core::result::Result<
5780 ::std::vec::Vec<Self>,
5781 #root::sql::ExecError,
5782 > {
5783 use #root::sql::FetcherPool as _;
5784 let _key = ::std::format!("{}__lt", col);
5785 #root::query::QuerySet::<Self>::default()
5786 .filter(&_key, val)
5787 .fetch(pool)
5788 .await
5789 }
5790
5791 /// Fetch every row where `<col> <= val`. Eloquent
5792 /// `Model::where($col, "<=", $val)->get()` parity.
5793 ///
5794 /// # Errors
5795 /// As [`FetcherPool::fetch`].
5796 ///
5797 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5798 pub async fn where_lte(
5799 col: &str,
5800 val: impl ::core::convert::Into<#root::core::SqlValue>,
5801 pool: &#root::sql::Pool,
5802 ) -> ::core::result::Result<
5803 ::std::vec::Vec<Self>,
5804 #root::sql::ExecError,
5805 > {
5806 use #root::sql::FetcherPool as _;
5807 let _key = ::std::format!("{}__lte", col);
5808 #root::query::QuerySet::<Self>::default()
5809 .filter(&_key, val)
5810 .fetch(pool)
5811 .await
5812 }
5813
5814 /// Fetch every row where `<col> <> val`. Eloquent
5815 /// `Model::where($col, "!=", $val)->get()` parity.
5816 ///
5817 /// # Errors
5818 /// As [`FetcherPool::fetch`].
5819 ///
5820 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5821 pub async fn where_ne(
5822 col: &str,
5823 val: impl ::core::convert::Into<#root::core::SqlValue>,
5824 pool: &#root::sql::Pool,
5825 ) -> ::core::result::Result<
5826 ::std::vec::Vec<Self>,
5827 #root::sql::ExecError,
5828 > {
5829 use #root::sql::FetcherPool as _;
5830 let _key = ::std::format!("{}__ne", col);
5831 #root::query::QuerySet::<Self>::default()
5832 .filter(&_key, val)
5833 .fetch(pool)
5834 .await
5835 }
5836
5837 /// Fetch every row matching `<col> = val` for ANY of the
5838 /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5839 /// parity. Empty `cols` returns no rows.
5840 ///
5841 /// Resolves each `&str` column to its SCHEMA-registered
5842 /// `&'static str` once and builds a single OR-composed
5843 /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5844 ///
5845 /// # Errors
5846 /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5847 /// when any column is not declared on the model.
5848 ///
5849 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5850 pub async fn where_any(
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, false, pool).await
5859 }
5860
5861 /// Fetch every row matching `<col> = val` for ALL listed
5862 /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5863 /// Empty `cols` returns every row (vacuous AND).
5864 ///
5865 /// # Errors
5866 /// As [`Self::where_any`].
5867 pub async fn where_all(
5868 cols: &[&str],
5869 val: impl ::core::convert::Into<#root::core::SqlValue>,
5870 pool: &#root::sql::Pool,
5871 ) -> ::core::result::Result<
5872 ::std::vec::Vec<Self>,
5873 #root::sql::ExecError,
5874 > {
5875 Self::__where_multi(cols, val, true, pool).await
5876 }
5877
5878 /// Internal: build a Q expression composing `cols` via
5879 /// AND (`all`) or OR (`!all`), then fetch. Backs
5880 /// `where_any` / `where_all`.
5881 #[doc(hidden)]
5882 pub async fn __where_multi(
5883 cols: &[&str],
5884 val: impl ::core::convert::Into<#root::core::SqlValue>,
5885 all: bool,
5886 pool: &#root::sql::Pool,
5887 ) -> ::core::result::Result<
5888 ::std::vec::Vec<Self>,
5889 #root::sql::ExecError,
5890 > {
5891 #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5892 .await
5893 }
5894
5895 /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5896 /// parity. PK-ordered
5897 /// is NOT guaranteed without an explicit `order_by` —
5898 /// drop into `Self::query()` for that.
5899 ///
5900 /// # Errors
5901 /// As [`FetcherPool::fetch`].
5902 ///
5903 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5904 pub async fn take(
5905 n: i64,
5906 pool: &#root::sql::Pool,
5907 ) -> ::core::result::Result<
5908 ::std::vec::Vec<Self>,
5909 #root::sql::ExecError,
5910 > {
5911 use #root::sql::FetcherPool as _;
5912 #root::query::QuerySet::<Self>::default()
5913 .limit(n)
5914 .fetch(pool)
5915 .await
5916 }
5917
5918 /// Fetch the page-th window of `per_page` rows
5919 /// (1-indexed). Eloquent
5920 /// `Model::query()->forPage($page, $perPage)->get()`
5921 /// parity. The DB scans an `OFFSET (page - 1) * per_page
5922 /// LIMIT per_page`; for large offsets this is O(N) —
5923 /// prefer keyset pagination via PK on hot paths.
5924 ///
5925 /// # Errors
5926 /// As [`FetcherPool::fetch`].
5927 ///
5928 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5929 pub async fn for_page(
5930 page: i64,
5931 per_page: i64,
5932 pool: &#root::sql::Pool,
5933 ) -> ::core::result::Result<
5934 ::std::vec::Vec<Self>,
5935 #root::sql::ExecError,
5936 > {
5937 use #root::sql::FetcherPool as _;
5938 let _offset = #root::list_params::page_offset(page, per_page);
5939 #root::query::QuerySet::<Self>::default()
5940 .limit(per_page)
5941 .offset(_offset)
5942 .fetch(pool)
5943 .await
5944 }
5945
5946 /// Eloquent `Model::paginate($per_page, $page)` — fetch
5947 /// one page of rows AND the total row count in one
5948 /// call. Returns `(rows, total)`. Two queries: a
5949 /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5950 /// for the total.
5951 ///
5952 /// Useful for paginated UIs that need both the visible
5953 /// rows AND a "Page X of Y" / total-count widget. For
5954 /// large tables prefer keyset pagination + cached count;
5955 /// every call to `paginate` re-counts the full table.
5956 ///
5957 /// Same 1-indexed `page` convention as [`Self::for_page`].
5958 ///
5959 /// # Errors
5960 /// As [`Self::for_page`] and [`Self::count`].
5961 pub async fn paginate(
5962 page: i64,
5963 per_page: i64,
5964 pool: &#root::sql::Pool,
5965 ) -> ::core::result::Result<
5966 (::std::vec::Vec<Self>, i64),
5967 #root::sql::ExecError,
5968 > {
5969 // Route through the queryset rather than `Self::count`:
5970 // the `count` inherent method is suppressed on models
5971 // that declare a field named `count` (field/shortcut
5972 // collision guard), and `paginate` must still compile
5973 // for those models.
5974 let total = {
5975 use #root::sql::CounterPool as _;
5976 #root::query::QuerySet::<Self>::default()
5977 .count(pool)
5978 .await?
5979 };
5980 let rows = Self::for_page(page, per_page, pool).await?;
5981 ::core::result::Result::Ok((rows, total))
5982 }
5983
5984 /// Bulk-update — set `set_col = set_val` on every row
5985 /// matching `where_col = where_val`. Returns affected row
5986 /// count. Eloquent
5987 /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5988 /// parity.
5989 ///
5990 /// For multi-column updates drop into the queryset
5991 /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5992 ///
5993 /// # Errors
5994 /// As [`UpdaterPool::execute_pool`].
5995 ///
5996 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5997 pub async fn update_where(
5998 where_col: &str,
5999 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
6000 set_col: &str,
6001 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
6002 pool: &#root::sql::Pool,
6003 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6004 let _query = #root::query::QuerySet::<Self>::default()
6005 .filter(where_col, where_val)
6006 .update()
6007 .set(set_col, set_val)
6008 .compile()?;
6009 #bulk_update_run
6010 }
6011
6012 /// Bulk-delete — remove every row matching
6013 /// `where_col = where_val`. Returns affected row count.
6014 /// Eloquent
6015 /// `Model::where($where_col, $where_val)->delete()` parity.
6016 ///
6017 /// # Errors
6018 /// As [`#root::sql::delete_pool`].
6019 pub async fn delete_where(
6020 where_col: &str,
6021 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
6022 pool: &#root::sql::Pool,
6023 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6024 let _query = #root::query::QuerySet::<Self>::default()
6025 .filter_op(where_col, #root::core::Op::Eq, where_val)
6026 .compile_delete()?;
6027 #bulk_delete_run
6028 }
6029
6030 /// Bulk-update — set `set_col = set_val` on EVERY row of
6031 /// this model's table. **No WHERE clause** — use with
6032 /// care. Eloquent `Model::query()->update([$col => $val])`
6033 /// parity.
6034 ///
6035 /// Use for backfills, one-shot reset flows, etc. The
6036 /// returned count is rows affected.
6037 ///
6038 /// # Errors
6039 /// As [`UpdaterPool::execute_pool`].
6040 ///
6041 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
6042 pub async fn update_all(
6043 set_col: &str,
6044 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
6045 pool: &#root::sql::Pool,
6046 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6047 let _query = #root::query::QuerySet::<Self>::default()
6048 .update()
6049 .set(set_col, set_val)
6050 .compile()?;
6051 #bulk_update_run
6052 }
6053
6054 /// Fetch every row where `<col> NOT LIKE <pattern>`.
6055 /// Eloquent `Model::whereNotLike` parity. Pattern is
6056 /// passed verbatim — caller controls `%` / `_`.
6057 ///
6058 /// # Errors
6059 /// As [`FetcherPool::fetch`].
6060 ///
6061 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6062 pub async fn where_not_like(
6063 col: &str,
6064 pattern: impl ::core::convert::Into<::std::string::String>,
6065 pool: &#root::sql::Pool,
6066 ) -> ::core::result::Result<
6067 ::std::vec::Vec<Self>,
6068 #root::sql::ExecError,
6069 > {
6070 use #root::sql::FetcherPool as _;
6071 let _key = ::std::format!("{}__not_like", col);
6072 #root::query::QuerySet::<Self>::default()
6073 .filter(
6074 &_key,
6075 #root::core::SqlValue::String(pattern.into()),
6076 )
6077 .fetch(pool)
6078 .await
6079 }
6080
6081 /// Fetch every row where `<col> NOT ILIKE <pattern>` —
6082 /// case-insensitive `NOT LIKE` (PG native, MySQL /
6083 /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
6084 ///
6085 /// # Errors
6086 /// As [`FetcherPool::fetch`].
6087 ///
6088 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6089 pub async fn where_not_ilike(
6090 col: &str,
6091 pattern: impl ::core::convert::Into<::std::string::String>,
6092 pool: &#root::sql::Pool,
6093 ) -> ::core::result::Result<
6094 ::std::vec::Vec<Self>,
6095 #root::sql::ExecError,
6096 > {
6097 use #root::sql::FetcherPool as _;
6098 let _key = ::std::format!("{}__not_ilike", col);
6099 #root::query::QuerySet::<Self>::default()
6100 .filter(
6101 &_key,
6102 #root::core::SqlValue::String(pattern.into()),
6103 )
6104 .fetch(pool)
6105 .await
6106 }
6107
6108 /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
6109 /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
6110 /// parity.
6111 ///
6112 /// # Errors
6113 /// As [`FetcherPool::fetch`].
6114 ///
6115 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6116 pub async fn where_not_between(
6117 col: &str,
6118 lo: impl ::core::convert::Into<#root::core::SqlValue>,
6119 hi: impl ::core::convert::Into<#root::core::SqlValue>,
6120 pool: &#root::sql::Pool,
6121 ) -> ::core::result::Result<
6122 ::std::vec::Vec<Self>,
6123 #root::sql::ExecError,
6124 > {
6125 use #root::sql::FetcherPool as _;
6126 let _key = ::std::format!("{}__not_between", col);
6127 let _vals = #root::core::SqlValue::List(::std::vec![
6128 ::core::convert::Into::into(lo),
6129 ::core::convert::Into::into(hi),
6130 ]);
6131 #root::query::QuerySet::<Self>::default()
6132 .filter(&_key, _vals)
6133 .fetch(pool)
6134 .await
6135 }
6136
6137 /// Returns the SQL table name for this model. Eloquent
6138 /// `$model->getTable()` parity.
6139 #[must_use]
6140 pub fn table_name() -> &'static str {
6141 <Self as #root::core::Model>::SCHEMA.table
6142 }
6143
6144 /// Returns the SQL column name of this model's primary
6145 /// key, or `None` when the model has no
6146 /// `#[rustango(primary_key)]`. Eloquent
6147 /// `$model->getKeyName()` parity.
6148 #[must_use]
6149 pub fn primary_key_column() -> ::core::option::Option<&'static str> {
6150 <Self as #root::core::Model>::SCHEMA
6151 .primary_key()
6152 .map(|f| f.column)
6153 }
6154
6155 /// Fetch every row where `<col> BETWEEN lo AND hi`
6156 /// (inclusive on both ends — same as SQL). Eloquent
6157 /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
6158 ///
6159 /// # Errors
6160 /// As [`FetcherPool::fetch`].
6161 ///
6162 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6163 pub async fn where_between(
6164 col: &str,
6165 lo: impl ::core::convert::Into<#root::core::SqlValue>,
6166 hi: impl ::core::convert::Into<#root::core::SqlValue>,
6167 pool: &#root::sql::Pool,
6168 ) -> ::core::result::Result<
6169 ::std::vec::Vec<Self>,
6170 #root::sql::ExecError,
6171 > {
6172 use #root::sql::FetcherPool as _;
6173 let _key = ::std::format!("{}__between", col);
6174 let _vals = #root::core::SqlValue::List(::std::vec![
6175 ::core::convert::Into::into(lo),
6176 ::core::convert::Into::into(hi),
6177 ]);
6178 #root::query::QuerySet::<Self>::default()
6179 .filter(&_key, _vals)
6180 .fetch(pool)
6181 .await
6182 }
6183
6184 /// Fetch the first row where `<col> = <val>`. Returns
6185 /// `Ok(None)` when no row matches. Eloquent
6186 /// `Model::firstWhere($col, $val)` parity.
6187 ///
6188 /// Thin wrapper over `QuerySet::<Self>::default()
6189 /// .filter(col, val).first(pool)`. Use this when you
6190 /// want one row identified by a non-PK column (e.g.
6191 /// `User::first_where_pool("email", "x@y.com", &pool)`).
6192 ///
6193 /// `val` accepts any value `Into<SqlValue>` so plain
6194 /// strings, ints, UUIDs, etc. all work.
6195 ///
6196 /// # Errors
6197 /// As `QuerySet::first`.
6198 pub async fn first_where(
6199 col: &str,
6200 val: impl ::core::convert::Into<#root::core::SqlValue>,
6201 pool: &#root::sql::Pool,
6202 ) -> ::core::result::Result<
6203 ::core::option::Option<Self>,
6204 #root::sql::ExecError,
6205 > {
6206 #root::query::QuerySet::<Self>::default()
6207 .filter(col, val)
6208 .first(pool)
6209 .await
6210 }
6211
6212 #value_method
6213
6214 /// Fetch the row with the largest non-NULL `field` value.
6215 /// Returns `Ok(None)` when the table is empty or every
6216 /// `field` is NULL. Thin wrapper over
6217 /// `QuerySet::<Self>::default().latest(field, pool)`.
6218 ///
6219 /// **Field name** is the Rust field ident as a string
6220 /// (not the SQL column). It is a runtime `&str`, so an
6221 /// unknown one compiles and surfaces as
6222 /// `ExecError::Query(QueryError::UnknownField)` when the
6223 /// query runs.
6224 ///
6225 /// # Errors
6226 /// As `QuerySet::latest`.
6227 pub async fn latest(
6228 field: &str,
6229 pool: &#root::sql::Pool,
6230 ) -> ::core::result::Result<
6231 ::core::option::Option<Self>,
6232 #root::sql::ExecError,
6233 > {
6234 #root::query::QuerySet::<Self>::default()
6235 .latest(field, pool)
6236 .await
6237 }
6238
6239 /// Sibling of [`Self::latest`]: fetches the row with the
6240 /// smallest non-NULL `field` value.
6241 ///
6242 /// # Errors
6243 /// As [`Self::latest`].
6244 pub async fn earliest(
6245 field: &str,
6246 pool: &#root::sql::Pool,
6247 ) -> ::core::result::Result<
6248 ::core::option::Option<Self>,
6249 #root::sql::ExecError,
6250 > {
6251 #root::query::QuerySet::<Self>::default()
6252 .earliest(field, pool)
6253 .await
6254 }
6255
6256 #count_method
6257
6258 /// `true` when the table contains at least one row.
6259 /// Eloquent `Model::query()->exists()` parity. Thin wrapper
6260 /// over `QuerySet::<Self>::default().exists(pool)`.
6261 ///
6262 /// # Errors
6263 /// As [`ExistsPool::exists`].
6264 ///
6265 /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
6266 pub async fn exists(
6267 pool: &#root::sql::Pool,
6268 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6269 use #root::sql::ExistsPool as _;
6270 #root::query::QuerySet::<Self>::default()
6271 .exists(pool)
6272 .await
6273 }
6274
6275 /// Inverse of [`Self::exists`] — returns `true` when
6276 /// the table has zero rows. Eloquent
6277 /// `Model::doesntExist()` parity.
6278 ///
6279 /// # Errors
6280 /// As [`Self::exists`].
6281 pub async fn doesnt_exist(
6282 pool: &#root::sql::Pool,
6283 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6284 Self::exists(pool).await.map(|e| !e)
6285 }
6286
6287 /// Eloquent `Model::query()->whereKey($pk)->exists()` —
6288 /// `true` when a row with primary key `pk` exists in
6289 /// the table. Sugar over
6290 /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
6291 ///
6292 /// Differs from [`Self::exists`] (which checks "any row
6293 /// in the table") by checking a specific PK existence.
6294 /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
6295 /// because the row is never materialized — the SQL is
6296 /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
6297 ///
6298 /// # Errors
6299 /// As [`#root::sql::ExistsPool::contains_pk`].
6300 pub async fn contains_pk(
6301 pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
6302 pool: &#root::sql::Pool,
6303 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6304 use #root::sql::ExistsPool as _;
6305 #root::query::QuerySet::<Self>::default()
6306 .contains_pk(pool, pk)
6307 .await
6308 }
6309
6310 #sum_method
6311 #avg_method
6312 #min_method
6313 #max_method
6314
6315 /// Fetch every row of this model from `pool`. Eloquent
6316 /// `Model::all()` parity — a thin wrapper over
6317 /// `QuerySet::<Self>::default().fetch(pool)`.
6318 ///
6319 /// **Use with care on large tables** — there's no
6320 /// pagination or limit; the entire table is materialized
6321 /// into memory. For anything beyond fixture / lookup
6322 /// tables, page through `QuerySet::<Self>::default()
6323 /// .order_by(...).limit(N).offset(M).fetch(pool)`
6324 /// or stream via `.iterator(chunk_size)`.
6325 ///
6326 /// # Errors
6327 /// As [`FetcherPool::fetch`].
6328 ///
6329 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6330 pub async fn all(
6331 pool: &#root::sql::Pool,
6332 ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
6333 {
6334 use #root::sql::FetcherPool as _;
6335 #root::query::QuerySet::<Self>::default()
6336 .fetch(pool)
6337 .await
6338 }
6339
6340 /// Look up every row whose primary key is in `pks`.
6341 /// Returns the matching rows in **inventory** order — NOT
6342 /// the order of `pks`. Empty `pks` returns an empty
6343 /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
6344 /// with a list) parity.
6345 ///
6346 /// Thin wrapper over `QuerySet::<Self>::default()
6347 /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
6348 /// Caller-supplied PKs that don't match a row are
6349 /// silently skipped (the returned vec is shorter than
6350 /// the input list). For an order-preserving / "fail
6351 /// when any missing" variant, build the queryset
6352 /// explicitly with `in_bulk` instead.
6353 ///
6354 /// Accepts any iterable whose elements are
6355 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
6356 /// `Vec<Uuid>`, etc.
6357 ///
6358 /// # Errors
6359 /// As [`FetcherPool::fetch`].
6360 ///
6361 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6362 pub async fn find_many<V>(
6363 pks: impl ::core::iter::IntoIterator<Item = V>,
6364 pool: &#root::sql::Pool,
6365 ) -> ::core::result::Result<
6366 ::std::vec::Vec<Self>,
6367 #root::sql::ExecError,
6368 >
6369 where
6370 V: ::core::convert::Into<#root::core::SqlValue>,
6371 {
6372 use #root::sql::FetcherPool as _;
6373 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6374 pks.into_iter().map(::core::convert::Into::into).collect();
6375 if _values.is_empty() {
6376 return ::core::result::Result::Ok(::std::vec::Vec::new());
6377 }
6378 let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
6379 let mut _rows = ::std::vec::Vec::new();
6380 for _keys in #root::sql::__rustango_in_batches::<Self>(pool, _values)? {
6381 _rows.extend(
6382 #root::query::QuerySet::<Self>::default()
6383 .filter(&_key, #root::core::SqlValue::List(_keys))
6384 .fetch(pool)
6385 .await?,
6386 );
6387 }
6388 ::core::result::Result::Ok(_rows)
6389 }
6390
6391 /// Look up the row whose primary key equals `pk`. Returns
6392 /// `Ok(None)` when no row matches, rather than erroring.
6393 /// Eloquent `Model::find`
6394 /// shape — accepts any value `Into<SqlValue>`.
6395 ///
6396 /// One-liner shortcut for the common
6397 /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6398 /// .limit(1).fetch(pool).await?.into_iter().next()`
6399 /// dance.
6400 ///
6401 /// # Errors
6402 /// As [`FetcherPool::fetch`].
6403 ///
6404 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6405 pub async fn find(
6406 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6407 pool: &#root::sql::Pool,
6408 ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6409 {
6410 use #root::sql::FetcherPool as _;
6411 let _pk_val: #root::core::SqlValue = pk.into();
6412 let mut _rows: ::std::vec::Vec<Self> =
6413 #root::query::QuerySet::<Self>::default()
6414 .filter(::core::stringify!(#pk_ident), _pk_val)
6415 .limit(1)
6416 .fetch(pool)
6417 .await?;
6418 ::core::result::Result::Ok(_rows.into_iter().next())
6419 }
6420
6421 /// Look up the row whose primary key equals `pk`. Errors
6422 /// when no row matches — the throwing counterpart of
6423 /// [`Self::find`]. Eloquent `Model::findOrFail`
6424 /// parity.
6425 ///
6426 /// Translates the miss into
6427 /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6428 /// so callers can `?`-bubble straight through the typical
6429 /// `ExecError` error chain.
6430 ///
6431 /// # Errors
6432 /// As [`Self::find`]; additionally
6433 /// [`sqlx::Error::RowNotFound`] when no row matches.
6434 ///
6435 /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6436 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6437 pub async fn find_or_fail(
6438 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6439 pool: &#root::sql::Pool,
6440 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6441 match Self::find(pk, pool).await? {
6442 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6443 ::core::option::Option::None => ::core::result::Result::Err(
6444 #root::sql::ExecError::Driver(
6445 #root::sql::sqlx::Error::RowNotFound,
6446 ),
6447 ),
6448 }
6449 }
6450
6451 /// Look up multiple rows by primary key, **failing** if
6452 /// any requested PK is missing. Eloquent
6453 /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6454 /// inventory order (NOT request order — same as
6455 /// [`Self::find_many`]).
6456 ///
6457 /// Empty `pks` returns an empty `Vec` (no rows requested
6458 /// → nothing to fail on).
6459 ///
6460 /// Differs from [`Self::find_many`] in that this method
6461 /// requires every PK to resolve. If even one is missing,
6462 /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6463 /// in `ExecError::Driver`). Rows are deduped at the SQL
6464 /// layer, so passing the same PK twice in `pks` counts as
6465 /// one expected row.
6466 ///
6467 /// # Errors
6468 /// As [`Self::find_many`], plus `RowNotFound` when the
6469 /// returned row count is less than the count of distinct
6470 /// requested PKs.
6471 pub async fn find_many_or_fail<V>(
6472 pks: impl ::core::iter::IntoIterator<Item = V>,
6473 pool: &#root::sql::Pool,
6474 ) -> ::core::result::Result<
6475 ::std::vec::Vec<Self>,
6476 #root::sql::ExecError,
6477 >
6478 where
6479 V: ::core::convert::Into<#root::core::SqlValue>,
6480 {
6481 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6482 pks.into_iter().map(::core::convert::Into::into).collect();
6483 if _values.is_empty() {
6484 return ::core::result::Result::Ok(::std::vec::Vec::new());
6485 }
6486 // Dedup the requested PK list before counting so
6487 // duplicate-PK requests don't false-fail (the SQL
6488 // `IN (...)` clause naturally dedups too).
6489 let mut _seen: ::std::collections::HashSet<
6490 ::std::string::String,
6491 > = ::std::collections::HashSet::new();
6492 for v in &_values {
6493 _seen.insert(v.to_display_string());
6494 }
6495 let _expected = _seen.len();
6496 let _rows = Self::find_many(_values, pool).await?;
6497 if _rows.len() < _expected {
6498 return ::core::result::Result::Err(
6499 #root::sql::ExecError::Driver(
6500 #root::sql::sqlx::Error::RowNotFound,
6501 ),
6502 );
6503 }
6504 ::core::result::Result::Ok(_rows)
6505 }
6506
6507 /// Find by primary key or run `fallback` to produce a
6508 /// default row to return. Eloquent
6509 /// `Model::findOr($pk, fn() => …)` parity.
6510 ///
6511 /// Unlike [`Self::find_or_fail`] (which raises on
6512 /// miss), this is the "give me something sensible"
6513 /// branch: typical use is "fetch the user's row, else
6514 /// fall back to an anonymous/guest stub".
6515 ///
6516 /// `fallback` runs only when no row matches — the DB
6517 /// round-trip happens unconditionally.
6518 ///
6519 /// # Errors
6520 /// As [`Self::find`].
6521 pub async fn find_or<F>(
6522 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6523 pool: &#root::sql::Pool,
6524 fallback: F,
6525 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6526 where
6527 F: ::core::ops::FnOnce() -> Self,
6528 {
6529 ::core::result::Result::Ok(
6530 Self::find(pk, pool).await?.unwrap_or_else(fallback),
6531 )
6532 }
6533
6534 /// Same as [`Self::find_or`] but also returns a `bool`
6535 /// indicating whether the row was found in the DB
6536 /// (`true`) or freshly built from `fallback` (`false`).
6537 /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6538 /// in PHP the returned model also exposes `->exists`;
6539 /// here that's surfaced as the second tuple element.
6540 ///
6541 /// Useful for edit-or-create form handlers where the
6542 /// caller needs to know whether to PATCH or POST when
6543 /// the user submits the form.
6544 ///
6545 /// # Errors
6546 /// As [`Self::find`].
6547 pub async fn find_or_new<F>(
6548 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6549 pool: &#root::sql::Pool,
6550 fallback: F,
6551 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6552 where
6553 F: ::core::ops::FnOnce() -> Self,
6554 {
6555 match Self::find(pk, pool).await? {
6556 ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6557 ::core::option::Option::None => {
6558 ::core::result::Result::Ok((fallback(), false))
6559 }
6560 }
6561 }
6562
6563 /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6564 /// [`Self::find_or_new`] but **persists** the new row
6565 /// when the PK isn't found. Returns `(row, exists: bool)`
6566 /// — `exists=true` when the PK was found,
6567 /// `false` when a fresh row was inserted.
6568 ///
6569 /// ```ignore
6570 /// let (post, found) = Post::find_or_insert(
6571 /// pk,
6572 /// &pool,
6573 /// || Post { id: Auto::default(), title: "new".into() },
6574 /// ).await?;
6575 /// // `found` true → returned existing row;
6576 /// // `found` false → fallback was inserted, `post.id` populated.
6577 /// ```
6578 ///
6579 /// **Caveat**: two concurrent calls that both miss the
6580 /// find can both INSERT, violating uniqueness. Wrap in a
6581 /// transaction or rely on a UNIQUE constraint + handle
6582 /// the conflict error if you need race-free semantics.
6583 ///
6584 /// # Errors
6585 /// As [`Self::find`] and [`Self::save_pool`].
6586 pub async fn find_or_insert<F>(
6587 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6588 pool: &#root::sql::Pool,
6589 fallback: F,
6590 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6591 where
6592 F: ::core::ops::FnOnce() -> Self,
6593 {
6594 if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6595 return ::core::result::Result::Ok((_row, true));
6596 }
6597 let mut _new = fallback();
6598 _new.save_pool(pool).await?;
6599 ::core::result::Result::Ok((_new, false))
6600 }
6601
6602 /// Fetch the first row of the table, or run `fallback`
6603 /// when the table is empty. Eloquent
6604 /// `Model::firstOr(fn() => …)` parity.
6605 ///
6606 /// # Errors
6607 /// As [`Self::first`].
6608 pub async fn first_or<F>(
6609 pool: &#root::sql::Pool,
6610 fallback: F,
6611 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6612 where
6613 F: ::core::ops::FnOnce() -> Self,
6614 {
6615 // Route through the queryset rather than `Self::first`,
6616 // which is suppressed on models with a field named
6617 // `first` (field/shortcut collision guard).
6618 ::core::result::Result::Ok(
6619 #root::query::QuerySet::<Self>::default()
6620 .first(pool)
6621 .await?
6622 .unwrap_or_else(fallback),
6623 )
6624 }
6625
6626 /// Fetch exactly one row matching `<col> = <val>`. Errors
6627 /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6628 /// or more than one matches
6629 /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6630 /// Eloquent `Model::sole($col, $val)` parity.
6631 ///
6632 /// # Errors
6633 /// As [`Self::where_`] plus the explicit
6634 /// `RowNotFound` / `MultipleRowsReturned` cases above.
6635 pub async fn sole(
6636 col: &str,
6637 val: impl ::core::convert::Into<#root::core::SqlValue>,
6638 pool: &#root::sql::Pool,
6639 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6640 let mut _rows = Self::where_(col, val, pool).await?;
6641 match _rows.len() {
6642 0 => ::core::result::Result::Err(
6643 #root::sql::ExecError::Driver(
6644 #root::sql::sqlx::Error::RowNotFound,
6645 ),
6646 ),
6647 1 => ::core::result::Result::Ok(_rows.remove(0)),
6648 n => ::core::result::Result::Err(
6649 #root::sql::ExecError::MultipleRowsReturned {
6650 op: "sole",
6651 table: <Self as #root::core::Model>::SCHEMA.name,
6652 count: n,
6653 },
6654 ),
6655 }
6656 }
6657 }
6658 } else {
6659 quote!()
6660 };
6661
6662 // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6663 // the non-audited `_pool` methods but execute against an open
6664 // `PoolTx` so the writes participate in the caller's transaction.
6665 // Auditing inside TX is deferred; these always use the plain
6666 // executor primitives regardless of whether the model is audited.
6667 let tx_insert_method = if fields.has_auto {
6668 let pushes = &fields.insert_pushes;
6669 let returning_cols = &fields.returning_cols;
6670 quote! {
6671 /// Insert this row inside an open transaction, populating
6672 /// any `Auto<T>` PK from the auto-assigned value. Works
6673 /// against any backend that `tx` wraps.
6674 ///
6675 /// # Errors
6676 /// As [`Self::insert_pool`].
6677 pub async fn insert_tx(
6678 &mut self,
6679 tx: &mut #root::sql::PoolTx<'_>,
6680 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6681 let mut _columns: ::std::vec::Vec<&'static str> =
6682 ::std::vec::Vec::new();
6683 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6684 ::std::vec::Vec::new();
6685 #( #pushes )*
6686 let _query = #root::core::InsertQuery::new(
6687 <Self as #root::core::Model>::SCHEMA,
6688 _columns,
6689 _values,
6690 )
6691 .returning(::std::vec![ #( #returning_cols ),* ]);
6692 let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6693 #root::sql::apply_auto_pk(_result, self)
6694 }
6695 }
6696 } else {
6697 let insert_columns = &fields.insert_columns;
6698 let insert_values = &fields.insert_values;
6699 quote! {
6700 /// Insert this row inside an open transaction.
6701 ///
6702 /// # Errors
6703 /// As [`Self::insert_pool`].
6704 pub async fn insert_tx(
6705 &self,
6706 tx: &mut #root::sql::PoolTx<'_>,
6707 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6708 let _query = #root::core::InsertQuery::new(
6709 <Self as #root::core::Model>::SCHEMA,
6710 ::std::vec![ #( #insert_columns ),* ],
6711 ::std::vec![ #( #insert_values ),* ],
6712 );
6713 #root::sql::insert_tx(tx, &_query).await
6714 }
6715 }
6716 };
6717
6718 let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6719 let pk_column_lit = pk_col.as_str();
6720 let assignments = &fields.update_assignments;
6721 let dispatch_unset = if fields.pk_is_auto {
6722 quote! {
6723 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6724 return self.insert_tx(tx).await.map(|()| 1u64);
6725 }
6726 }
6727 } else {
6728 quote!()
6729 };
6730 let write = restore_auto_now_unless(
6731 &fields.auto_now_idents,
6732 &save_ret,
6733 &save_keep,
6734 "e! {
6735 let _query = #root::core::UpdateQuery::new(
6736 <Self as #root::core::Model>::SCHEMA,
6737 ::std::vec![ #( #assignments ),* ],
6738 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6739 #pk_column_lit,
6740 #root::core::Op::Eq,
6741 ::core::convert::Into::<#root::core::SqlValue>::into(
6742 ::core::clone::Clone::clone(&self.#pk_ident)
6743 ),
6744 )),
6745 );
6746 let _affected = #root::sql::update_tx(tx, &_query).await?;
6747 ::core::result::Result::Ok(_affected)
6748 },
6749 );
6750 quote! {
6751 /// Save this row inside an open transaction. `INSERT` when
6752 /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6753 /// PK. Works against any backend that `tx` wraps.
6754 ///
6755 /// # Errors
6756 /// As [`Self::save_pool`].
6757 pub async fn save_tx(
6758 &mut self,
6759 tx: &mut #root::sql::PoolTx<'_>,
6760 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6761 #dispatch_unset
6762 #write
6763 }
6764 }
6765 } else {
6766 quote!()
6767 };
6768
6769 let tx_delete_method = {
6770 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6771 let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6772 if let Some(pk_ident) = pk_ident_for_tx {
6773 quote! {
6774 /// Delete the row identified by this instance's PK
6775 /// inside an open transaction. Works against any backend
6776 /// that `tx` wraps.
6777 ///
6778 /// # Errors
6779 /// As [`Self::delete_pool`].
6780 pub async fn delete_tx(
6781 &self,
6782 tx: &mut #root::sql::PoolTx<'_>,
6783 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6784 let _query = #root::core::DeleteQuery::by_pk(
6785 <Self as #root::core::Model>::SCHEMA,
6786 #pk_column_lit,
6787 ::core::convert::Into::<#root::core::SqlValue>::into(
6788 ::core::clone::Clone::clone(&self.#pk_ident)
6789 ),
6790 );
6791 #root::sql::delete_tx(tx, &_query).await
6792 }
6793 }
6794 } else {
6795 quote!()
6796 }
6797 };
6798
6799 // Update emission captures both BEFORE and AFTER state — runs an
6800 // extra SELECT against `_executor` BEFORE the UPDATE, captures
6801 // each tracked field's prior value, then after the UPDATE diffs
6802 // against the in-memory `&self`. `diff_changes` drops unchanged
6803 // columns so the JSON only contains the actual delta.
6804 //
6805 // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6806 // and binds `_audit_before_pairs`; `audit_update_post` runs
6807 // after the UPDATE and emits the PendingEntry.
6808 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6809 audited_fields
6810 {
6811 if tracked.is_empty() {
6812 (quote!(), quote!())
6813 } else {
6814 let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
6815 let pk_column_lit_for_select =
6816 primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6817 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6818 if fields.pk_is_auto {
6819 quote!(self.#pk_ident.get().copied().unwrap_or_default())
6820 } else {
6821 quote!(::core::clone::Clone::clone(&self.#pk_ident))
6822 }
6823 } else {
6824 quote!(0_i64)
6825 };
6826 let before_pairs = tracked.iter().map(|c| {
6827 let column_lit = c.column.as_str();
6828 let value_ty = &c.value_ty;
6829 quote! {
6830 (
6831 #column_lit,
6832 match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6833 &_audit_before_row, #column_lit,
6834 ) {
6835 ::core::result::Result::Ok(v) => #root::core::__json_value(&v),
6836 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6837 },
6838 )
6839 }
6840 });
6841 let pre = quote! {
6842 let _audit_before_query = #root::audit::before_image_query(
6843 <Self as #root::core::Model>::SCHEMA,
6844 #pk_column_lit_for_select,
6845 ::core::convert::Into::<#root::core::SqlValue>::into(#pk_value_for_bind),
6846 &[ #( #tracked_cols ),* ],
6847 );
6848 // A failed pre-read must not let the UPDATE run unaudited (#1907).
6849 let _audit_before_pairs:
6850 ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6851 #root::sql::select_rows_on(&mut *_executor, &_audit_before_query)
6852 .await?
6853 .into_iter()
6854 .next()
6855 .map(|_audit_before_row| ::std::vec![ #( #before_pairs ),* ]);
6856 };
6857 let post = quote! {
6858 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6859 let _audit_after = self.__rustango_audit_pairs();
6860 if let ::core::option::Option::Some(_audit_entry) =
6861 #root::audit::PendingEntry::update_diff(
6862 <Self as #root::core::Model>::SCHEMA.table,
6863 self.__rustango_audit_pk(),
6864 &_audit_before,
6865 &_audit_after,
6866 )
6867 {
6868 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6869 }
6870 }
6871 };
6872 (pre, post)
6873 }
6874 } else {
6875 (quote!(), quote!())
6876 };
6877
6878 // Bulk-insert audit: one `Create` entry per row once RETURNING has
6879 // set its PK, written in one batched INSERT.
6880 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6881 quote! {
6882 let _audit_entries: ::std::vec::Vec<#root::audit::PendingEntry> = rows
6883 .iter()
6884 .map(|_row| _row.__rustango_audit_entry(#root::audit::AuditOp::Create))
6885 .collect();
6886 #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6887 }
6888 } else {
6889 quote!()
6890 };
6891
6892 let save_method = if fields.pk_is_auto {
6893 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6894 let pk_column_lit = pk_column.as_str();
6895 let assignments = &fields.update_assignments;
6896 let upsert_cols = &fields.upsert_update_columns;
6897 let upsert_pushes = &fields.insert_pushes;
6898 // Read before the pushes, which fill each unset id.
6899 let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
6900 fields.uuid_v7_fields.iter().cloned().unzip();
6901 let upsert_returning = &fields.returning_cols;
6902 let upsert_auto_assigns = &fields.auto_assigns;
6903 let auto_now_idents = &fields.auto_now_idents;
6904 // Conflict target: prefer the first declared `unique_together`
6905 // when it exists. Plain `Auto<T>` PKs are server-assigned via
6906 // `BIGSERIAL` and never collide on insert, so a PK-only target
6907 // would silently turn `upsert()` into "always-insert" for
6908 // surrogate-PK models with composite UNIQUE constraints — see
6909 // `RolePermission` / `UserRole` / `UserPermission` in the
6910 // tenancy permission engine. When no `unique_together` is
6911 // declared we keep the PK target (the original behaviour).
6912 let upsert_target_columns: Vec<String> =
6913 upsert_unique.map_or_else(|| vec![pk_column.clone()], <[String]>::to_vec);
6914 let upsert_target_lits = upsert_target_columns
6915 .iter()
6916 .map(String::as_str)
6917 .collect::<Vec<_>>();
6918 let conflict_clause = if fields.upsert_update_columns.is_empty() {
6919 quote!(#root::core::ConflictClause::DoNothing)
6920 } else {
6921 quote!(#root::core::ConflictClause::DoUpdate {
6922 target: ::std::vec![ #( #upsert_target_lits ),* ],
6923 update_columns: ::std::vec![ #( #upsert_cols ),* ],
6924 })
6925 };
6926 // Audited: the upsert reports whether it inserted or updated (#1795).
6927 let (upsert_run, audit_upsert_emit) = if audited_fields.is_some() {
6928 (
6929 quote! {
6930 let (_returning_row_v, _audit_op) =
6931 #root::audit::upsert_returning_on(&mut *_executor, &query).await?;
6932 },
6933 quote! {
6934 let _audit_entry = self.__rustango_audit_entry(_audit_op);
6935 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6936 },
6937 )
6938 } else {
6939 (
6940 quote! {
6941 let _returning_row_v =
6942 #root::sql::__macro_internals::insert_returning_on(_executor, &query)
6943 .await?;
6944 },
6945 quote!(),
6946 )
6947 };
6948 let save_on_write = restore_auto_now_unless(
6949 auto_now_idents,
6950 &save_ret,
6951 &save_keep,
6952 "e! {
6953 #audit_update_pre
6954 let _query = #root::core::UpdateQuery::new(
6955 <Self as #root::core::Model>::SCHEMA,
6956 ::std::vec![ #( #assignments ),* ],
6957 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6958 #pk_column_lit,
6959 #root::core::Op::Eq,
6960 ::core::convert::Into::<#root::core::SqlValue>::into(
6961 ::core::clone::Clone::clone(&self.#pk_ident)
6962 ),
6963 )),
6964 );
6965 let _affected = #root::sql::__macro_internals::update_on(
6966 #executor_passes_to_data_write,
6967 &_query,
6968 ).await?;
6969 #audit_update_post
6970 ::core::result::Result::Ok(_affected)
6971 },
6972 );
6973 let upsert_write = restore_auto_now_unless(
6974 auto_now_idents,
6975 "e!(::core::result::Result<(), #root::sql::ExecError>),
6976 "e!(_saved.is_ok()),
6977 "e! {
6978 // The UPDATE branch writes `auto_now` too, so stamp it like `save`.
6979 #( self.#auto_now_idents = #root::sql::Auto::Set(#root::__chrono::Utc::now()); )*
6980 #( #upsert_pushes )*
6981 let query = #root::core::InsertQuery::new(
6982 <Self as #root::core::Model>::SCHEMA,
6983 _columns,
6984 _values,
6985 )
6986 .returning(::std::vec![ #( #upsert_returning ),* ])
6987 .on_conflict(#conflict_clause)
6988 .generated(_generated);
6989 #upsert_run
6990 let _returning_row = &_returning_row_v;
6991 #( #upsert_auto_assigns )*
6992 #audit_upsert_emit
6993 ::core::result::Result::Ok(())
6994 },
6995 );
6996 Some(quote! {
6997 /// Insert this row if its `Auto<T>` primary key is
6998 /// `Unset`, otherwise update the existing row matching the
6999 /// PK. The caller doesn't need to pick `insert` vs the
7000 /// bulk-update path manually.
7001 ///
7002 /// On the insert branch, populates the PK from `RETURNING`
7003 /// (same behavior as `insert`). On the update branch,
7004 /// writes every non-PK column back; if no row matches the
7005 /// PK, returns `Ok(())` silently.
7006 ///
7007 /// Only generated when the primary key is declared as
7008 /// `Auto<T>`. Models with a manually-managed PK must use
7009 /// `insert` or the QuerySet update builder.
7010 ///
7011 /// **Postgres only — prefer [`Self::save_pool`].** This takes a
7012 /// driver-specific `PgPool`, so it does not exist at all on a
7013 /// build without the `postgres` feature: selecting `sqlite`
7014 /// makes the method vanish rather than fail with anything that
7015 /// names the cause.
7016 ///
7017 /// [`Self::save_pool`] takes `rustango::sql::Pool` and works on
7018 /// all three backends. The naming is inverted — the short name
7019 /// is the narrow one — and that is tracked in #1293.
7020 ///
7021 /// # Errors
7022 /// Returns [`#root::sql::ExecError`] for SQL-writing
7023 /// or driver failures.
7024 #[cfg(feature = "postgres")]
7025 pub async fn save(
7026 &mut self,
7027 pool: &#root::sql::sqlx::PgPool,
7028 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7029 #pool_to_save_on
7030 }
7031
7032 /// Like [`Self::save`] but accepts any sqlx executor —
7033 /// `&PgPool`, `&mut PgConnection`, or a transaction. The
7034 /// escape hatch for tenant-scoped writes: schema-mode
7035 /// tenants share the registry pool but rely on a per-
7036 /// checkout `SET search_path`, so passing `&PgPool` would
7037 /// silently hit the wrong schema. Acquire a connection
7038 /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
7039 ///
7040 /// # Errors
7041 /// As [`Self::save`].
7042 #[cfg(feature = "postgres")]
7043 pub async fn save_on #executor_generics (
7044 &mut self,
7045 #executor_param,
7046 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7047 #executor_where
7048 {
7049 // #1029 — INSERT writes exactly one row → 1; UPDATE returns
7050 // the rows-affected count (0 when the PK no longer exists).
7051 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
7052 return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
7053 }
7054 #save_on_write
7055 }
7056
7057 /// Per-call override for the audit source. Runs
7058 /// [`Self::save_on`] inside an [`#root::audit::with_source`]
7059 /// scope so the resulting audit entry records `source`
7060 /// instead of the task-local default. Useful for seed
7061 /// scripts and one-off CLI tools that don't sit inside an
7062 /// admin handler. The override applies only to this call;
7063 /// no global state changes.
7064 ///
7065 /// # Errors
7066 /// As [`Self::save_on`].
7067 #[cfg(feature = "postgres")]
7068 pub async fn save_on_with #executor_generics (
7069 &mut self,
7070 #executor_param,
7071 source: #root::audit::AuditSource,
7072 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7073 #executor_where
7074 {
7075 #root::audit::with_source(source, self.save_on(_executor)).await
7076 }
7077
7078 /// Insert this row or update it in-place if the primary key already
7079 /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
7080 ///
7081 /// With `Auto::Unset` PK the server assigns a new key and no conflict
7082 /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
7083 /// inserted if absent or all non-PK columns are overwritten if present.
7084 ///
7085 /// # Errors
7086 /// As [`Self::insert_on`].
7087 #[cfg(feature = "postgres")]
7088 pub async fn upsert(
7089 &mut self,
7090 pool: &#root::sql::sqlx::PgPool,
7091 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7092 #pool_to_upsert_on
7093 }
7094
7095 /// Like [`Self::upsert`] but accepts any sqlx executor.
7096 /// See [`Self::save_on`] for tenancy-scoped rationale.
7097 ///
7098 /// # Errors
7099 /// As [`Self::upsert`].
7100 #[cfg(feature = "postgres")]
7101 pub async fn upsert_on #executor_generics (
7102 &mut self,
7103 #executor_param,
7104 ) -> ::core::result::Result<(), #root::sql::ExecError>
7105 #executor_where
7106 {
7107 let mut _columns: ::std::vec::Vec<&'static str> =
7108 ::std::vec::Vec::new();
7109 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7110 ::std::vec::Vec::new();
7111 #[allow(unused_mut)]
7112 let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7113 #(
7114 if matches!(&self.#gen_idents, #root::sql::Auto::Unset) {
7115 _generated.push(#gen_cols);
7116 }
7117 )*
7118 #upsert_write
7119 }
7120 })
7121 } else {
7122 None
7123 };
7124
7125 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
7126 let pk_column_lit = pk_column.as_str();
7127 // Optional `soft_delete_on` / `restore_on` companions when the
7128 // model has a `#[rustango(soft_delete)]` column. They land
7129 // alongside the regular `delete_on` so callers have both
7130 // options — a hard delete (audit-tracked as a real DELETE) and
7131 // a logical delete (audit-tracked as an UPDATE setting the
7132 // deleted_at column to NOW()).
7133 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
7134 let col_lit = col;
7135 let sd_field_ident = fields
7136 .soft_delete_field_ident
7137 .clone()
7138 .expect("soft_delete_column without ident");
7139 quote! {
7140 /// Soft-delete this row by setting its
7141 /// `#[rustango(soft_delete)]` column to `NOW()`.
7142 /// The row stays in the table; query helpers can
7143 /// filter it out by checking the column for `IS NOT
7144 /// NULL`.
7145 ///
7146 /// # Errors
7147 /// As [`Self::delete`].
7148 #[cfg(feature = "postgres")]
7149 pub async fn soft_delete_on #executor_generics (
7150 &self,
7151 #executor_param,
7152 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7153 #executor_where
7154 {
7155 let _now = #root::__chrono::Utc::now();
7156 let _query = #root::soft_delete::__mark_query(
7157 <Self as #root::core::Model>::SCHEMA,
7158 #col_lit,
7159 #pk_column_lit,
7160 ::core::convert::Into::<#root::core::SqlValue>::into(
7161 ::core::clone::Clone::clone(&self.#pk_ident)
7162 ),
7163 ::core::option::Option::Some(_now),
7164 );
7165 let _affected = #root::sql::__macro_internals::update_on(
7166 #executor_passes_to_data_write,
7167 &_query,
7168 ).await?;
7169 #audit_softdelete_emit
7170 ::core::result::Result::Ok(_affected)
7171 }
7172
7173 /// Inverse of [`Self::soft_delete_on`] — clears the
7174 /// soft-delete column back to NULL so the row is
7175 /// considered live again.
7176 ///
7177 /// # Errors
7178 /// As [`Self::delete`].
7179 #[cfg(feature = "postgres")]
7180 pub async fn restore_on #executor_generics (
7181 &self,
7182 #executor_param,
7183 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7184 #executor_where
7185 {
7186 let _query = #root::soft_delete::__mark_query(
7187 <Self as #root::core::Model>::SCHEMA,
7188 #col_lit,
7189 #pk_column_lit,
7190 ::core::convert::Into::<#root::core::SqlValue>::into(
7191 ::core::clone::Clone::clone(&self.#pk_ident)
7192 ),
7193 ::core::option::Option::None,
7194 );
7195 let _affected = #root::sql::__macro_internals::update_on(
7196 #executor_passes_to_data_write,
7197 &_query,
7198 ).await?;
7199 #audit_restore_emit
7200 ::core::result::Result::Ok(_affected)
7201 }
7202
7203 /// Tri-dialect counterpart of [`Self::soft_delete_on`]
7204 /// — takes [`#root::sql::Pool`] and dispatches per
7205 /// backend. Eloquent `Model::delete()` semantics on
7206 /// soft-delete-enabled models (closes #821 partial).
7207 ///
7208 /// Sets the `#[rustango(soft_delete)]` column to
7209 /// `NOW()` on every backend. Query helpers
7210 /// (`QuerySet::active()` / `only_trashed()`,
7211 /// `soft_delete::active_filter` /
7212 /// `compose_with_active`) filter trashed rows out by
7213 /// reading `IS NULL` on the same column. Keyed on the PK;
7214 /// global scopes do not apply.
7215 ///
7216 /// # Errors
7217 /// As [`#root::sql::update_pool`].
7218 pub async fn soft_delete(
7219 &self,
7220 pool: &#root::sql::Pool,
7221 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7222 let _now = #root::__chrono::Utc::now();
7223 let _query = #root::soft_delete::__mark_query(
7224 <Self as #root::core::Model>::SCHEMA,
7225 #col_lit,
7226 #pk_column_lit,
7227 ::core::convert::Into::<#root::core::SqlValue>::into(
7228 ::core::clone::Clone::clone(&self.#pk_ident)
7229 ),
7230 ::core::option::Option::Some(_now),
7231 );
7232 #pool_softdelete_run
7233 }
7234
7235 /// Tri-dialect counterpart of [`Self::restore_on`].
7236 /// Clears the `#[rustango(soft_delete)]` column back
7237 /// to `NULL`, marking the row live again. Eloquent
7238 /// `Model::restore()` parity. Keyed on the PK; global
7239 /// scopes do not apply.
7240 ///
7241 /// # Errors
7242 /// As [`#root::sql::update_pool`].
7243 pub async fn restore(
7244 &self,
7245 pool: &#root::sql::Pool,
7246 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7247 let _query = #root::soft_delete::__mark_query(
7248 <Self as #root::core::Model>::SCHEMA,
7249 #col_lit,
7250 #pk_column_lit,
7251 ::core::convert::Into::<#root::core::SqlValue>::into(
7252 ::core::clone::Clone::clone(&self.#pk_ident)
7253 ),
7254 ::core::option::Option::None,
7255 );
7256 #pool_restore_run
7257 }
7258
7259 /// Hard-delete this row, ignoring the soft-delete
7260 /// column. Eloquent `Model::forceDelete()` parity —
7261 /// the escape hatch when you need to actually purge
7262 /// data (GDPR, fixture cleanup, etc.).
7263 ///
7264 /// Equivalent to [`Self::delete_pool`] (the framework's
7265 /// non-soft delete) — exposed under the Eloquent name
7266 /// for muscle-memory + so soft-delete-enabled models
7267 /// have all three operations (soft / restore / force)
7268 /// in one place.
7269 ///
7270 /// # Errors
7271 /// As [`Self::delete_pool`].
7272 pub async fn force_delete(
7273 &self,
7274 pool: &#root::sql::Pool,
7275 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7276 Self::delete_pool(self, pool).await
7277 }
7278
7279 /// Returns `true` when this row is soft-deleted (its
7280 /// `#[rustango(soft_delete)]` column is currently
7281 /// set — Eloquent `$model->trashed()` parity).
7282 ///
7283 /// Pure in-memory predicate over `&self`; does not
7284 /// hit the database. Useful in admin/template code
7285 /// like `{% if post.trashed() %}…{% endif %}` and in
7286 /// guard clauses on restore/force-delete flows.
7287 pub fn trashed(&self) -> bool {
7288 ::core::option::Option::is_some(&self.#sd_field_ident)
7289 }
7290
7291 /// Fetch every row whose `#[rustango(soft_delete)]`
7292 /// column is `NULL` (a.k.a. the live, non-trashed
7293 /// rows). Eloquent's default `Model::all()` behavior on
7294 /// a soft-delete model (Eloquent auto-scopes trashed
7295 /// rows out; rustango doesn't have auto-scoping yet —
7296 /// see issue #820 — so this is the explicit shortcut).
7297 ///
7298 /// One-liner over `QuerySet::<Self>::default()
7299 /// .active().fetch(pool)`. Closes #821 partial.
7300 ///
7301 /// # Errors
7302 /// As [`#root::sql::FetcherPool::fetch`].
7303 pub async fn active(
7304 pool: &#root::sql::Pool,
7305 ) -> ::core::result::Result<
7306 ::std::vec::Vec<Self>,
7307 #root::sql::ExecError,
7308 > {
7309 use #root::sql::FetcherPool as _;
7310 #root::query::QuerySet::<Self>::default()
7311 .active()
7312 .fetch(pool)
7313 .await
7314 }
7315
7316 /// Fetch ONLY soft-deleted rows. Eloquent
7317 /// `Model::onlyTrashed()->get()` parity — drives the
7318 /// admin "Trash" page, restore flows, GDPR purge
7319 /// scans, etc.
7320 ///
7321 /// One-liner over `QuerySet::<Self>::default()
7322 /// .only_trashed().fetch(pool)`. Closes #821
7323 /// partial.
7324 ///
7325 /// # Errors
7326 /// As [`#root::sql::FetcherPool::fetch`].
7327 pub async fn only_trashed(
7328 pool: &#root::sql::Pool,
7329 ) -> ::core::result::Result<
7330 ::std::vec::Vec<Self>,
7331 #root::sql::ExecError,
7332 > {
7333 use #root::sql::FetcherPool as _;
7334 #root::query::QuerySet::<Self>::default()
7335 .only_trashed()
7336 .fetch(pool)
7337 .await
7338 }
7339
7340 /// Fetch every row, both live and soft-deleted.
7341 /// Eloquent `Model::withTrashed()->get()` parity.
7342 ///
7343 /// Today every queryset already includes trashed rows
7344 /// (rustango has no global-scope tracking yet — issue
7345 /// #820), so this is functionally equivalent to
7346 /// [`Self::all`]. Exposed as a named shortcut so
7347 /// soft-delete-aware code reads `Model::with_trashed_pool`
7348 /// rather than `Model::all_pool` — keeps intent visible
7349 /// in callers and stays correct when auto-scoping lands.
7350 ///
7351 /// Closes #821 partial.
7352 ///
7353 /// # Errors
7354 /// As [`#root::sql::FetcherPool::fetch`].
7355 pub async fn with_trashed(
7356 pool: &#root::sql::Pool,
7357 ) -> ::core::result::Result<
7358 ::std::vec::Vec<Self>,
7359 #root::sql::ExecError,
7360 > {
7361 use #root::sql::FetcherPool as _;
7362 #root::query::QuerySet::<Self>::default()
7363 .with_trashed()
7364 .fetch(pool)
7365 .await
7366 }
7367
7368 }
7369 } else {
7370 quote!()
7371 };
7372 quote! {
7373 /// Delete the row identified by this instance's primary key.
7374 ///
7375 /// Returns the number of rows affected (0 or 1).
7376 ///
7377 /// **Postgres only — prefer [`Self::delete_pool`].** This takes
7378 /// a driver-specific `PgPool`, so it does not exist on a build
7379 /// without the `postgres` feature. [`Self::delete_pool`] takes
7380 /// `rustango::sql::Pool` and works on all three. The naming is
7381 /// inverted; tracked in #1293.
7382 ///
7383 /// # Errors
7384 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7385 /// driver failures.
7386 #[cfg(feature = "postgres")]
7387 pub async fn delete(
7388 &self,
7389 pool: &#root::sql::sqlx::PgPool,
7390 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7391 #pool_to_delete_on
7392 }
7393
7394 /// Like [`Self::delete`] but accepts any sqlx executor —
7395 /// for tenant-scoped deletes against an explicitly-acquired
7396 /// connection. See [`Self::save_on`] for the rationale.
7397 ///
7398 /// # Errors
7399 /// As [`Self::delete`].
7400 #[cfg(feature = "postgres")]
7401 pub async fn delete_on #executor_generics (
7402 &self,
7403 #executor_param,
7404 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7405 #executor_where
7406 {
7407 let query = #root::core::DeleteQuery::by_pk(
7408 <Self as #root::core::Model>::SCHEMA,
7409 #pk_column_lit,
7410 ::core::convert::Into::<#root::core::SqlValue>::into(
7411 ::core::clone::Clone::clone(&self.#pk_ident)
7412 ),
7413 );
7414 let _affected = #root::sql::__macro_internals::delete_on(
7415 #executor_passes_to_data_write,
7416 &query,
7417 ).await?;
7418 #audit_delete_emit
7419 ::core::result::Result::Ok(_affected)
7420 }
7421
7422 /// Per-call audit-source override for [`Self::delete_on`].
7423 /// See [`Self::save_on_with`] for shape rationale.
7424 ///
7425 /// # Errors
7426 /// As [`Self::delete_on`].
7427 #[cfg(feature = "postgres")]
7428 pub async fn delete_on_with #executor_generics (
7429 &self,
7430 #executor_param,
7431 source: #root::audit::AuditSource,
7432 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7433 #executor_where
7434 {
7435 #root::audit::with_source(source, self.delete_on(_executor)).await
7436 }
7437 #pool_delete_method
7438 #pool_insert_method
7439 #audit_entry_method
7440 #pool_save_method
7441 #refresh_replicate_methods
7442 #tx_delete_method
7443 #tx_insert_method
7444 #tx_save_method
7445 #soft_delete_methods
7446
7447 /// Returns `true` when `other` represents the same DB
7448 /// row as `self` — i.e. their primary keys compare
7449 /// equal. Eloquent `$model->is($other)` parity.
7450 ///
7451 /// Because both arguments are typed `&Self`, the
7452 /// model/table check is automatic — `Post::is` cannot
7453 /// be invoked against a `Comment` at compile time. Only
7454 /// the PK has to be compared at runtime.
7455 pub fn is(&self, other: &Self) -> bool {
7456 self.#pk_ident == other.#pk_ident
7457 }
7458
7459 /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7460 /// parity.
7461 pub fn is_not(&self, other: &Self) -> bool {
7462 self.#pk_ident != other.#pk_ident
7463 }
7464
7465 /// Returns this row's primary-key value as an
7466 /// [`#root::core::SqlValue`]. Eloquent
7467 /// `$model->getKey()` parity.
7468 ///
7469 /// Useful when you need to thread the PK through a
7470 /// generic `Into<SqlValue>`-bound API without knowing the
7471 /// concrete PK type (`i64` vs `Uuid` vs `String`).
7472 #[must_use]
7473 pub fn get_key(&self) -> #root::core::SqlValue {
7474 ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7475 }
7476
7477 }
7478 });
7479
7480 let insert_method = if fields.has_auto {
7481 let pushes = &fields.insert_pushes;
7482 let returning_cols = &fields.returning_cols;
7483 let auto_assigns = &fields.auto_assigns;
7484 quote! {
7485 /// Insert this row into its table. Skips columns whose
7486 /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7487 /// sequence fills them in, then reads each `Auto` column
7488 /// back via `RETURNING` and stores it on `self`.
7489 ///
7490 /// **Postgres only — prefer [`Self::insert_pool`].** This takes
7491 /// a driver-specific `PgPool`, so it does not exist on a build
7492 /// without the `postgres` feature. [`Self::insert_pool`] takes
7493 /// `rustango::sql::Pool` and works on all three. The naming is
7494 /// inverted; tracked in #1293.
7495 ///
7496 /// # Errors
7497 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7498 /// driver failures.
7499 #[cfg(feature = "postgres")]
7500 pub async fn insert(
7501 &mut self,
7502 pool: &#root::sql::sqlx::PgPool,
7503 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7504 #pool_to_insert_on
7505 }
7506
7507 /// Like [`Self::insert`] but accepts any sqlx executor.
7508 /// See [`Self::save_on`] for tenancy-scoped rationale.
7509 ///
7510 /// # Errors
7511 /// As [`Self::insert`].
7512 #[cfg(feature = "postgres")]
7513 pub async fn insert_on #executor_generics (
7514 &mut self,
7515 #executor_param,
7516 ) -> ::core::result::Result<(), #root::sql::ExecError>
7517 #executor_where
7518 {
7519 let mut _columns: ::std::vec::Vec<&'static str> =
7520 ::std::vec::Vec::new();
7521 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7522 ::std::vec::Vec::new();
7523 #( #pushes )*
7524 let query = #root::core::InsertQuery::new(
7525 <Self as #root::core::Model>::SCHEMA,
7526 _columns,
7527 _values,
7528 )
7529 .returning(::std::vec![ #( #returning_cols ),* ]);
7530 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7531 #executor_passes_to_data_write,
7532 &query,
7533 ).await?;
7534 let _returning_row = &_returning_row_v;
7535 #( #auto_assigns )*
7536 #audit_insert_emit
7537 ::core::result::Result::Ok(())
7538 }
7539
7540 /// Per-call audit-source override for [`Self::insert_on`].
7541 /// See [`Self::save_on_with`] for shape rationale.
7542 ///
7543 /// # Errors
7544 /// As [`Self::insert_on`].
7545 #[cfg(feature = "postgres")]
7546 pub async fn insert_on_with #executor_generics (
7547 &mut self,
7548 #executor_param,
7549 source: #root::audit::AuditSource,
7550 ) -> ::core::result::Result<(), #root::sql::ExecError>
7551 #executor_where
7552 {
7553 #root::audit::with_source(source, self.insert_on(_executor)).await
7554 }
7555 }
7556 } else {
7557 let insert_columns = &fields.insert_columns;
7558 let insert_values = &fields.insert_values;
7559 quote! {
7560 /// Insert this row into its table.
7561 ///
7562 /// # Errors
7563 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7564 /// driver failures.
7565 #[cfg(feature = "postgres")]
7566 pub async fn insert(
7567 &self,
7568 pool: &#root::sql::sqlx::PgPool,
7569 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7570 self.insert_on(pool).await
7571 }
7572
7573 /// Like [`Self::insert`] but accepts any sqlx executor.
7574 /// See [`Self::save_on`] for tenancy-scoped rationale.
7575 ///
7576 /// # Errors
7577 /// As [`Self::insert`].
7578 #[cfg(feature = "postgres")]
7579 pub async fn insert_on<'_c, _E>(
7580 &self,
7581 _executor: _E,
7582 ) -> ::core::result::Result<(), #root::sql::ExecError>
7583 where
7584 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7585 {
7586 let query = #root::core::InsertQuery::new(
7587 <Self as #root::core::Model>::SCHEMA,
7588 ::std::vec![ #( #insert_columns ),* ],
7589 ::std::vec![ #( #insert_values ),* ],
7590 );
7591 #root::sql::__macro_internals::insert_on(_executor, &query).await
7592 }
7593 }
7594 };
7595
7596 let bulk_insert_method = if fields.has_auto {
7597 let cols_no_auto = &fields.bulk_columns_no_auto;
7598 let cols_all = &fields.bulk_columns_all;
7599 let pushes_no_auto = &fields.bulk_pushes_no_auto;
7600 let pushes_all = &fields.bulk_pushes_all;
7601 let returning_cols = &fields.returning_cols;
7602 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7603 let uniformity = &fields.bulk_auto_uniformity;
7604 let fill_in_place = &fields.bulk_fill_in_place;
7605 // Nothing to read back when every Auto field is filled Rust-side (#1934).
7606 let read_back = if fields.returning_cols.is_empty() {
7607 quote!()
7608 } else {
7609 quote! {
7610 if _returned.len() != rows.len() {
7611 return ::core::result::Result::Err(
7612 #root::sql::ExecError::Sql(
7613 #root::sql::SqlError::BulkInsertReturningMismatch {
7614 expected: rows.len(),
7615 actual: _returned.len(),
7616 }
7617 )
7618 );
7619 }
7620 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7621 #auto_assigns_for_row
7622 }
7623 }
7624 };
7625 // The DB-filled `Auto` picks the path; Rust-filled ones fill per row (#1950).
7626 let first_auto_ident = fields
7627 .first_db_auto
7628 .as_ref()
7629 .map(|(ident, _)| ident)
7630 .or(fields.first_auto_ident.as_ref())
7631 .expect("has_auto implies first_auto_ident is Some");
7632 quote! {
7633 /// Bulk-insert `rows`: one statement, or one per chunk in a
7634 /// transaction past the bind cap. Every row's
7635 /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7636 /// (sequence fills them in) or uniformly `Auto::Set(_)`
7637 /// (caller-supplied values). Mixed Set/Unset is rejected
7638 /// — call `insert` per row for that case.
7639 ///
7640 /// Empty slice is a no-op. Each row's `Auto` fields are
7641 /// populated from the `RETURNING` clause in input order
7642 /// before this returns.
7643 ///
7644 /// # Errors
7645 /// Returns [`#root::sql::ExecError`] for validation,
7646 /// SQL-writing, mixed-Auto rejection, or driver failures.
7647 #[cfg(feature = "postgres")]
7648 pub async fn bulk_insert(
7649 rows: &mut [Self],
7650 pool: &#root::sql::sqlx::PgPool,
7651 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7652 #pool_to_bulk_insert_mut
7653 }
7654
7655 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7656 /// See [`Self::save_on`] for tenancy-scoped rationale.
7657 ///
7658 /// # Errors
7659 /// As [`Self::bulk_insert`].
7660 #[cfg(feature = "postgres")]
7661 pub async fn bulk_insert_on #executor_generics (
7662 rows: &mut [Self],
7663 #executor_param,
7664 ) -> ::core::result::Result<(), #root::sql::ExecError>
7665 #executor_where
7666 {
7667 if rows.is_empty() {
7668 return ::core::result::Result::Ok(());
7669 }
7670 let _first_unset = matches!(
7671 rows[0].#first_auto_ident,
7672 #root::sql::Auto::Unset
7673 );
7674 #( #uniformity )*
7675 for _row in rows.iter_mut() {
7676 #( #fill_in_place )*
7677 }
7678
7679 let mut _all_rows: ::std::vec::Vec<
7680 ::std::vec::Vec<#root::core::SqlValue>,
7681 > = ::std::vec::Vec::with_capacity(rows.len());
7682 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7683 for _row in rows.iter() {
7684 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7685 ::std::vec::Vec::new();
7686 #( #pushes_no_auto )*
7687 _all_rows.push(_row_vals);
7688 }
7689 ::std::vec![ #( #cols_no_auto ),* ]
7690 } else {
7691 for _row in rows.iter() {
7692 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7693 ::std::vec::Vec::new();
7694 #( #pushes_all )*
7695 _all_rows.push(_row_vals);
7696 }
7697 ::std::vec![ #( #cols_all ),* ]
7698 };
7699
7700 let _query = #root::core::BulkInsertQuery::new(
7701 <Self as #root::core::Model>::SCHEMA,
7702 _columns,
7703 _all_rows,
7704 )
7705 .returning(::std::vec![ #( #returning_cols ),* ]);
7706 let _returned = #root::sql::__macro_internals::bulk_insert_on(
7707 #executor_passes_to_data_write,
7708 &_query,
7709 ).await?;
7710 #read_back
7711 #audit_bulk_insert_emit
7712 ::core::result::Result::Ok(())
7713 }
7714 }
7715 } else {
7716 let cols_all = &fields.bulk_columns_all;
7717 let pushes_all = &fields.bulk_pushes_all;
7718 quote! {
7719 /// Bulk-insert `rows`: one statement, or one per chunk in a
7720 /// transaction past the bind cap. Every row's
7721 /// fields are written verbatim — there are no `Auto<T>`
7722 /// fields on this model.
7723 ///
7724 /// Empty slice is a no-op.
7725 ///
7726 /// # Errors
7727 /// Returns [`#root::sql::ExecError`] for validation,
7728 /// SQL-writing, or driver failures.
7729 #[cfg(feature = "postgres")]
7730 pub async fn bulk_insert(
7731 rows: &[Self],
7732 pool: &#root::sql::sqlx::PgPool,
7733 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7734 #pool_to_bulk_insert_on
7735 }
7736
7737 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7738 /// See [`Self::save_on`] for tenancy-scoped rationale.
7739 ///
7740 /// # Errors
7741 /// As [`Self::bulk_insert`].
7742 #[cfg(feature = "postgres")]
7743 pub async fn bulk_insert_on #executor_generics (
7744 rows: &[Self],
7745 #executor_param,
7746 ) -> ::core::result::Result<(), #root::sql::ExecError>
7747 #executor_where
7748 {
7749 if rows.is_empty() {
7750 return ::core::result::Result::Ok(());
7751 }
7752 let mut _all_rows: ::std::vec::Vec<
7753 ::std::vec::Vec<#root::core::SqlValue>,
7754 > = ::std::vec::Vec::with_capacity(rows.len());
7755 for _row in rows.iter() {
7756 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7757 ::std::vec::Vec::new();
7758 #( #pushes_all )*
7759 _all_rows.push(_row_vals);
7760 }
7761 let _query = #root::core::BulkInsertQuery::new(
7762 <Self as #root::core::Model>::SCHEMA,
7763 ::std::vec![ #( #cols_all ),* ],
7764 _all_rows,
7765 );
7766 let _ = #root::sql::__macro_internals::bulk_insert_on(
7767 #executor_passes_to_data_write,
7768 &_query,
7769 ).await?;
7770 #audit_bulk_insert_emit
7771 ::core::result::Result::Ok(())
7772 }
7773 }
7774 };
7775
7776 // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7777 // (no postgres-feature gate); routes through the existing
7778 // `bulk_insert_pool` + per-dialect conflict writer.
7779 //
7780 // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7781 // sequence picks the PK for fresh inserts; the UPDATE branch never
7782 // touches the Auto column.
7783 // Audited models audit each written row; MySQL, with no RETURNING,
7784 // is refused there (#1747, #1795).
7785 let run_conflict_bulk_insert = match (audited_fields.is_some(), primary_key.is_some()) {
7786 (false, _) => quote!(#root::sql::bulk_insert_pool(pool, &_query).await),
7787 (true, true) => quote! {
7788 #root::audit::bulk_insert_with_audit::<Self>(pool, &_query, |_r: &Self, _op| {
7789 _r.__rustango_audit_entry(_op)
7790 })
7791 .await
7792 },
7793 (true, false) => quote! {
7794 ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
7795 table: <Self as #root::core::Model>::SCHEMA.table,
7796 })
7797 },
7798 };
7799 let bulk_upsert_pool_method = {
7800 let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
7801 fields.uuid_v7_fields.iter().cloned().unzip();
7802 // Pick the "no Auto" columns when the model has Auto fields,
7803 // else every column.
7804 let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7805 (
7806 fields.bulk_columns_no_auto.clone(),
7807 fields.bulk_pushes_no_auto.clone(),
7808 )
7809 } else {
7810 (
7811 fields.bulk_columns_all.clone(),
7812 fields.bulk_pushes_all.clone(),
7813 )
7814 };
7815 quote! {
7816 /// Tri-dialect bulk insert that updates on conflict — the
7817 /// "import a batch idempotently" shape. Issue #267 / T1.5.
7818 ///
7819 /// Per-row values are extracted and lowered into a
7820 /// [`#root::core::BulkInsertQuery`] with
7821 /// `on_conflict = DoUpdate { target, update_columns }`. The
7822 /// writer dispatches per-dialect:
7823 /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7824 /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)`; refused when
7825 /// `target` is not the only unique key, since MySQL matches every UNIQUE index
7826 ///
7827 /// `target` names the column(s) whose unique constraint
7828 /// defines the conflict (typically a `unique` or
7829 /// `unique_together` natural-key column, NOT the `Auto<T>`
7830 /// PK). `update_cols` names the columns to overwrite on
7831 /// conflict — every other column is left untouched on the
7832 /// existing row.
7833 ///
7834 /// Auto-PK rows must all have `Auto::Unset` (the sequence
7835 /// picks the PK on insert; the update path never touches
7836 /// the Auto column). Auto-set rows trigger a hard error.
7837 /// Rows repeating a `target` key: the last one wins.
7838 /// Empty slice is a no-op. On an audited model each written
7839 /// row gets a `create` or `update` audit row; MySQL refuses it.
7840 ///
7841 /// # Errors
7842 /// Returns [`#root::sql::ExecError`] for validation,
7843 /// SQL-writing, or driver failures.
7844 pub async fn bulk_upsert_pool(
7845 rows: &[Self],
7846 target: &[&'static str],
7847 update_cols: &[&'static str],
7848 pool: &#root::sql::Pool,
7849 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7850 if rows.is_empty() {
7851 return ::core::result::Result::Ok(());
7852 }
7853 let mut _all_rows: ::std::vec::Vec<
7854 ::std::vec::Vec<#root::core::SqlValue>,
7855 > = ::std::vec::Vec::with_capacity(rows.len());
7856 for _row in rows.iter() {
7857 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7858 ::std::vec::Vec::new();
7859 #( #upsert_pushes )*
7860 _all_rows.push(_row_vals);
7861 }
7862 #[allow(unused_mut)]
7863 let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7864 #(
7865 if rows.iter().all(|_r| matches!(&_r.#gen_idents, #root::sql::Auto::Unset)) {
7866 _generated.push(#gen_cols);
7867 }
7868 )*
7869 let _query = #root::core::BulkInsertQuery::new(
7870 <Self as #root::core::Model>::SCHEMA,
7871 ::std::vec![ #( #upsert_cols ),* ],
7872 _all_rows,
7873 )
7874 .on_conflict_do_update(target, update_cols)
7875 .generated(_generated);
7876 #run_conflict_bulk_insert
7877 }
7878
7879 /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7880 /// skip rows that would violate a unique constraint. Issue
7881 /// #267 / T1.5. Same per-dialect dispatch as
7882 /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7883 /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7884 /// <pivot> = <pivot>` (MySQL no-op write).
7885 ///
7886 /// # Errors
7887 /// As [`Self::bulk_upsert_pool`].
7888 pub async fn bulk_insert_or_ignore_pool(
7889 rows: &[Self],
7890 pool: &#root::sql::Pool,
7891 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7892 if rows.is_empty() {
7893 return ::core::result::Result::Ok(());
7894 }
7895 let mut _all_rows: ::std::vec::Vec<
7896 ::std::vec::Vec<#root::core::SqlValue>,
7897 > = ::std::vec::Vec::with_capacity(rows.len());
7898 for _row in rows.iter() {
7899 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7900 ::std::vec::Vec::new();
7901 #( #upsert_pushes )*
7902 _all_rows.push(_row_vals);
7903 }
7904 let _query = #root::core::BulkInsertQuery::new(
7905 <Self as #root::core::Model>::SCHEMA,
7906 ::std::vec![ #( #upsert_cols ),* ],
7907 _all_rows,
7908 )
7909 .on_conflict_do_nothing();
7910 #run_conflict_bulk_insert
7911 }
7912 }
7913 };
7914
7915 // Ergonomic `Model::bulk_update(objs, fields)`.
7916 // The SQL/IR/executor stack
7917 // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7918 // `write_bulk_update_*` writers) already existed; what was missing
7919 // was the per-model constructor that maps `&[Self]` + a runtime
7920 // column list into rows of `[pk, col_vals…]` so callers don't
7921 // hand-build the IR. Emitted only when the model has a primary key
7922 // (the PK is the join key and can't itself be updated).
7923 let bulk_update_rows_run = if audited_fields.is_some() {
7924 quote!(#root::audit::bulk_update_with_audit::<Self>(pool, &_query, |_r: &Self| {
7925 _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
7926 })
7927 .await)
7928 } else {
7929 quote!(#root::sql::bulk_update_pool(pool, &_query).await)
7930 };
7931 let bulk_update_method = match &fields.primary_key {
7932 None => quote! {},
7933 Some((pk_ident, pk_col)) => {
7934 // One pair of match arms per non-PK column: a validation arm
7935 // resolving the runtime name to its `&'static str` column,
7936 // and a value arm pushing that field off the row.
7937 let mut col_arms: Vec<TokenStream2> = Vec::new();
7938 let mut val_arms: Vec<TokenStream2> = Vec::new();
7939 for entry in &fields.column_entries {
7940 if &entry.column == pk_col {
7941 continue;
7942 }
7943 let col = &entry.column;
7944 let ident = &entry.ident;
7945 col_arms.push(quote! { #col => #col, });
7946 val_arms.push(quote! {
7947 #col => _row_vals.push(
7948 ::core::convert::Into::<#root::core::SqlValue>::into(
7949 ::core::clone::Clone::clone(&_o.#ident)
7950 )
7951 ),
7952 });
7953 }
7954 quote! {
7955 /// Write per-row-different values for the named `fields`
7956 /// across every object in `objs`, matched by primary key.
7957 /// Past the backend's bind cap the rows go in batches inside
7958 /// one transaction, so the update is all or none.
7959 ///
7960 /// `fields` names the **columns** to update. The primary
7961 /// key identifies each row and cannot itself be updated
7962 /// (pass it and you get
7963 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7964 /// Empty `objs` or `fields` is a no-op returning `0`.
7965 /// Objects whose PK matches no row are simply not updated.
7966 /// Returns the number of rows affected.
7967 ///
7968 /// Tri-dialect: lowers to a
7969 /// [`#root::core::BulkUpdateQuery`] and dispatches
7970 /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7971 /// a CTE + correlated subquery on SQLite, an inner
7972 /// `JOIN (VALUES …)` on MySQL.
7973 ///
7974 /// # Errors
7975 /// [`#root::core::QueryError::UnknownField`] for a name
7976 /// that isn't a column on this model,
7977 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7978 /// `fields` names the PK, or [`#root::sql::ExecError`] for
7979 /// SQL-writing / driver failures.
7980 pub async fn bulk_update(
7981 objs: &[Self],
7982 fields: &[&str],
7983 pool: &#root::sql::Pool,
7984 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7985 if objs.is_empty() || fields.is_empty() {
7986 return ::core::result::Result::Ok(0);
7987 }
7988 let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7989 let mut _update_columns: ::std::vec::Vec<&'static str> =
7990 ::std::vec::Vec::with_capacity(fields.len());
7991 for &_f in fields {
7992 let _col: &'static str = match _f {
7993 #pk_col => {
7994 return ::core::result::Result::Err(
7995 ::core::convert::Into::into(
7996 #root::core::QueryError::BulkUpdatePrimaryKey {
7997 model: _model_name,
7998 field: ::std::string::ToString::to_string(_f),
7999 }
8000 )
8001 );
8002 }
8003 #( #col_arms )*
8004 _ => {
8005 return ::core::result::Result::Err(
8006 ::core::convert::Into::into(
8007 #root::core::QueryError::UnknownField {
8008 model: _model_name,
8009 field: ::std::string::ToString::to_string(_f),
8010 }
8011 )
8012 );
8013 }
8014 };
8015 // A model whose only field is the primary key emits no
8016 // non-PK arms, so every arm above diverges and this push
8017 // is genuinely unreachable — correct, since such a model
8018 // has nothing to bulk-update. Scoped to the statement
8019 // rather than the fn so a real unreachability elsewhere
8020 // in this body still warns. Without it every PK-only
8021 // model warns in the *user's* crate, which they cannot
8022 // act on (#1290).
8023 #[allow(unreachable_code)]
8024 _update_columns.push(_col);
8025 }
8026 let mut _rows: ::std::vec::Vec<
8027 ::std::vec::Vec<#root::core::SqlValue>,
8028 > = ::std::vec::Vec::with_capacity(objs.len());
8029 for _o in objs.iter() {
8030 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
8031 ::std::vec::Vec::with_capacity(fields.len() + 1);
8032 // PK first — the writers expect `[pk, …update cols]`.
8033 _row_vals.push(
8034 ::core::convert::Into::<#root::core::SqlValue>::into(
8035 ::core::clone::Clone::clone(&_o.#pk_ident)
8036 )
8037 );
8038 for &_f in fields {
8039 match _f {
8040 #( #val_arms )*
8041 // Unreachable: every name was validated
8042 // against the same arm set above.
8043 _ => {}
8044 }
8045 }
8046 _rows.push(_row_vals);
8047 }
8048 let _query = #root::core::BulkUpdateQuery::new(
8049 <Self as #root::core::Model>::SCHEMA,
8050 _update_columns,
8051 _rows,
8052 );
8053 #bulk_update_rows_run
8054 }
8055 }
8056 }
8057 };
8058
8059 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
8060 quote! {
8061 /// Hidden runtime accessor for the primary-key value as a
8062 /// [`SqlValue`]. Used by reverse-relation helpers
8063 /// (`<parent>::<child>_set`) emitted from sibling models'
8064 /// FK fields. Not part of the public API.
8065 #[doc(hidden)]
8066 pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
8067 ::core::convert::Into::<#root::core::SqlValue>::into(
8068 ::core::clone::Clone::clone(&self.#pk_ident)
8069 )
8070 }
8071 }
8072 });
8073
8074 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
8075 let column_arms = fields.column_entries.iter().map(|entry| {
8076 let col = &entry.column;
8077 let ident = &entry.ident;
8078 quote! {
8079 #col => ::core::option::Option::Some(
8080 ::core::convert::Into::<#root::core::SqlValue>::into(
8081 ::core::clone::Clone::clone(&self.#ident)
8082 )
8083 ),
8084 }
8085 });
8086 quote! {
8087 impl #root::sql::HasPkValue for #struct_name {
8088 fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
8089 ::core::convert::Into::<#root::core::SqlValue>::into(
8090 ::core::clone::Clone::clone(&self.#pk_ident)
8091 )
8092 }
8093 fn __rustango_column_value(
8094 &self,
8095 column: &str,
8096 ) -> ::core::option::Option<#root::core::SqlValue> {
8097 match column {
8098 #( #column_arms )*
8099 _ => ::core::option::Option::None,
8100 }
8101 }
8102 }
8103 }
8104 });
8105
8106 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
8107
8108 // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
8109 // dispatch to the right per-backend body without the macro emitting
8110 // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
8111 // so audited models with non-Auto PKs (which still go through
8112 // `insert_one_with_audit` → `apply_auto_pk`) link.
8113 let assign_auto_pk_pool_impl = {
8114 let auto_assigns = &fields.auto_assigns;
8115 // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
8116 // so the body is structurally identical to `auto_assigns` —
8117 // only the helper name swaps from `try_get_returning` to
8118 // `try_get_returning_sqlite` so the closure typechecks against
8119 // a `SqliteRow` instead of a `PgRow`.
8120 let auto_assigns_sqlite: Vec<TokenStream2> = fields
8121 .auto_field_idents
8122 .iter()
8123 .map(|(ident, column, uuid)| {
8124 // A UUID a column default wrote is TEXT, not the 16 bytes sqlx binds.
8125 if *uuid {
8126 return quote! {
8127 self.#ident = #root::sql::Auto::Set(#root::sql::try_get_flat_sqlite(
8128 _returning_row, #column
8129 )?);
8130 };
8131 }
8132 quote! {
8133 self.#ident = #root::sql::try_get_returning_sqlite(
8134 _returning_row, #column
8135 )?;
8136 }
8137 })
8138 .collect();
8139 // #1028 — decode each `generated_as` column from the same
8140 // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
8141 // own type drives the decode (no `Auto<T>` wrapper).
8142 let generated_assigns: Vec<TokenStream2> = fields
8143 .generated_field_idents
8144 .iter()
8145 .map(|(ident, column)| {
8146 quote! {
8147 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
8148 }
8149 })
8150 .collect();
8151 let generated_assigns_sqlite: Vec<TokenStream2> = fields
8152 .generated_field_idents
8153 .iter()
8154 .map(|(ident, column)| {
8155 quote! {
8156 self.#ident = #root::sql::try_get_returning_sqlite(
8157 _returning_row, #column
8158 )?;
8159 }
8160 })
8161 .collect();
8162 let mysql_body = if let Some((first, value_ty)) = fields.first_db_auto.as_ref() {
8163 // The MySQL `LAST_INSERT_ID()` is always i64. Route through
8164 // `MysqlAutoIdSet` so Auto<i32> narrows safely and
8165 // Auto<Uuid>/etc. fail to link against MySQL (intended —
8166 // those models can't use AUTO_INCREMENT). The trait is only
8167 // touched on the MySQL arm at runtime, so PG-only consumers
8168 // never see the bound failure.
8169 //
8170 // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
8171 // Auto<i64> PK + auto_now_add timestamp) errored hard at
8172 // runtime with "multi-column RETURNING". MySQL has no
8173 // multi-column RETURNING semantic and a follow-up SELECT
8174 // would need cross-trait plumbing. Pragmatic shape: succeed
8175 // with the FIRST Auto field populated from LAST_INSERT_ID();
8176 // any other Auto fields stay `Auto::Unset`. Callers that
8177 // need the DB-defaulted timestamp / UUID can re-fetch the
8178 // row by PK after `save_pool`. Fixes the cookbook chapter
8179 // 12 dialect divergence.
8180 quote! {
8181 let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
8182 ::rustango_from_mysql_auto_id(_id)?;
8183 self.#first = #root::sql::Auto::Set(_converted);
8184 ::core::result::Result::Ok(())
8185 }
8186 } else {
8187 quote! {
8188 let _ = _id;
8189 ::core::result::Result::Ok(())
8190 }
8191 };
8192 quote! {
8193 impl #root::sql::AssignAutoPkPool for #struct_name {
8194 fn __rustango_assign_from_pg_row(
8195 &mut self,
8196 _returning_row: &#root::sql::PgReturningRow,
8197 ) -> ::core::result::Result<(), #root::sql::ExecError> {
8198 #( #auto_assigns )*
8199 #( #generated_assigns )*
8200 ::core::result::Result::Ok(())
8201 }
8202 fn __rustango_assign_from_mysql_id(
8203 &mut self,
8204 _id: i64,
8205 ) -> ::core::result::Result<(), #root::sql::ExecError> {
8206 #mysql_body
8207 }
8208 fn __rustango_assign_from_sqlite_row(
8209 &mut self,
8210 _returning_row: &#root::sql::SqliteReturningRow,
8211 ) -> ::core::result::Result<(), #root::sql::ExecError> {
8212 #( #auto_assigns_sqlite )*
8213 #( #generated_assigns_sqlite )*
8214 ::core::result::Result::Ok(())
8215 }
8216 }
8217 }
8218 };
8219
8220 let from_aliased_row_inits = &fields.from_aliased_row_inits;
8221 let aliased_row_helper = quote! {
8222 /// Decode a row's aliased target columns (produced by
8223 /// `select_related`'s LEFT JOIN) into a fresh instance of
8224 /// this model. Reads each column via
8225 /// `format!("{prefix}__{col}")`, matching the alias the
8226 /// SELECT writer emitted. Slice 9.0d.
8227 #[doc(hidden)]
8228 #[cfg(feature = "postgres")]
8229 pub fn __rustango_from_aliased_row(
8230 row: &#root::sql::sqlx::postgres::PgRow,
8231 prefix: &str,
8232 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8233 ::core::result::Result::Ok(Self {
8234 #( #from_aliased_row_inits ),*
8235 })
8236 }
8237 };
8238 // v0.23.0-batch8 — MySQL counterpart, gated through the
8239 // cfg-aware macro_rules so PG-only builds expand to nothing.
8240 let from_aliased_row_inits_my = &fields.from_aliased_row_inits_my;
8241 let aliased_row_helper_my = quote! {
8242 #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
8243 #( #from_aliased_row_inits_my ),*
8244 });
8245 };
8246
8247 // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
8248 // pattern + cfg gate on the `sqlite` feature.
8249 let aliased_row_helper_sqlite = quote! {
8250 #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
8251 #( #from_aliased_row_inits ),*
8252 });
8253 };
8254
8255 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
8256 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
8257 let load_related_impl_sqlite =
8258 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
8259
8260 // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
8261 // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
8262 // default `Self::objects()`. Each accessor returns a fresh
8263 // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
8264 // methods the user defined.
8265 let extra_manager_fns: Vec<TokenStream2> = manager_fns
8266 .iter()
8267 .map(|fn_ident| {
8268 let model_name_str = struct_name.to_string();
8269 let fn_name_str = fn_ident.to_string();
8270 let doc = format!(
8271 "Custom-named QuerySet accessor for [`{model_name_str}`]. \
8272 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
8273 equivalent to `Self::objects()`. Chains with any \
8274 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
8275 extension methods."
8276 );
8277 quote! {
8278 #[doc = #doc]
8279 #[must_use]
8280 pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
8281 #root::query::QuerySet::new()
8282 }
8283 }
8284 })
8285 .collect();
8286
8287 quote! {
8288 impl #struct_name {
8289 /// Start a new `QuerySet` over this model.
8290 #[must_use]
8291 pub fn objects() -> #root::query::QuerySet<#struct_name> {
8292 #root::query::QuerySet::new()
8293 }
8294
8295 /// Eloquent-shape alias of [`Self::objects`]. Returns
8296 /// a fresh `QuerySet<Self>` ready for `.filter()` /
8297 /// `.where_()` / etc. Matches Laravel muscle-memory:
8298 ///
8299 /// ```ignore
8300 /// // Eloquent: Post::query()->where('published', true)
8301 /// // rustango: Post::query().filter("published", true)
8302 /// // or: Post::objects().filter("published", true)
8303 /// ```
8304 ///
8305 /// Both names point at the same underlying constructor;
8306 /// neither is preferred.
8307 #[must_use]
8308 pub fn query() -> #root::query::QuerySet<#struct_name> {
8309 #root::query::QuerySet::new()
8310 }
8311
8312 #( #extra_manager_fns )*
8313
8314 #insert_method
8315
8316 #bulk_insert_method
8317
8318 #bulk_upsert_pool_method
8319
8320 #bulk_update_method
8321
8322 #save_method
8323
8324 #pk_methods
8325
8326 #pk_value_helper
8327
8328 #aliased_row_helper
8329
8330 #column_consts
8331 }
8332
8333 #aliased_row_helper_my
8334
8335 #aliased_row_helper_sqlite
8336
8337 #load_related_impl
8338
8339 #load_related_impl_my
8340
8341 #load_related_impl_sqlite
8342
8343 #has_pk_value_impl
8344
8345 #fk_pk_access_impl
8346
8347 #assign_auto_pk_pool_impl
8348 }
8349}
8350
8351/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
8352/// `auto_assigns` but reading from `_returning_row` and writing to
8353/// `_row_mut` instead of `self`.
8354fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
8355 let root = rustango_root();
8356 let lines = fields.auto_field_idents.iter().map(|(ident, column, _)| {
8357 let col_lit = column.as_str();
8358 quote! {
8359 _row_mut.#ident = #root::sql::sqlx::Row::try_get(
8360 _returning_row,
8361 #col_lit,
8362 )?;
8363 }
8364 });
8365 quote! { #( #lines )* }
8366}
8367
8368/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
8369fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
8370 let lines = entries.iter().map(|e| {
8371 let ident = &e.ident;
8372 let col_ty = column_type_ident(ident);
8373 quote! {
8374 #[allow(non_upper_case_globals)]
8375 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
8376 }
8377 });
8378 quote! { #(#lines)* }
8379}
8380
8381/// Emit a hidden per-model module carrying one zero-sized type per field,
8382/// each with a `Column` impl pointing back at the model.
8383fn column_module_tokens(
8384 module_ident: &syn::Ident,
8385 struct_name: &syn::Ident,
8386 entries: &[ColumnEntry],
8387) -> TokenStream2 {
8388 let root = rustango_root();
8389 let items = entries.iter().map(|e| {
8390 let col_ty = column_type_ident(&e.ident);
8391 let value_ty = &e.value_ty;
8392 let name = &e.name;
8393 let column = &e.column;
8394 let field_type_tokens = &e.field_type_tokens;
8395 quote! {
8396 #[derive(::core::clone::Clone, ::core::marker::Copy)]
8397 pub struct #col_ty;
8398
8399 impl #root::core::Column for #col_ty {
8400 type Model = super::#struct_name;
8401 type Value = #value_ty;
8402 const NAME: &'static str = #name;
8403 const COLUMN: &'static str = #column;
8404 const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
8405 }
8406 }
8407 });
8408 quote! {
8409 #[doc(hidden)]
8410 #[allow(non_camel_case_types, non_snake_case)]
8411 pub mod #module_ident {
8412 // Re-import the parent scope so field types referencing
8413 // sibling models (e.g. `ForeignKey<Author>`) resolve
8414 // inside this submodule. Without this we'd hit
8415 // `proc_macro_derive_resolution_fallback` warnings.
8416 #[allow(unused_imports)]
8417 use super::*;
8418 #(#items)*
8419 }
8420 }
8421}
8422
8423fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
8424 let name = ident_name(field_ident);
8425 syn::Ident::new(&format!("{name}_col"), field_ident.span())
8426}
8427
8428/// Run `body` (of type `ret`) and put the `auto_now` fields back unless
8429/// `keep` holds for `_saved`, so a failed or no-row write keeps the old stamp.
8430fn restore_auto_now_unless(
8431 idents: &[syn::Ident],
8432 ret: &TokenStream2,
8433 keep: &TokenStream2,
8434 body: &TokenStream2,
8435) -> TokenStream2 {
8436 if idents.is_empty() {
8437 return body.clone();
8438 }
8439 let prev: Vec<syn::Ident> = (0..idents.len())
8440 .map(|i| quote::format_ident!("__rustango_auto_now_prev_{}", i))
8441 .collect();
8442 quote! {
8443 #( let #prev = ::core::clone::Clone::clone(&self.#idents); )*
8444 let _saved: #ret = async { #body }.await;
8445 if !(#keep) {
8446 #( self.#idents = #prev; )*
8447 }
8448 _saved
8449 }
8450}
8451
8452/// A field's name without the `r#` of a raw ident: `r#type` is `type` (#1937).
8453fn ident_name(ident: &syn::Ident) -> String {
8454 syn::ext::IdentExt::unraw(ident).to_string()
8455}
8456
8457/// The ident for a field named `name`, raw when it is a keyword (`type` → `r#type`).
8458fn ident_from_name(name: &str, span: proc_macro2::Span) -> syn::Ident {
8459 match syn::parse_str::<syn::Ident>(name) {
8460 Ok(mut ident) => {
8461 ident.set_span(span);
8462 ident
8463 }
8464 Err(_) => syn::Ident::new_raw(name, span),
8465 }
8466}
8467
8468fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8469 syn::Ident::new(
8470 &format!("__rustango_cols_{struct_name}"),
8471 struct_name.span(),
8472 )
8473}
8474
8475fn from_row_impl_tokens(
8476 struct_name: &syn::Ident,
8477 from_row_inits: &[TokenStream2],
8478 from_row_inits_my: &[TokenStream2],
8479) -> TokenStream2 {
8480 let root = rustango_root();
8481 // The Postgres impl is always emitted — every rustango build pulls in
8482 // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8483 // routed through `#root::__impl_my_from_row!`, a cfg-gated
8484 // macro_rules whose body collapses to nothing when rustango is built
8485 // without the `mysql` feature. No user-facing feature shim required.
8486 //
8487 // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8488 // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8489 // back into a comma-separated list inside square brackets. Since each
8490 // entry is already in `field: expr` shape, the existing tokens slot in.
8491 quote! {
8492 #[cfg(feature = "postgres")]
8493 impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8494 for #struct_name
8495 {
8496 fn from_row(
8497 row: &'r #root::sql::sqlx::postgres::PgRow,
8498 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8499 ::core::result::Result::Ok(Self {
8500 #( #from_row_inits ),*
8501 })
8502 }
8503 }
8504
8505 #root::__impl_my_from_row!(#struct_name, |row| {
8506 #( #from_row_inits_my ),*
8507 });
8508
8509 #root::__impl_sqlite_from_row!(#struct_name, |row| {
8510 #( #from_row_inits ),*
8511 });
8512 }
8513}
8514
8515struct ContainerAttrs {
8516 table: Option<String>,
8517 display: Option<(String, proc_macro2::Span)>,
8518 /// Explicit app label from `#[rustango(app = "blog")]`.
8519 /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8520 /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8521 /// at runtime — this attribute is the override for cases where
8522 /// the inference is wrong (e.g. a model that conceptually belongs
8523 /// to one app but is physically in another module).
8524 app: Option<String>,
8525 /// Per-model admin knobs from
8526 /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8527 /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8528 /// admin code falls back to `AdminConfig::DEFAULT`.
8529 admin: Option<AdminAttrs>,
8530 /// Per-model audit configuration from `#[rustango(audit(...))]`.
8531 /// `None` when the model isn't audited — write paths emit no
8532 /// audit entries. When present, single-row writes capture
8533 /// before/after for the listed fields and bulk writes batch
8534 /// snapshots into one INSERT into `rustango_audit_log`.
8535 audit: Option<AuditAttrs>,
8536 /// `true` when `#[rustango(permissions)]` is present. Signals that
8537 /// `auto_create_permissions` should seed the four CRUD codenames for
8538 /// this model.
8539 permissions: bool,
8540 /// Many-to-many relations declared via
8541 /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8542 /// src = "post_id", dst = "tag_id"))]`.
8543 m2m: Vec<M2MAttr>,
8544 /// Polymorphic M2M relations declared via
8545 /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8546 /// pk_column = "taggable_id", ct_column = "taggable_type",
8547 /// related_column = "tag_id"))]` (issue #818).
8548 generic_m2m: Vec<GenericM2MAttr>,
8549 /// Composite indexes declared via
8550 /// `#[rustango(index("col1, col2"))]` or
8551 /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8552 /// Single-column indexes from `#[rustango(index)]` on fields are
8553 /// accumulated here during field collection.
8554 indexes: Vec<IndexAttr>,
8555 /// Table-level CHECK constraints declared via
8556 /// `#[rustango(check(name = "…", expr = "…"))]`.
8557 checks: Vec<CheckAttr>,
8558 /// Table-level PG `EXCLUDE` constraints declared via
8559 /// `#[rustango(exclude(name = "…", using = "gist", elements =
8560 /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8561 /// migration writer renders them on Postgres and skips with a
8562 /// warning on MySQL/SQLite. Issue #319.
8563 excludes: Vec<ExcludeAttr>,
8564 /// Composite (multi-column) FKs declared via
8565 /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8566 /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8567 composite_fks: Vec<CompositeFkAttr>,
8568 /// Generic ("any model") FKs declared via
8569 /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8570 /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8571 generic_fks: Vec<GenericFkAttr>,
8572 /// Where this model lives in a tenancy deployment, declared via
8573 /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8574 /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8575 /// to partition diff output between registry-scoped and
8576 /// tenant-scoped migration files.
8577 scope: Option<String>,
8578 /// Extra QuerySet accessor names from
8579 /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8580 /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8581 /// the default `Self::objects()`. Multiple attributes allowed.
8582 manager_fns: Vec<syn::Ident>,
8583 /// Default ordering declared via `#[rustango(default_order =
8584 /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8585 /// `(column_name, desc_flag, span_for_error_reporting)` — the
8586 /// `-` prefix means descending; the `+` prefix or no prefix means
8587 /// ascending.
8588 default_order: Vec<(String, bool, proc_macro2::Span)>,
8589 /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8590 /// Routes the emitted schema's `is_view = true` so the migration
8591 /// snapshot skips this model (its underlying SQL view is operator-
8592 /// managed, not rustango-managed).
8593 is_view: bool,
8594 /// `#[rustango(managed = false)]` — who owns the table.
8595 /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8596 /// the migration snapshot skips this model so `makemigrations` /
8597 /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8598 /// TABLE` against it (operator-managed schema).
8599 managed: bool,
8600 /// Name of the model's base manager, from
8601 /// `#[rustango(base_manager_name = "...")]`. Threaded into
8602 /// `ModelSchema::base_manager_name`. Declarative-only today.
8603 base_manager_name: Option<String>,
8604 /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8605 /// field this model's instances are ordered relative to.
8606 /// Declarative-only today; threaded onto
8607 /// `ModelSchema::order_with_respect_to`.
8608 order_with_respect_to: Option<String>,
8609 /// `#[rustango(proxy)]` / `#[rustango(proxy = true)]`.
8610 /// Marks the model as a proxy that
8611 /// shares its DB table with another struct. Threaded into
8612 /// `ModelSchema::proxy` so future codegen can skip table-owning
8613 /// behavior for proxies.
8614 proxy: bool,
8615 /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8616 /// Each comma-separated capability token surfaces on
8617 /// `ModelSchema::required_db_features` so `manage check --deploy`
8618 /// can warn when the active dialect lacks one.
8619 required_db_features: Vec<String>,
8620 /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8621 /// Normalized to the dialect name `manage check --deploy`
8622 /// compares against `Settings.database.backend`. Aliases
8623 /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8624 /// stored under the canonical name.
8625 required_db_vendor: Option<String>,
8626 /// `#[rustango(default_related_name = "...")]`. Threaded into
8627 /// `ModelSchema::default_related_name`. Reverse-relation accessor
8628 /// name to use when an FK / M2M field doesn't override it.
8629 /// Today rustango doesn't auto-emit reverse managers; the
8630 /// metadata is the foundation for that work.
8631 default_related_name: Option<String>,
8632 /// `#[rustango(db_table_comment = "...")]`. Threaded into
8633 /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8634 /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8635 /// inline `COMMENT='...'`, SQLite: no-op).
8636 db_table_comment: Option<String>,
8637 /// `#[rustango(get_latest_by = "created_at")]` /
8638 /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8639 /// `(column, descending)` where `descending = true` when the
8640 /// attribute value starts with `-`. Threaded into
8641 /// `ModelSchema::get_latest_by`.
8642 get_latest_by: Option<(String, bool, proc_macro2::Span)>,
8643 /// Extra `(codename, label)` permissions
8644 /// from `#[rustango(extra_permissions = "approve:Can approve,
8645 /// archive:Can archive")]`. Comma-separated `codename:label`
8646 /// pairs. Threaded into `ModelSchema::extra_permissions`.
8647 extra_permissions: Vec<(String, String)>,
8648 /// Which CRUD codenames (`"add"` / `"change"` / `"delete"` /
8649 /// `"view"`) the framework
8650 /// auto-creates. Empty `Vec` (default) means **all four**. Set via
8651 /// `#[rustango(default_permissions = "view,change")]` to opt out.
8652 /// Validated at parse time; unknown actions fail with a span-pointing
8653 /// error.
8654 default_permissions: Vec<String>,
8655 /// `#[rustango(verbose_name = "blog post")]` — the
8656 /// human-readable singular label for the model. Threaded into
8657 /// `ModelSchema::verbose_name` so admin section headers /
8658 /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8659 /// over the Rust struct identifier.
8660 verbose_name: Option<String>,
8661 /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8662 /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8663 /// When unset, `display_label_plural()` auto-suffixes `s`.
8664 verbose_name_plural: Option<String>,
8665 /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8666 /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8667 /// a name (used by `QuerySet::without_global_scope`) with a
8668 /// `fn() -> WhereExpr` path that the macro emits into
8669 /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8670 global_scopes: Vec<GlobalScopeAttr>,
8671 /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8672 /// `#[rustango(through(name, far, far_fk_column, intermediate,
8673 /// intermediate_fk_column, intermediate_pk_column))]` — issue
8674 /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8675 /// entry emits an inherent `<name>_through(&self)` accessor that
8676 /// returns a `QuerySet<Far>` filtered via a correlated subquery
8677 /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8678 /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8679 through_relations: Vec<ThroughAttr>,
8680 /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8681 /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8682 /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8683 /// Each entry emits two associated functions on the parent —
8684 /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8685 /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8686 /// over a correlated subquery against the child table. Users
8687 /// drop the result into `QuerySet::where_raw(...)`.
8688 reverse_has_relations: Vec<ReverseHasAttr>,
8689 /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8690 /// issue #830. Each emits a `Model::generic_reverse_relations()`
8691 /// entry so the relation-existence family resolves polymorphic
8692 /// children by name.
8693 generic_has_relations: Vec<GenericHasAttr>,
8694}
8695
8696/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8697/// declaration. Each entry becomes one `core::GlobalScope` in the
8698/// emitted schema literal; `apply` resolves at macro-expand time
8699/// against the consumer's scope so the function must be in scope at
8700/// the use site. Issue #820.
8701struct GlobalScopeAttr {
8702 name: String,
8703 apply: syn::Path,
8704}
8705
8706/// Parsed `#[rustango(through(...))]` declaration. Issue
8707/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8708/// `hasManyThrough` / `hasOneThrough` parity.
8709///
8710/// `Country hasManyThrough Post via User` declares as:
8711///
8712/// ```ignore
8713/// #[rustango(through(
8714/// name = "posts",
8715/// far = "Post",
8716/// far_fk_column = "author_id",
8717/// intermediate = "User",
8718/// intermediate_fk_column = "country_id",
8719/// ))]
8720/// struct Country { ... }
8721/// ```
8722///
8723/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8724/// which returns a queryset filtered via a correlated subquery —
8725/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8726/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8727/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8728/// because the subquery lives inside a `WhereExpr::InSubquery` node
8729/// and the outer queryset's pending list stays empty.
8730///
8731/// All four required arguments use **SQL column / table names**
8732/// (not Rust field names) to sidestep the multi-hop-filter substrate
8733/// gap. Once that substrate lands, a higher-level Rust-field-name
8734/// shorthand can be added without breaking this surface.
8735struct ThroughAttr {
8736 /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8737 name: String,
8738 /// Far model type identifier. `far = "Post"` → returns
8739 /// `QuerySet<Post>`. Resolved verbatim against the scope where
8740 /// the derive expands.
8741 far: syn::Ident,
8742 /// SQL column on the far model's table that references the
8743 /// intermediate model's primary key. For `Post`'s
8744 /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8745 /// default `<field>_id` convention) or whatever the user
8746 /// declared via `#[rustango(db_column = "...")]`.
8747 far_fk_column: String,
8748 /// Intermediate model type identifier. Needed to look up its
8749 /// `SCHEMA` so the subquery's `FROM` clause points at the
8750 /// intermediate table. `intermediate = "User"`.
8751 intermediate: syn::Ident,
8752 /// SQL column on the intermediate's table that references the
8753 /// source (this) model's primary key. For `User`'s
8754 /// `country: ForeignKey<Country>` the column is `"country_id"`.
8755 intermediate_fk_column: String,
8756 /// SQL primary-key column on the intermediate's table — the
8757 /// column the subquery projects. Optional; defaults to `"id"`
8758 /// (rustango's default PK column name). Override when the
8759 /// intermediate declares a custom PK column.
8760 intermediate_pk_column: String,
8761}
8762
8763/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8764/// child_fk_column = "..."))]` declaration. Issue
8765/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8766/// `whereHas` / `whereDoesntHave` parity.
8767///
8768/// `Post hasMany Comment` declares as:
8769///
8770/// ```ignore
8771/// #[rustango(reverse_has(
8772/// name = "comments",
8773/// child = "Comment",
8774/// child_fk_column = "post_id",
8775/// ))]
8776/// struct Post { ... }
8777/// ```
8778///
8779/// The macro emits two associated functions on `Post`:
8780///
8781/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8782/// `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8783/// <outer>.<self_pk_column>)`.
8784/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8785/// but `NOT EXISTS`, the `whereDoesntHave` analog.
8786///
8787/// User code:
8788///
8789/// ```ignore
8790/// // Posts with at least one comment:
8791/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8792/// // Posts with no comments:
8793/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8794/// ```
8795///
8796/// As with #817, all column / table identifiers are **SQL names**
8797/// (not Rust field names) so the substrate is independent of the
8798/// outstanding multi-hop filter resolver gap. The emitted
8799/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8800/// SQL-emit time via the writer's scope stack.
8801struct ReverseHasAttr {
8802 /// Accessor name. `name = "comments"` → emits
8803 /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8804 name: String,
8805 /// Child model type identifier. `child = "Comment"` — needed to
8806 /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8807 /// points at the child table.
8808 child: syn::Ident,
8809 /// SQL column on the child's table that references this model's
8810 /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8811 /// column is `"post_id"`.
8812 child_fk_column: String,
8813 /// Column on **this** model's table the child FK references — the
8814 /// `OuterRef` target. Defaults to the PK column (`"id"` without one).
8815 self_pk_column: Option<String>,
8816}
8817
8818impl ReverseHasAttr {
8819 fn self_pk_column(&self) -> &str {
8820 self.self_pk_column.as_deref().unwrap_or("id")
8821 }
8822}
8823
8824/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8825/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8826/// relation-existence family (issue #830). The child is a polymorphic,
8827/// content-type-discriminated model; emits a `Model::
8828/// generic_reverse_relations()` entry the queryset resolves by name.
8829struct GenericHasAttr {
8830 /// Accessor name, e.g. `name = "tags"`.
8831 name: String,
8832 /// Child model type identifier (`child = "Tag"`) — looked up for its
8833 /// `SCHEMA` so the subquery's `FROM` points at the child table.
8834 child: syn::Ident,
8835 /// Column on the child table holding the parent's content-type id.
8836 /// Optional; defaults to `"content_type_id"`.
8837 ct_column: String,
8838 /// Column on the child table holding the parent's PK value.
8839 /// Optional; defaults to `"object_pk"`.
8840 pk_column: String,
8841 /// SQL primary-key column on **this** (parent) model's table.
8842 /// Defaults to the PK column (`"id"` without one).
8843 self_pk_column: Option<String>,
8844}
8845
8846impl GenericHasAttr {
8847 fn self_pk_column(&self) -> &str {
8848 self.self_pk_column.as_deref().unwrap_or("id")
8849 }
8850}
8851
8852/// Parsed form of one index declaration (field-level or container-level).
8853struct IndexAttr {
8854 /// The declaring attribute, for diagnostics.
8855 span: proc_macro2::Span,
8856 /// Index name; auto-derived when `None` at parse time.
8857 name: Option<String>,
8858 /// Column names in the index.
8859 columns: Vec<String>,
8860 /// `true` for `CREATE UNIQUE INDEX`.
8861 unique: bool,
8862 /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8863 /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8864 /// `"btree"` when the attribute is absent — the DDL writer omits
8865 /// the `USING` clause and the backend uses its own default
8866 /// (btree on every supported dialect).
8867 method: String,
8868 /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8869 /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8870 /// condition = "...", name = "..."))]`. `None` for plain indexes.
8871 where_clause: Option<String>,
8872 /// Covering-index columns (PG 11+ `INCLUDE (...)` clause).
8873 /// Empty `Vec` (the default) means "no covering columns".
8874 include: Vec<String>,
8875}
8876
8877/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8878struct CheckAttr {
8879 name: String,
8880 expr: String,
8881}
8882
8883/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8884/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8885/// PG-only — surfaced on every backend in the macro emit; the migration
8886/// writer skips the op on MySQL/SQLite. Issue #319.
8887struct ExcludeAttr {
8888 /// Constraint name (free-form Rust identifier).
8889 name: String,
8890 /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8891 using: String,
8892 /// Comma-separated `(column, operator)` pairs, in declaration
8893 /// order. Parsed from `"col WITH op, col WITH op"`.
8894 elements: Vec<(String, String)>,
8895 /// Optional `WHERE` predicate (raw SQL).
8896 where_clause: Option<String>,
8897}
8898
8899/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8900/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8901/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8902/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8903/// the model, not the field.
8904struct CompositeFkAttr {
8905 /// Logical relation name (free-form Rust identifier).
8906 name: String,
8907 /// SQL table name of the target.
8908 to: String,
8909 /// Source-side column names, in declaration order.
8910 from: Vec<String>,
8911 /// Target-side column names, same length / order as `from`.
8912 on: Vec<String>,
8913}
8914
8915/// Parsed form of one `#[rustango(generic_fk(name = "target",
8916/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8917/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8918/// generic ("any model") FKs live on the model, not the field.
8919struct GenericFkAttr {
8920 /// Logical relation name (free-form Rust identifier).
8921 name: String,
8922 /// Source-side column carrying the `content_type_id` value.
8923 ct_column: String,
8924 /// Source-side column carrying the target row's primary key.
8925 pk_column: String,
8926}
8927
8928/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8929struct M2MAttr {
8930 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8931 name: String,
8932 /// Target table (e.g. `"app_tags"`).
8933 to: String,
8934 /// Junction table (e.g. `"post_tags"`).
8935 through: String,
8936 /// Source FK column in the junction table (e.g. `"post_id"`).
8937 src: String,
8938 /// Destination FK column in the junction table (e.g. `"tag_id"`).
8939 dst: String,
8940 /// Whether the migration writer should auto-create the junction
8941 /// table. Default `true`. Set `auto_create = false` (#324) when
8942 /// the operator declares the through table as its own
8943 /// `#[derive(Model)]` struct with extra columns.
8944 auto_create: bool,
8945}
8946
8947/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8948/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8949/// junction carries a ContentType discriminator so unrelated models
8950/// share one pivot + related set.
8951struct GenericM2MAttr {
8952 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8953 name: String,
8954 /// Polymorphic junction table (e.g. `"taggables"`).
8955 through: String,
8956 /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8957 pk_column: String,
8958 /// Junction column holding the owning model's `content_type_id`
8959 /// discriminator (e.g. `"taggable_type"`).
8960 ct_column: String,
8961 /// Junction column holding the related model PK (e.g. `"tag_id"`).
8962 related_column: String,
8963}
8964
8965/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8966/// "user"))]`. `track` is a comma-separated list of field names whose
8967/// before/after values land in the JSONB `changes` column. `source`
8968/// is informational only — it pins a default source when the model
8969/// is written outside any `audit::with_source(...)` scope (rare).
8970#[derive(Default)]
8971struct AuditAttrs {
8972 /// Field names to capture in the `changes` JSONB. Validated
8973 /// against declared scalar fields at compile time. Empty means
8974 /// "track every scalar field".
8975 track: Option<(Vec<String>, proc_macro2::Span)>,
8976}
8977
8978/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8979/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8980/// comma-separated strings; we validate each ident against the model's
8981/// declared fields at compile time.
8982#[derive(Default)]
8983struct AdminAttrs {
8984 list_display: Option<(Vec<String>, proc_macro2::Span)>,
8985 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8986 list_per_page: Option<usize>,
8987 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8988 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8989 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8990 /// Bulk action names. No field-validation against model fields —
8991 /// these are action handlers, not column references.
8992 actions: Option<(Vec<String>, proc_macro2::Span)>,
8993 /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8994 /// sections, comma-separated fields per section, optional
8995 /// `Title:` prefix. Empty title omits the `<legend>`.
8996 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8997 /// `admin(list_display_links = "title")`. Names
8998 /// from `list_display` whose cells should link to detail/edit.
8999 /// Issue #350.
9000 list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
9001 /// `admin(search_help_text = "...")`. Short
9002 /// caption rendered beside the admin list view's search box.
9003 /// Issue #353.
9004 search_help_text: Option<String>,
9005 /// `admin(actions_on_top = false)`. Hides the
9006 /// action-bar above the table. Default `true`. Issue #354.
9007 actions_on_top: Option<bool>,
9008 /// `admin(actions_on_bottom = true)`. Renders an
9009 /// additional action-bar below the table. Default `false`.
9010 /// Issue #354.
9011 actions_on_bottom: Option<bool>,
9012 /// `admin(date_hierarchy = "created_at")`. Name of
9013 /// a date / datetime field whose values render as a clickable
9014 /// year / month / day drill-down strip above the list table.
9015 /// Empty / unset disables the strip. Issue #355.
9016 date_hierarchy: Option<String>,
9017 /// `admin(prepopulated_fields = "slug:title")`.
9018 /// Each entry is `target:source[+source2]`; multiple entries are
9019 /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
9020 /// The admin change-form emits JS that slugifies the source values
9021 /// into the target field on every keystroke. Issue #356.
9022 prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
9023 /// `admin(raw_id_fields = "parent, owner")`. Names
9024 /// of FK fields whose change-form widget renders a lookup link
9025 /// next to the input. Issue #357.
9026 raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
9027 /// `admin(autocomplete_fields = "author_id")`.
9028 /// Names of FK fields whose change-form widget renders an
9029 /// Ajax-driven typeahead populated from a `__autocomplete`
9030 /// endpoint on the target model. Issue #358.
9031 autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
9032 /// `admin(list_select_related = "all" | "none" | "author, …")`
9033 /// — tunes the admin list view's FK auto-JOIN
9034 /// policy. Default `"all"` matches rustango's join-everything
9035 /// behavior; `"none"` opts out; CSV restricts. Issue #352.
9036 list_select_related: Option<String>,
9037 /// `admin(formfield_overrides = "field:widget, field2:widget2")`.
9038 /// Each entry is `field_name:widget_name`; multiple
9039 /// entries comma-separated. Empty / unset → no overrides. The
9040 /// list of widget names supported is documented on
9041 /// `AdminConfig::formfield_overrides`. Issue #359.
9042 formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
9043}
9044
9045/// The names `#[derive(Model)]` reads its attributes under: `rgo` is a
9046/// short alias of `rustango` with the same meaning.
9047#[derive(Clone, Copy, PartialEq, Eq)]
9048enum ModelAttrName {
9049 Rustango,
9050 Rgo,
9051}
9052
9053impl ModelAttrName {
9054 fn of(attr: &syn::Attribute) -> Option<Self> {
9055 let path = attr.path();
9056 if path.is_ident("rustango") {
9057 Some(Self::Rustango)
9058 } else if path.is_ident("rgo") {
9059 Some(Self::Rgo)
9060 } else {
9061 None
9062 }
9063 }
9064
9065 fn as_str(self) -> &'static str {
9066 match self {
9067 Self::Rustango => "rustango",
9068 Self::Rgo => "rgo",
9069 }
9070 }
9071}
9072
9073/// The model attributes in `attrs`, under either name. One item or field
9074/// uses one name, so a mix reads as a mistake rather than a merge.
9075fn model_attrs(attrs: &[syn::Attribute]) -> syn::Result<Vec<&syn::Attribute>> {
9076 let mut first: Option<ModelAttrName> = None;
9077 let mut out = Vec::new();
9078 for attr in attrs {
9079 let Some(name) = ModelAttrName::of(attr) else {
9080 continue;
9081 };
9082 match first {
9083 Some(seen) if seen != name => {
9084 return Err(syn::Error::new_spanned(
9085 attr.path(),
9086 format!(
9087 "`#[{}(...)]` and `#[{}(...)]` are the same attribute; \
9088 use one name on an item or field",
9089 seen.as_str(),
9090 name.as_str(),
9091 ),
9092 ));
9093 }
9094 _ => first = Some(name),
9095 }
9096 out.push(attr);
9097 }
9098 Ok(out)
9099}
9100
9101fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
9102 let mut out = ContainerAttrs {
9103 table: None,
9104 display: None,
9105 app: None,
9106 admin: None,
9107 audit: None,
9108 // Default `permissions = true` so every `#[derive(Model)]`
9109 // gets the four CRUD codenames seeded by `auto_create_permissions`
9110 // and is visible to non-superusers in the tenant admin without
9111 // manual per-model annotation. Models that intentionally don't
9112 // want permission rows (registry-internal types, framework
9113 // tables operators shouldn't manage directly) opt out via
9114 // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
9115 // out-of-the-box admin invisibility regression (#62).
9116 permissions: true,
9117 m2m: Vec::new(),
9118 generic_m2m: Vec::new(),
9119 indexes: Vec::new(),
9120 checks: Vec::new(),
9121 excludes: Vec::new(),
9122 composite_fks: Vec::new(),
9123 generic_fks: Vec::new(),
9124 scope: None,
9125 manager_fns: Vec::new(),
9126 default_order: Vec::new(),
9127 is_view: false,
9128 managed: true,
9129 verbose_name: None,
9130 verbose_name_plural: None,
9131 base_manager_name: None,
9132 order_with_respect_to: None,
9133 proxy: false,
9134 required_db_features: Vec::new(),
9135 required_db_vendor: None,
9136 default_related_name: None,
9137 db_table_comment: None,
9138 get_latest_by: None,
9139 extra_permissions: Vec::new(),
9140 default_permissions: Vec::new(),
9141 global_scopes: Vec::new(),
9142 through_relations: Vec::new(),
9143 reverse_has_relations: Vec::new(),
9144 generic_has_relations: Vec::new(),
9145 };
9146 for attr in model_attrs(&input.attrs)? {
9147 // The name the user wrote, for hints that quote the attribute.
9148 let written = ModelAttrName::of(attr).map_or("rustango", ModelAttrName::as_str);
9149 attr.parse_nested_meta(|meta| {
9150 if meta.path.is_ident("table") {
9151 let s: LitStr = meta.value()?.parse()?;
9152 let name = s.value();
9153 // v0.27.3 (#65) — macro-time guard against table names
9154 // that compile but break SQL downstream. Hyphens are
9155 // the common footgun: PostgreSQL accepts a quoted
9156 // `"intermediate-region"` in CREATE TABLE, but the
9157 // FK / index name derivation in `migrate::ddl`
9158 // emits `intermediate-region_field_fkey` unquoted,
9159 // which then fails the SQL parser. Same shape rule
9160 // as Postgres regular identifiers so the safe path
9161 // is the only path.
9162 validate_table_name(&name, s.span())?;
9163 out.table = Some(name);
9164 return Ok(());
9165 }
9166 if meta.path.is_ident("display") {
9167 let s: LitStr = meta.value()?.parse()?;
9168 out.display = Some((s.value(), s.span()));
9169 return Ok(());
9170 }
9171 if meta.path.is_ident("app") {
9172 let s: LitStr = meta.value()?.parse()?;
9173 out.app = Some(s.value());
9174 return Ok(());
9175 }
9176 if meta.path.is_ident("scope") {
9177 let s: LitStr = meta.value()?.parse()?;
9178 let val = s.value();
9179 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
9180 return Err(meta.error(format!(
9181 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
9182 )));
9183 }
9184 out.scope = Some(val);
9185 return Ok(());
9186 }
9187 if meta.path.is_ident("admin") {
9188 let mut admin = AdminAttrs::default();
9189 meta.parse_nested_meta(|inner| {
9190 if inner.path.is_ident("list_display") {
9191 let s: LitStr = inner.value()?.parse()?;
9192 admin.list_display =
9193 Some((split_field_list(&s.value()), s.span()));
9194 return Ok(());
9195 }
9196 if inner.path.is_ident("search_fields") {
9197 let s: LitStr = inner.value()?.parse()?;
9198 admin.search_fields =
9199 Some((split_field_list(&s.value()), s.span()));
9200 return Ok(());
9201 }
9202 if inner.path.is_ident("readonly_fields") {
9203 let s: LitStr = inner.value()?.parse()?;
9204 admin.readonly_fields =
9205 Some((split_field_list(&s.value()), s.span()));
9206 return Ok(());
9207 }
9208 if inner.path.is_ident("list_per_page") {
9209 let lit: syn::LitInt = inner.value()?.parse()?;
9210 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
9211 return Ok(());
9212 }
9213 if inner.path.is_ident("ordering") {
9214 let s: LitStr = inner.value()?.parse()?;
9215 admin.ordering = Some((
9216 parse_ordering_list(&s.value()),
9217 s.span(),
9218 ));
9219 return Ok(());
9220 }
9221 if inner.path.is_ident("list_filter") {
9222 let s: LitStr = inner.value()?.parse()?;
9223 admin.list_filter =
9224 Some((split_field_list(&s.value()), s.span()));
9225 return Ok(());
9226 }
9227 if inner.path.is_ident("actions") {
9228 let s: LitStr = inner.value()?.parse()?;
9229 admin.actions =
9230 Some((split_field_list(&s.value()), s.span()));
9231 return Ok(());
9232 }
9233 if inner.path.is_ident("fieldsets") {
9234 let s: LitStr = inner.value()?.parse()?;
9235 admin.fieldsets =
9236 Some((parse_fieldset_list(&s.value()), s.span()));
9237 return Ok(());
9238 }
9239 if inner.path.is_ident("list_display_links") {
9240 let s: LitStr = inner.value()?.parse()?;
9241 admin.list_display_links =
9242 Some((split_field_list(&s.value()), s.span()));
9243 return Ok(());
9244 }
9245 if inner.path.is_ident("search_help_text") {
9246 let s: LitStr = inner.value()?.parse()?;
9247 admin.search_help_text = Some(s.value());
9248 return Ok(());
9249 }
9250 if inner.path.is_ident("actions_on_top") {
9251 let lit: syn::LitBool = inner.value()?.parse()?;
9252 admin.actions_on_top = Some(lit.value);
9253 return Ok(());
9254 }
9255 if inner.path.is_ident("actions_on_bottom") {
9256 let lit: syn::LitBool = inner.value()?.parse()?;
9257 admin.actions_on_bottom = Some(lit.value);
9258 return Ok(());
9259 }
9260 if inner.path.is_ident("date_hierarchy") {
9261 let s: LitStr = inner.value()?.parse()?;
9262 admin.date_hierarchy = Some(s.value());
9263 return Ok(());
9264 }
9265 if inner.path.is_ident("prepopulated_fields") {
9266 let s: LitStr = inner.value()?.parse()?;
9267 admin.prepopulated_fields =
9268 Some((parse_prepopulated_list(&s.value()), s.span()));
9269 return Ok(());
9270 }
9271 if inner.path.is_ident("raw_id_fields") {
9272 let s: LitStr = inner.value()?.parse()?;
9273 admin.raw_id_fields =
9274 Some((split_field_list(&s.value()), s.span()));
9275 return Ok(());
9276 }
9277 if inner.path.is_ident("autocomplete_fields") {
9278 let s: LitStr = inner.value()?.parse()?;
9279 admin.autocomplete_fields =
9280 Some((split_field_list(&s.value()), s.span()));
9281 return Ok(());
9282 }
9283 if inner.path.is_ident("list_select_related") {
9284 let s: LitStr = inner.value()?.parse()?;
9285 admin.list_select_related = Some(s.value());
9286 return Ok(());
9287 }
9288 if inner.path.is_ident("formfield_overrides") {
9289 let s: LitStr = inner.value()?.parse()?;
9290 admin.formfield_overrides =
9291 Some((parse_formfield_overrides(&s.value()), s.span()));
9292 return Ok(());
9293 }
9294 Err(inner.error(
9295 "unknown admin attribute (supported: \
9296 `list_display`, `list_display_links`, \
9297 `search_fields`, `search_help_text`, \
9298 `readonly_fields`, \
9299 `list_filter`, `list_per_page`, `ordering`, `actions`, \
9300 `actions_on_top`, `actions_on_bottom`, \
9301 `date_hierarchy`, \
9302 `prepopulated_fields`, \
9303 `raw_id_fields`, \
9304 `autocomplete_fields`, \
9305 `list_select_related`, \
9306 `formfield_overrides`, \
9307 `fieldsets`)",
9308 ))
9309 })?;
9310 out.admin = Some(admin);
9311 return Ok(());
9312 }
9313 if meta.path.is_ident("manager") {
9314 // Its empty trait could not take methods (#2132).
9315 return Err(meta.error(
9316 "`manager(ext = ...)` was removed (#2132): declare your own \
9317 `trait FooManagerExt: Sized { … }` and implement it for `QuerySet<Foo>`",
9318 ));
9319 }
9320 if meta.path.is_ident("manager_fn") {
9321 // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
9322 // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
9323 // next to the default `Self::objects()`. Multiple
9324 // attributes accumulate.
9325 let s: LitStr = meta.value()?.parse()?;
9326 let name = s.value();
9327 if name.is_empty() {
9328 return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
9329 }
9330 if name == "objects" {
9331 return Err(meta.error(
9332 "`manager_fn = \"objects\"` collides with the default \
9333 accessor — pick a different name",
9334 ));
9335 }
9336 let ident = syn::Ident::new(&name, s.span());
9337 if out.manager_fns.iter().any(|prev| *prev == ident) {
9338 return Err(meta.error(format!(
9339 "duplicate `manager_fn = \"{name}\"`"
9340 )));
9341 }
9342 out.manager_fns.push(ident);
9343 return Ok(());
9344 }
9345 if meta.path.is_ident("default_order") {
9346 // `#[rustango(default_order = "-created_at, status")]`
9347 // — issue #291 / T2.5. Comma-separated list; `-prefix`
9348 // means descending, `+prefix` or bare name means ascending.
9349 // Per-query opt-in via `QuerySet::with_default_order()`.
9350 let s: LitStr = meta.value()?.parse()?;
9351 let raw = s.value();
9352 let span = s.span();
9353 let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
9354 Vec::new();
9355 for entry in raw.split(',') {
9356 let trimmed = entry.trim();
9357 if trimmed.is_empty() {
9358 return Err(syn::Error::new(
9359 span,
9360 "`default_order = \"...\"` has an empty entry — \
9361 check for a stray comma",
9362 ));
9363 }
9364 let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
9365 (true, rest.trim().to_owned())
9366 } else if let Some(rest) = trimmed.strip_prefix('+') {
9367 (false, rest.trim().to_owned())
9368 } else {
9369 (false, trimmed.to_owned())
9370 };
9371 if name.is_empty() {
9372 return Err(syn::Error::new(
9373 span,
9374 "`default_order` entry has no column name after the prefix",
9375 ));
9376 }
9377 if parsed.iter().any(|(n, _, _)| *n == name) {
9378 return Err(syn::Error::new(
9379 span,
9380 format!("duplicate column `{name}` in `default_order`"),
9381 ));
9382 }
9383 parsed.push((name, desc, span));
9384 }
9385 if parsed.is_empty() {
9386 return Err(syn::Error::new(
9387 span,
9388 "`default_order = \"...\"` cannot be empty",
9389 ));
9390 }
9391 out.default_order = parsed;
9392 return Ok(());
9393 }
9394 if meta.path.is_ident("global_scope") {
9395 // `#[rustango(global_scope(name = "active", apply =
9396 // path::to::fn))]` — issue #820. The apply function
9397 // path resolves at macro-expand time in the consumer's
9398 // scope; the macro re-emits it verbatim into the
9399 // `ModelSchema::global_scopes` slice literal.
9400 let span = meta.path.span();
9401 let mut scope_name: Option<String> = None;
9402 let mut apply_path: Option<syn::Path> = None;
9403 meta.parse_nested_meta(|inner| {
9404 if inner.path.is_ident("name") {
9405 let s: LitStr = inner.value()?.parse()?;
9406 let raw = s.value();
9407 if raw.trim().is_empty() {
9408 return Err(syn::Error::new(
9409 s.span(),
9410 "`global_scope(name = \"...\")` must not be empty",
9411 ));
9412 }
9413 scope_name = Some(raw);
9414 return Ok(());
9415 }
9416 if inner.path.is_ident("apply") {
9417 let p: syn::Path = inner.value()?.parse()?;
9418 apply_path = Some(p);
9419 return Ok(());
9420 }
9421 Err(inner.error(
9422 "unknown `global_scope` attribute (supported: \
9423 `name`, `apply`)",
9424 ))
9425 })?;
9426 let Some(name) = scope_name else {
9427 return Err(syn::Error::new(
9428 span,
9429 "`global_scope` requires `name = \"...\"`",
9430 ));
9431 };
9432 let Some(apply) = apply_path else {
9433 return Err(syn::Error::new(
9434 span,
9435 "`global_scope` requires `apply = fn_path`",
9436 ));
9437 };
9438 if out.global_scopes.iter().any(|s| s.name == name) {
9439 return Err(syn::Error::new(
9440 span,
9441 format!(
9442 "duplicate global scope name `{name}` — \
9443 pick a unique identifier so \
9444 `QuerySet::without_global_scope(\"{name}\")` \
9445 is unambiguous"
9446 ),
9447 ));
9448 }
9449 out.global_scopes.push(GlobalScopeAttr { name, apply });
9450 return Ok(());
9451 }
9452 if meta.path.is_ident("through") {
9453 // `#[rustango(through(name = "posts", far = "Post",
9454 // far_fk_column = "author_id", intermediate = "User",
9455 // intermediate_fk_column = "country_id"
9456 // [, intermediate_pk_column = "id"]))]` — issue #817.
9457 let span = meta.path.span();
9458 let mut accessor_name: Option<String> = None;
9459 let mut far_ident: Option<syn::Ident> = None;
9460 let mut far_fk_column: Option<String> = None;
9461 let mut intermediate_ident: Option<syn::Ident> = None;
9462 let mut intermediate_fk_column: Option<String> = None;
9463 let mut intermediate_pk_column: Option<String> = None;
9464 fn parse_nonempty_string(
9465 inner: &syn::meta::ParseNestedMeta<'_>,
9466 field: &str,
9467 ) -> syn::Result<String> {
9468 let s: LitStr = inner.value()?.parse()?;
9469 let raw = s.value();
9470 let trimmed = raw.trim();
9471 if trimmed.is_empty() {
9472 return Err(syn::Error::new(
9473 s.span(),
9474 format!("`through({field} = \"...\")` must not be empty"),
9475 ));
9476 }
9477 Ok(trimmed.to_owned())
9478 }
9479 meta.parse_nested_meta(|inner| {
9480 if inner.path.is_ident("name") {
9481 accessor_name = Some(parse_nonempty_string(&inner, "name")?);
9482 return Ok(());
9483 }
9484 if inner.path.is_ident("far") {
9485 // Far model name accepted as a string literal
9486 // so the attribute fits inside the existing
9487 // `parse_nested_meta` shape; parsed back into a
9488 // `syn::Ident` so the emitted accessor can
9489 // reference the type directly.
9490 let s: LitStr = inner.value()?.parse()?;
9491 let raw = s.value();
9492 let trimmed = raw.trim();
9493 if trimmed.is_empty() {
9494 return Err(syn::Error::new(
9495 s.span(),
9496 "`through(far = \"...\")` must not be empty",
9497 ));
9498 }
9499 far_ident = Some(syn::Ident::new(trimmed, s.span()));
9500 return Ok(());
9501 }
9502 if inner.path.is_ident("far_fk_column") {
9503 far_fk_column =
9504 Some(parse_nonempty_string(&inner, "far_fk_column")?);
9505 return Ok(());
9506 }
9507 if inner.path.is_ident("intermediate") {
9508 let s: LitStr = inner.value()?.parse()?;
9509 let raw = s.value();
9510 let trimmed = raw.trim();
9511 if trimmed.is_empty() {
9512 return Err(syn::Error::new(
9513 s.span(),
9514 "`through(intermediate = \"...\")` must not be empty",
9515 ));
9516 }
9517 intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9518 return Ok(());
9519 }
9520 if inner.path.is_ident("intermediate_fk_column") {
9521 intermediate_fk_column =
9522 Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9523 return Ok(());
9524 }
9525 if inner.path.is_ident("intermediate_pk_column") {
9526 intermediate_pk_column =
9527 Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9528 return Ok(());
9529 }
9530 Err(inner.error(
9531 "unknown `through` attribute (supported: \
9532 `name`, `far`, `far_fk_column`, \
9533 `intermediate`, `intermediate_fk_column`, \
9534 `intermediate_pk_column`)",
9535 ))
9536 })?;
9537 let Some(name) = accessor_name else {
9538 return Err(syn::Error::new(
9539 span,
9540 "`through` requires `name = \"...\"`",
9541 ));
9542 };
9543 let Some(far) = far_ident else {
9544 return Err(syn::Error::new(
9545 span,
9546 "`through` requires `far = \"FarModelType\"`",
9547 ));
9548 };
9549 let Some(far_fk_column) = far_fk_column else {
9550 return Err(syn::Error::new(
9551 span,
9552 "`through` requires `far_fk_column = \"<column>\"`",
9553 ));
9554 };
9555 let Some(intermediate) = intermediate_ident else {
9556 return Err(syn::Error::new(
9557 span,
9558 "`through` requires `intermediate = \"IntermediateModelType\"`",
9559 ));
9560 };
9561 let Some(intermediate_fk_column) = intermediate_fk_column else {
9562 return Err(syn::Error::new(
9563 span,
9564 "`through` requires `intermediate_fk_column = \"<column>\"`",
9565 ));
9566 };
9567 let intermediate_pk_column =
9568 intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9569 if out.through_relations.iter().any(|t| t.name == name) {
9570 return Err(syn::Error::new(
9571 span,
9572 format!(
9573 "duplicate `through(name = \"{name}\")` — \
9574 pick a unique accessor name"
9575 ),
9576 ));
9577 }
9578 out.through_relations.push(ThroughAttr {
9579 name,
9580 far,
9581 far_fk_column,
9582 intermediate,
9583 intermediate_fk_column,
9584 intermediate_pk_column,
9585 });
9586 return Ok(());
9587 }
9588 if meta.path.is_ident("reverse_has") {
9589 // `#[rustango(reverse_has(name = "comments",
9590 // child = "Comment", child_fk_column = "post_id"
9591 // [, self_pk_column = "id"]))]` — issue #830.
9592 let span = meta.path.span();
9593 let mut accessor_name: Option<String> = None;
9594 let mut child_ident: Option<syn::Ident> = None;
9595 let mut child_fk_column: Option<String> = None;
9596 let mut self_pk_column: Option<String> = None;
9597 meta.parse_nested_meta(|inner| {
9598 if inner.path.is_ident("name") {
9599 let s: LitStr = inner.value()?.parse()?;
9600 let raw = s.value();
9601 if raw.trim().is_empty() {
9602 return Err(syn::Error::new(
9603 s.span(),
9604 "`reverse_has(name = \"...\")` must not be empty",
9605 ));
9606 }
9607 accessor_name = Some(raw);
9608 return Ok(());
9609 }
9610 if inner.path.is_ident("child") {
9611 let s: LitStr = inner.value()?.parse()?;
9612 let raw = s.value();
9613 let trimmed = raw.trim();
9614 if trimmed.is_empty() {
9615 return Err(syn::Error::new(
9616 s.span(),
9617 "`reverse_has(child = \"...\")` must not be empty",
9618 ));
9619 }
9620 child_ident = Some(syn::Ident::new(trimmed, s.span()));
9621 return Ok(());
9622 }
9623 if inner.path.is_ident("child_fk_column") {
9624 let s: LitStr = inner.value()?.parse()?;
9625 let raw = s.value();
9626 let trimmed = raw.trim();
9627 if trimmed.is_empty() {
9628 return Err(syn::Error::new(
9629 s.span(),
9630 "`reverse_has(child_fk_column = \"...\")` must not be empty",
9631 ));
9632 }
9633 child_fk_column = Some(trimmed.to_owned());
9634 return Ok(());
9635 }
9636 if inner.path.is_ident("self_pk_column") {
9637 let s: LitStr = inner.value()?.parse()?;
9638 let raw = s.value();
9639 let trimmed = raw.trim();
9640 if trimmed.is_empty() {
9641 return Err(syn::Error::new(
9642 s.span(),
9643 "`reverse_has(self_pk_column = \"...\")` must not be empty",
9644 ));
9645 }
9646 self_pk_column = Some(trimmed.to_owned());
9647 return Ok(());
9648 }
9649 Err(inner.error(
9650 "unknown `reverse_has` attribute (supported: \
9651 `name`, `child`, `child_fk_column`, \
9652 `self_pk_column`)",
9653 ))
9654 })?;
9655 let Some(name) = accessor_name else {
9656 return Err(syn::Error::new(
9657 span,
9658 "`reverse_has` requires `name = \"...\"`",
9659 ));
9660 };
9661 let Some(child) = child_ident else {
9662 return Err(syn::Error::new(
9663 span,
9664 "`reverse_has` requires `child = \"ChildModelType\"`",
9665 ));
9666 };
9667 let Some(child_fk_column) = child_fk_column else {
9668 return Err(syn::Error::new(
9669 span,
9670 "`reverse_has` requires `child_fk_column = \"<column>\"`",
9671 ));
9672 };
9673 if out.reverse_has_relations.iter().any(|r| r.name == name) {
9674 return Err(syn::Error::new(
9675 span,
9676 format!(
9677 "duplicate `reverse_has(name = \"{name}\")` — \
9678 pick a unique accessor name"
9679 ),
9680 ));
9681 }
9682 out.reverse_has_relations.push(ReverseHasAttr {
9683 name,
9684 child,
9685 child_fk_column,
9686 self_pk_column,
9687 });
9688 return Ok(());
9689 }
9690 if meta.path.is_ident("generic_has") {
9691 // `#[rustango(generic_has(name = "tags",
9692 // child = "Tag", ct_column = "content_type_id",
9693 // pk_column = "object_pk" [, self_pk_column = "id"]))]`
9694 // — issue #830, the reverse generic-FK (GFK) arm of the
9695 // relation-existence family.
9696 let span = meta.path.span();
9697 let mut accessor_name: Option<String> = None;
9698 let mut child_ident: Option<syn::Ident> = None;
9699 let mut ct_column: Option<String> = None;
9700 let mut pk_column: Option<String> = None;
9701 let mut self_pk_column: Option<String> = None;
9702 meta.parse_nested_meta(|inner| {
9703 if inner.path.is_ident("name") {
9704 let s: LitStr = inner.value()?.parse()?;
9705 let raw = s.value();
9706 if raw.trim().is_empty() {
9707 return Err(syn::Error::new(
9708 s.span(),
9709 "`generic_has(name = \"...\")` must not be empty",
9710 ));
9711 }
9712 accessor_name = Some(raw);
9713 return Ok(());
9714 }
9715 if inner.path.is_ident("child") {
9716 let s: LitStr = inner.value()?.parse()?;
9717 let trimmed = s.value().trim().to_owned();
9718 if trimmed.is_empty() {
9719 return Err(syn::Error::new(
9720 s.span(),
9721 "`generic_has(child = \"...\")` must not be empty",
9722 ));
9723 }
9724 child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9725 return Ok(());
9726 }
9727 if inner.path.is_ident("ct_column") {
9728 let s: LitStr = inner.value()?.parse()?;
9729 let trimmed = s.value().trim().to_owned();
9730 if trimmed.is_empty() {
9731 return Err(syn::Error::new(
9732 s.span(),
9733 "`generic_has(ct_column = \"...\")` must not be empty",
9734 ));
9735 }
9736 ct_column = Some(trimmed);
9737 return Ok(());
9738 }
9739 if inner.path.is_ident("pk_column") {
9740 let s: LitStr = inner.value()?.parse()?;
9741 let trimmed = s.value().trim().to_owned();
9742 if trimmed.is_empty() {
9743 return Err(syn::Error::new(
9744 s.span(),
9745 "`generic_has(pk_column = \"...\")` must not be empty",
9746 ));
9747 }
9748 pk_column = Some(trimmed);
9749 return Ok(());
9750 }
9751 if inner.path.is_ident("self_pk_column") {
9752 let s: LitStr = inner.value()?.parse()?;
9753 let trimmed = s.value().trim().to_owned();
9754 if trimmed.is_empty() {
9755 return Err(syn::Error::new(
9756 s.span(),
9757 "`generic_has(self_pk_column = \"...\")` must not be empty",
9758 ));
9759 }
9760 self_pk_column = Some(trimmed);
9761 return Ok(());
9762 }
9763 Err(inner.error(
9764 "unknown `generic_has` attribute (supported: \
9765 `name`, `child`, `ct_column`, `pk_column`, \
9766 `self_pk_column`)",
9767 ))
9768 })?;
9769 let Some(name) = accessor_name else {
9770 return Err(syn::Error::new(
9771 span,
9772 "`generic_has` requires `name = \"...\"`",
9773 ));
9774 };
9775 let Some(child) = child_ident else {
9776 return Err(syn::Error::new(
9777 span,
9778 "`generic_has` requires `child = \"ChildModelType\"`",
9779 ));
9780 };
9781 let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9782 let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9783 if out.generic_has_relations.iter().any(|r| r.name == name) {
9784 return Err(syn::Error::new(
9785 span,
9786 format!(
9787 "duplicate `generic_has(name = \"{name}\")` — \
9788 pick a unique accessor name"
9789 ),
9790 ));
9791 }
9792 out.generic_has_relations.push(GenericHasAttr {
9793 name,
9794 child,
9795 ct_column,
9796 pk_column,
9797 self_pk_column,
9798 });
9799 return Ok(());
9800 }
9801 if meta.path.is_ident("audit") {
9802 let mut audit = AuditAttrs::default();
9803 meta.parse_nested_meta(|inner| {
9804 if inner.path.is_ident("track") {
9805 let s: LitStr = inner.value()?.parse()?;
9806 audit.track =
9807 Some((split_field_list(&s.value()), s.span()));
9808 return Ok(());
9809 }
9810 Err(inner.error(
9811 "unknown audit attribute (supported: `track`)",
9812 ))
9813 })?;
9814 out.audit = Some(audit);
9815 return Ok(());
9816 }
9817 if meta.path.is_ident("permissions") {
9818 // Two forms accepted:
9819 // #[rustango(permissions)] — flag form, true
9820 // #[rustango(permissions = false)] — explicit opt-out
9821 // #[rustango(permissions = true)] — explicit opt-in
9822 if let Ok(v) = meta.value() {
9823 let lit: syn::LitBool = v.parse()?;
9824 out.permissions = lit.value;
9825 } else {
9826 out.permissions = true;
9827 }
9828 return Ok(());
9829 }
9830 if meta.path.is_ident("view") {
9831 // Issue #293 / T2.10. Two forms accepted, matching
9832 // the `permissions` flag pattern:
9833 // #[rustango(view)] — flag form, true
9834 // #[rustango(view = false)] — explicit opt-out
9835 // #[rustango(view = true)] — explicit opt-in
9836 if let Ok(v) = meta.value() {
9837 let lit: syn::LitBool = v.parse()?;
9838 out.is_view = lit.value;
9839 } else {
9840 out.is_view = true;
9841 }
9842 return Ok(());
9843 }
9844 if meta.path.is_ident("managed") {
9845 // Who owns the table. Issue #321.
9846 // #[rustango(managed = false)] — operator-managed table
9847 // #[rustango(managed = true)] — rustango-managed (the default)
9848 // Bare-flag form is intentionally not accepted: writing
9849 // `#[rustango(managed)]` reads as "yes please manage it"
9850 // which is already the default. The opt-out is the only
9851 // useful state, so it must be explicit.
9852 let v = meta.value()?;
9853 let lit: syn::LitBool = v.parse()?;
9854 out.managed = lit.value;
9855 return Ok(());
9856 }
9857 if meta.path.is_ident("verbose_name") {
9858 let s: LitStr = meta.value()?.parse()?;
9859 out.verbose_name = Some(s.value());
9860 return Ok(());
9861 }
9862 if meta.path.is_ident("verbose_name_plural") {
9863 let s: LitStr = meta.value()?.parse()?;
9864 out.verbose_name_plural = Some(s.value());
9865 return Ok(());
9866 }
9867 if meta.path.is_ident("db_table_comment") {
9868 // Free-form table-level comment attached to the DB
9869 // catalog.
9870 let s: LitStr = meta.value()?.parse()?;
9871 out.db_table_comment = Some(s.value());
9872 return Ok(());
9873 }
9874 if meta.path.is_ident("proxy") {
9875 // Declarative flag
9876 // marking the struct as a proxy of another model that
9877 // shares its DB table. Stored on `ModelSchema::proxy`
9878 // so future codegen can skip `CreateTable` emission
9879 // for proxies (parent owns the table) and route
9880 // per-instance method resolution to the proxy class.
9881 //
9882 // Accepts `proxy` (bare → true) and `proxy = true/false`.
9883 let value = if meta.input.peek(syn::Token![=]) {
9884 meta.value()?.parse::<syn::LitBool>()?.value
9885 } else {
9886 true
9887 };
9888 out.proxy = value;
9889 return Ok(());
9890 }
9891 if meta.path.is_ident("order_with_respect_to") {
9892 // `order_with_respect_to = "parent_fk"` — the model's
9893 // instances are intrinsically ordered relative to
9894 // their parent FK.
9895 //
9896 // rustango stores the FK field name on
9897 // `ModelSchema::order_with_respect_to`. Declarative-only
9898 // today: the migration writer + admin surfaces still
9899 // treat every model identically. Future codegen will
9900 // key off the metadata to auto-emit the `_order`
9901 // column and reorder helpers.
9902 //
9903 // Validated as a Rust-shape identifier so the macro
9904 // can reject typos at derive time.
9905 let s: LitStr = meta.value()?.parse()?;
9906 let raw = s.value();
9907 if raw.is_empty() {
9908 return Err(syn::Error::new(
9909 s.span(),
9910 "`order_with_respect_to` must be a non-empty FK field name",
9911 ));
9912 }
9913 let valid = raw
9914 .chars()
9915 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9916 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9917 if !valid {
9918 return Err(syn::Error::new(
9919 s.span(),
9920 format!(
9921 "`order_with_respect_to` must be a valid Rust \
9922 identifier (letters / digits / underscores, \
9923 not starting with a digit); got `{raw}`"
9924 ),
9925 ));
9926 }
9927 out.order_with_respect_to = Some(raw);
9928 return Ok(());
9929 }
9930 if meta.path.is_ident("required_db_features") {
9931 // Capability tokens the model needs (e.g.
9932 // `"json_extract"`, `"window_functions"`,
9933 // `"row_security"`). Comma-separated.
9934 // `manage check --deploy` walks every model and warns
9935 // when the active backend doesn't advertise the
9936 // capability.
9937 //
9938 // rustango ships a small registry of capability tokens
9939 // each dialect supports — see `Dialect::supports`.
9940 // Unknown tokens still parse (they end up on the
9941 // schema and show up in the warning) so projects can
9942 // declare aspirational capabilities and the check
9943 // verb will keep nagging until the dialect implements
9944 // them.
9945 let s: LitStr = meta.value()?.parse()?;
9946 let raw = s.value();
9947 let features: Vec<String> = raw
9948 .split(',')
9949 .map(str::trim)
9950 .filter(|s| !s.is_empty())
9951 .map(str::to_owned)
9952 .collect();
9953 if features.is_empty() {
9954 return Err(syn::Error::new(
9955 s.span(),
9956 "`required_db_features` must list at least one \
9957 comma-separated capability token",
9958 ));
9959 }
9960 out.required_db_features = features;
9961 return Ok(());
9962 }
9963 if meta.path.is_ident("required_db_vendor") {
9964 // The model is only meant to run against the named DB
9965 // backend. `manage check --deploy` flags a mismatch so
9966 // ops catches "I forgot to switch DATABASE_URL" at
9967 // deploy time rather than runtime.
9968 //
9969 // Restricted to the three backends rustango ships
9970 // dialects for so the check verb can compare reliably.
9971 let s: LitStr = meta.value()?.parse()?;
9972 let raw = s.value().to_ascii_lowercase();
9973 match raw.as_str() {
9974 "postgresql" | "postgres" | "pg" => {
9975 out.required_db_vendor = Some("postgres".to_owned());
9976 }
9977 "mysql" | "mariadb" => {
9978 out.required_db_vendor = Some("mysql".to_owned());
9979 }
9980 "sqlite" | "sqlite3" => {
9981 out.required_db_vendor = Some("sqlite".to_owned());
9982 }
9983 _ => {
9984 return Err(syn::Error::new(
9985 s.span(),
9986 format!(
9987 "unknown required_db_vendor `{raw}` — \
9988 expected `postgres` (aliases: `postgresql`, `pg`), \
9989 `mysql` (alias: `mariadb`), or `sqlite` \
9990 (alias: `sqlite3`)"
9991 ),
9992 ));
9993 }
9994 }
9995 return Ok(());
9996 }
9997 if meta.path.is_ident("base_manager_name") {
9998 // Name of the manager that `<instance>.<relation>_set`
9999 // uses when resolving reverse-relation managers.
10000 // Distinct from `default_manager_name` (what
10001 // `Self::objects()` returns). Stored on
10002 // `ModelSchema::base_manager_name`.
10003 //
10004 // Validated as a Rust identifier so it stays safe to
10005 // re-emit as code in future reverse-manager codegen.
10006 let s: LitStr = meta.value()?.parse()?;
10007 let raw = s.value();
10008 if raw.is_empty() {
10009 return Err(syn::Error::new(
10010 s.span(),
10011 "`base_manager_name` must be a non-empty string",
10012 ));
10013 }
10014 let valid = raw
10015 .chars()
10016 .all(|c| c == '_' || c.is_ascii_alphanumeric())
10017 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
10018 if !valid {
10019 return Err(syn::Error::new(
10020 s.span(),
10021 format!(
10022 "`base_manager_name` must be a valid Rust \
10023 identifier (letters / digits / underscores, \
10024 not starting with a digit); got `{raw}`"
10025 ),
10026 ));
10027 }
10028 out.base_manager_name = Some(raw);
10029 return Ok(());
10030 }
10031 if meta.path.is_ident("default_related_name") {
10032 // The name reverse-relation accessors use when callers
10033 // don't override `related_name = "..."` on the FK / M2M
10034 // field. Stored on `ModelSchema::default_related_name`
10035 // so future reverse-manager codegen / schema emit /
10036 // admin templates can pick the right accessor name
10037 // (today rustango doesn't auto-emit reverse managers;
10038 // the metadata is the foundation for that work).
10039 //
10040 // Enforced non-empty + ASCII identifier-shape so the
10041 // string is safe to use as a Rust ident later.
10042 let s: LitStr = meta.value()?.parse()?;
10043 let raw = s.value();
10044 if raw.is_empty() {
10045 return Err(syn::Error::new(
10046 s.span(),
10047 "`default_related_name` must be a non-empty string",
10048 ));
10049 }
10050 let valid = raw
10051 .chars()
10052 .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
10053 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
10054 if !valid {
10055 return Err(syn::Error::new(
10056 s.span(),
10057 format!(
10058 "`default_related_name` must be snake_case ASCII \
10059 (lowercase letters / digits / underscores, not \
10060 starting with a digit); got `{raw}`"
10061 ),
10062 ));
10063 }
10064 out.default_related_name = Some(raw);
10065 return Ok(());
10066 }
10067 if meta.path.is_ident("extra_permissions") {
10068 // Comma-separated `codename:label` pairs.
10069 let s: LitStr = meta.value()?.parse()?;
10070 let raw = s.value();
10071 let mut pairs = Vec::new();
10072 for entry in raw.split(',') {
10073 let entry = entry.trim();
10074 if entry.is_empty() {
10075 continue;
10076 }
10077 let (codename, label) = match entry.split_once(':') {
10078 Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
10079 None => (entry.to_owned(), entry.to_owned()),
10080 };
10081 if codename.is_empty() {
10082 return Err(meta.error(
10083 "`extra_permissions` entries must be `codename:label` pairs",
10084 ));
10085 }
10086 pairs.push((codename, label));
10087 }
10088 if pairs.is_empty() {
10089 return Err(meta
10090 .error("`extra_permissions = \"…\"` must list at least one pair"));
10091 }
10092 out.extra_permissions = pairs;
10093 return Ok(());
10094 }
10095 if meta.path.is_ident("default_permissions") {
10096 // Comma-separated subset of the CRUD action set.
10097 // Empty means all four (the framework default).
10098 let s: LitStr = meta.value()?.parse()?;
10099 let raw = s.value();
10100 let mut actions: Vec<String> = Vec::new();
10101 for entry in raw.split(',') {
10102 let action = entry.trim().to_ascii_lowercase();
10103 if action.is_empty() {
10104 continue;
10105 }
10106 match action.as_str() {
10107 "add" | "change" | "delete" | "view" => {}
10108 other => {
10109 return Err(syn::Error::new(
10110 s.span(),
10111 format!(
10112 "unknown default_permissions action `{other}` — \
10113 expected one of `add`, `change`, `delete`, `view`"
10114 ),
10115 ));
10116 }
10117 }
10118 if !actions.contains(&action) {
10119 actions.push(action);
10120 }
10121 }
10122 if actions.is_empty() {
10123 return Err(syn::Error::new(
10124 s.span(),
10125 "`default_permissions = \"…\"` must list at least one action; \
10126 use `permissions = false` on the container if you want NO \
10127 permissions seeded for this model.",
10128 ));
10129 }
10130 out.default_permissions = actions;
10131 return Ok(());
10132 }
10133 if meta.path.is_ident("get_latest_by") {
10134 // The `-` prefix selects descending order.
10135 let s: LitStr = meta.value()?.parse()?;
10136 let raw = s.value();
10137 let trimmed = raw.trim();
10138 if trimmed.is_empty() {
10139 return Err(meta.error("`get_latest_by` must name a column"));
10140 }
10141 let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
10142 (stripped.to_owned(), true)
10143 } else if let Some(stripped) = trimmed.strip_prefix('+') {
10144 (stripped.to_owned(), false)
10145 } else {
10146 (trimmed.to_owned(), false)
10147 };
10148 if col.is_empty() {
10149 return Err(meta.error("`get_latest_by` must name a column"));
10150 }
10151 out.get_latest_by = Some((col, desc, s.span()));
10152 return Ok(());
10153 }
10154 if meta.path.is_ident("unique_together") {
10155 // Composite UNIQUE index. Two syntaxes:
10156 //
10157 // #[rustango(unique_together = "org_id, user_id")] — auto-derived name
10158 // #[rustango(unique_together(columns = "org_id, user_id", name = "x"))] — explicit name
10159 //
10160 // Both produce `CREATE UNIQUE INDEX <name> ON <table>
10161 // (col1, col2)`, where <name> defaults to
10162 // `<table>_<col1>_<col2>_uq` when not supplied.
10163 let (columns, name) = parse_together_attr(&meta, "unique_together", written)?;
10164 out.indexes.push(IndexAttr {
10165 span: meta.path.span(),
10166 name,
10167 columns,
10168 unique: true,
10169 method: "btree".to_owned(),
10170 where_clause: None,
10171 include: Vec::new(),
10172 });
10173 return Ok(());
10174 }
10175 if meta.path.is_ident("index_together") {
10176 // Composite (non-unique) index. Two syntaxes
10177 // mirroring `unique_together`.
10178 //
10179 // #[rustango(index_together = "created_at, status")]
10180 // #[rustango(index_together(columns = "created_at, status", name = "x"))]
10181 let (columns, name) = parse_together_attr(&meta, "index_together", written)?;
10182 out.indexes.push(IndexAttr {
10183 span: meta.path.span(),
10184 name,
10185 columns,
10186 unique: false,
10187 method: "btree".to_owned(),
10188 where_clause: None,
10189 include: Vec::new(),
10190 });
10191 return Ok(());
10192 }
10193 if meta.path.is_ident("unique_when") {
10194 // Partial unique index. Issue #265 / T1.3.
10195 //
10196 // #[rustango(unique_when(
10197 // columns = "email",
10198 // condition = "deleted_at IS NULL",
10199 // name = "unique_active_email"
10200 // ))]
10201 //
10202 // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
10203 // on PG / SQLite (both ship partial indexes natively).
10204 // MySQL falls back to a plain UNIQUE index — the
10205 // condition is lost; document the limitation in the
10206 // generated migration.
10207 let mut columns: Option<Vec<String>> = None;
10208 let mut condition: Option<String> = None;
10209 let mut name: Option<String> = None;
10210 let mut include: Vec<String> = Vec::new();
10211 meta.parse_nested_meta(|inner| {
10212 if inner.path.is_ident("columns") {
10213 let s: LitStr = inner.value()?.parse()?;
10214 columns = Some(split_field_list(&s.value()));
10215 return Ok(());
10216 }
10217 if inner.path.is_ident("condition") {
10218 let s: LitStr = inner.value()?.parse()?;
10219 condition = Some(s.value());
10220 return Ok(());
10221 }
10222 if inner.path.is_ident("name") {
10223 let s: LitStr = inner.value()?.parse()?;
10224 name = Some(s.value());
10225 return Ok(());
10226 }
10227 if inner.path.is_ident("include") {
10228 // PG 11+ covering-index columns. Non-key columns
10229 // travel with the index leaf for index-only
10230 // scans. Dropped on MySQL/SQLite by the writer.
10231 let s: LitStr = inner.value()?.parse()?;
10232 include = split_field_list(&s.value());
10233 return Ok(());
10234 }
10235 Err(inner.error(
10236 "unknown unique_when attribute (supported: \
10237 `columns = \"...\"`, `condition = \"...\"`, \
10238 `name = \"...\"`, `include = \"...\"`)",
10239 ))
10240 })?;
10241 let columns = columns.ok_or_else(|| {
10242 meta.error("`unique_when(...)` requires `columns = \"...\"`")
10243 })?;
10244 let condition = condition.ok_or_else(|| {
10245 meta.error("`unique_when(...)` requires `condition = \"...\"`")
10246 })?;
10247 if columns.is_empty() {
10248 return Err(meta.error("`unique_when(columns = \"\")` is empty"));
10249 }
10250 out.indexes.push(IndexAttr {
10251 span: meta.path.span(),
10252 name,
10253 columns,
10254 unique: true,
10255 method: "btree".to_owned(),
10256 where_clause: Some(condition),
10257 include,
10258 });
10259 return Ok(());
10260 }
10261 if meta.path.is_ident("index_when") {
10262 // Non-unique partial index. Sibling of `unique_when`
10263 // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
10264 //
10265 // #[rustango(index_when(
10266 // columns = "status, created_at",
10267 // condition = "deleted_at IS NULL",
10268 // name = "active_status_created_idx"
10269 // ))]
10270 //
10271 // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
10272 // on PG / SQLite (both ship partial indexes natively).
10273 // MySQL has no native partial-index support — the writer
10274 // emits a plain CREATE INDEX and the condition is lost;
10275 // operators wanting that selectivity on MySQL should
10276 // declare a covering index plus an application-level
10277 // filter.
10278 let mut columns: Option<Vec<String>> = None;
10279 let mut condition: Option<String> = None;
10280 let mut name: Option<String> = None;
10281 let mut method: String = "btree".to_owned();
10282 let mut include: Vec<String> = Vec::new();
10283 meta.parse_nested_meta(|inner| {
10284 if inner.path.is_ident("columns") {
10285 let s: LitStr = inner.value()?.parse()?;
10286 columns = Some(split_field_list(&s.value()));
10287 return Ok(());
10288 }
10289 if inner.path.is_ident("condition") {
10290 let s: LitStr = inner.value()?.parse()?;
10291 condition = Some(s.value());
10292 return Ok(());
10293 }
10294 if inner.path.is_ident("name") {
10295 let s: LitStr = inner.value()?.parse()?;
10296 name = Some(s.value());
10297 return Ok(());
10298 }
10299 if inner.path.is_ident("method") {
10300 let s: LitStr = inner.value()?.parse()?;
10301 method = s.value();
10302 return Ok(());
10303 }
10304 if inner.path.is_ident("include") {
10305 // PG 11+ covering-index columns; non-key
10306 // columns travel with the index leaf. Dropped
10307 // on MySQL/SQLite.
10308 let s: LitStr = inner.value()?.parse()?;
10309 include = split_field_list(&s.value());
10310 return Ok(());
10311 }
10312 Err(inner.error(
10313 "unknown index_when attribute (supported: \
10314 `columns = \"...\"`, `condition = \"...\"`, \
10315 `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
10316 `include = \"...\"`)",
10317 ))
10318 })?;
10319 let columns = columns
10320 .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
10321 let condition = condition.ok_or_else(|| {
10322 meta.error("`index_when(...)` requires `condition = \"...\"`")
10323 })?;
10324 if columns.is_empty() {
10325 return Err(meta.error("`index_when(columns = \"\")` is empty"));
10326 }
10327 out.indexes.push(IndexAttr {
10328 span: meta.path.span(),
10329 name,
10330 columns,
10331 unique: false,
10332 method,
10333 where_clause: Some(condition),
10334 include,
10335 });
10336 return Ok(());
10337 }
10338 if meta.path.is_ident("index") {
10339 // Container-level composite index. Two syntaxes:
10340 // #[rustango(index = "col1, col2")] — bare, non-unique btree
10341 // #[rustango(index("col1, col2"))] — call form (same result)
10342 // #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
10343 // The call form takes the column list as a leading string
10344 // literal, then optional `unique` / `name = "..."` /
10345 // `method = "..."` flags (a leading literal can't compose
10346 // under `parse_nested_meta`, so the paren body is parsed by
10347 // hand). `unique_together` / `index_together` remain
10348 // aliases for the same feature.
10349 let cols_lit: LitStr;
10350 let mut unique = false;
10351 let mut name: Option<String> = None;
10352 let mut method = "btree".to_owned();
10353 if meta.input.peek(syn::token::Paren) {
10354 let content;
10355 syn::parenthesized!(content in meta.input);
10356 cols_lit = content.parse()?;
10357 while content.peek(syn::Token![,]) {
10358 content.parse::<syn::Token![,]>()?;
10359 if content.is_empty() {
10360 break;
10361 }
10362 let flag: syn::Ident = content.parse()?;
10363 if flag == "unique" {
10364 unique = true;
10365 } else if flag == "name" {
10366 content.parse::<syn::Token![=]>()?;
10367 let s: LitStr = content.parse()?;
10368 name = Some(s.value());
10369 } else if flag == "method" {
10370 content.parse::<syn::Token![=]>()?;
10371 let s: LitStr = content.parse()?;
10372 let v = s.value();
10373 match v.as_str() {
10374 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
10375 | "bloom" => method = v,
10376 other => {
10377 return Err(syn::Error::new(
10378 s.span(),
10379 format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
10380 ));
10381 }
10382 }
10383 } else {
10384 return Err(syn::Error::new(
10385 flag.span(),
10386 "unknown index flag (supported: `unique`, `name`, `method`)",
10387 ));
10388 }
10389 }
10390 } else {
10391 cols_lit = meta.value()?.parse()?;
10392 }
10393 let columns = split_field_list(&cols_lit.value());
10394 out.indexes.push(IndexAttr {
10395 span: meta.path.span(),
10396 name,
10397 columns,
10398 unique,
10399 method,
10400 where_clause: None,
10401 include: Vec::new(),
10402 });
10403 return Ok(());
10404 }
10405 if meta.path.is_ident("check") {
10406 // #[rustango(check(name = "…", expr = "…"))]
10407 let mut name: Option<String> = None;
10408 let mut expr: Option<String> = None;
10409 meta.parse_nested_meta(|inner| {
10410 if inner.path.is_ident("name") {
10411 let s: LitStr = inner.value()?.parse()?;
10412 name = Some(s.value());
10413 return Ok(());
10414 }
10415 if inner.path.is_ident("expr") {
10416 let s: LitStr = inner.value()?.parse()?;
10417 expr = Some(s.value());
10418 return Ok(());
10419 }
10420 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
10421 })?;
10422 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
10423 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
10424 out.checks.push(CheckAttr { name, expr });
10425 return Ok(());
10426 }
10427 if meta.path.is_ident("exclude") {
10428 // #[rustango(exclude(name = "…", using = "gist",
10429 // elements = "col WITH op, col WITH op",
10430 // where = "…"))]
10431 let mut name: Option<String> = None;
10432 let mut using: Option<String> = None;
10433 let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
10434 let mut where_clause: Option<String> = None;
10435 meta.parse_nested_meta(|inner| {
10436 if inner.path.is_ident("name") {
10437 let s: LitStr = inner.value()?.parse()?;
10438 name = Some(s.value());
10439 return Ok(());
10440 }
10441 if inner.path.is_ident("using") {
10442 let s: LitStr = inner.value()?.parse()?;
10443 using = Some(s.value());
10444 return Ok(());
10445 }
10446 if inner.path.is_ident("elements") {
10447 let s: LitStr = inner.value()?.parse()?;
10448 elements_raw = Some((s.value(), s.span()));
10449 return Ok(());
10450 }
10451 if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
10452 let s: LitStr = inner.value()?.parse()?;
10453 where_clause = Some(s.value());
10454 return Ok(());
10455 }
10456 Err(inner.error(
10457 "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
10458 ))
10459 })?;
10460 let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
10461 let using = using.unwrap_or_else(|| "gist".to_owned());
10462 let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
10463 meta.error(
10464 "exclude requires `elements = \"col WITH op, col WITH op\"`",
10465 )
10466 })?;
10467 // Parse `col WITH op` pairs separated by commas.
10468 let mut elements: Vec<(String, String)> = Vec::new();
10469 for pair in elements_str.split(',') {
10470 let pair = pair.trim();
10471 if pair.is_empty() {
10472 continue;
10473 }
10474 let mut split = pair.splitn(2, |c: char| c.is_whitespace());
10475 let col = split.next().unwrap_or("").trim();
10476 let rest = split.next().unwrap_or("").trim();
10477 // `WITH op` — case-insensitive on `WITH`, then op.
10478 let rest_lc = rest.to_ascii_lowercase();
10479 let op = rest_lc
10480 .strip_prefix("with")
10481 .map(|r| r.trim_start())
10482 .filter(|r| !r.is_empty())
10483 .map(|_| {
10484 // Pull the original-case op from `rest` after the
10485 // `WITH ` token (5 chars).
10486 rest[4..].trim_start().to_owned()
10487 });
10488 let Some(op) = op else {
10489 return Err(syn::Error::new(
10490 elements_span,
10491 format!(
10492 "exclude elements: `{pair}` must be `<col> WITH <op>` \
10493 (e.g. `room_id WITH =` or `during WITH &&`)"
10494 ),
10495 ));
10496 };
10497 if col.is_empty() || op.is_empty() {
10498 return Err(syn::Error::new(
10499 elements_span,
10500 format!(
10501 "exclude elements: `{pair}` must be `<col> WITH <op>` \
10502 (both sides non-empty)"
10503 ),
10504 ));
10505 }
10506 elements.push((col.to_owned(), op));
10507 }
10508 if elements.is_empty() {
10509 return Err(syn::Error::new(
10510 elements_span,
10511 "exclude requires at least one `col WITH op` element",
10512 ));
10513 }
10514 out.excludes.push(ExcludeAttr {
10515 name,
10516 using,
10517 elements,
10518 where_clause,
10519 });
10520 return Ok(());
10521 }
10522 if meta.path.is_ident("generic_fk") {
10523 let mut gfk = GenericFkAttr {
10524 name: String::new(),
10525 ct_column: String::new(),
10526 pk_column: String::new(),
10527 };
10528 meta.parse_nested_meta(|inner| {
10529 if inner.path.is_ident("name") {
10530 let s: LitStr = inner.value()?.parse()?;
10531 gfk.name = s.value();
10532 return Ok(());
10533 }
10534 if inner.path.is_ident("ct_column") {
10535 let s: LitStr = inner.value()?.parse()?;
10536 gfk.ct_column = s.value();
10537 return Ok(());
10538 }
10539 if inner.path.is_ident("pk_column") {
10540 let s: LitStr = inner.value()?.parse()?;
10541 gfk.pk_column = s.value();
10542 return Ok(());
10543 }
10544 Err(inner.error(
10545 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10546 ))
10547 })?;
10548 if gfk.name.is_empty() {
10549 return Err(meta.error("generic_fk requires `name = \"...\"`"));
10550 }
10551 if gfk.ct_column.is_empty() {
10552 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10553 }
10554 if gfk.pk_column.is_empty() {
10555 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10556 }
10557 out.generic_fks.push(gfk);
10558 return Ok(());
10559 }
10560 if meta.path.is_ident("fk_composite") {
10561 let mut fk = CompositeFkAttr {
10562 name: String::new(),
10563 to: String::new(),
10564 from: Vec::new(),
10565 on: Vec::new(),
10566 };
10567 meta.parse_nested_meta(|inner| {
10568 if inner.path.is_ident("name") {
10569 let s: LitStr = inner.value()?.parse()?;
10570 fk.name = s.value();
10571 return Ok(());
10572 }
10573 if inner.path.is_ident("to") {
10574 let s: LitStr = inner.value()?.parse()?;
10575 fk.to = s.value();
10576 return Ok(());
10577 }
10578 // `on = ("col1", "col2", ...)` — parse a parenthesised
10579 // comma-list of string literals.
10580 if inner.path.is_ident("on") || inner.path.is_ident("from") {
10581 let value = inner.value()?;
10582 let content;
10583 syn::parenthesized!(content in value);
10584 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10585 content.parse_terminated(
10586 |p| p.parse::<syn::LitStr>(),
10587 syn::Token![,],
10588 )?;
10589 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10590 if inner.path.is_ident("on") {
10591 fk.on = cols;
10592 } else {
10593 fk.from = cols;
10594 }
10595 return Ok(());
10596 }
10597 Err(inner.error(
10598 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10599 ))
10600 })?;
10601 if fk.name.is_empty() {
10602 return Err(meta.error("fk_composite requires `name = \"...\"`"));
10603 }
10604 if fk.to.is_empty() {
10605 return Err(meta.error("fk_composite requires `to = \"...\"`"));
10606 }
10607 if fk.from.is_empty() || fk.on.is_empty() {
10608 return Err(meta.error(
10609 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10610 ));
10611 }
10612 if fk.from.len() != fk.on.len() {
10613 return Err(meta.error(format!(
10614 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10615 fk.from.len(),
10616 fk.on.len(),
10617 )));
10618 }
10619 out.composite_fks.push(fk);
10620 return Ok(());
10621 }
10622 if meta.path.is_ident("m2m") {
10623 let mut m2m = M2MAttr {
10624 name: String::new(),
10625 to: String::new(),
10626 through: String::new(),
10627 src: String::new(),
10628 dst: String::new(),
10629 auto_create: true,
10630 };
10631 meta.parse_nested_meta(|inner| {
10632 if inner.path.is_ident("name") {
10633 let s: LitStr = inner.value()?.parse()?;
10634 m2m.name = s.value();
10635 return Ok(());
10636 }
10637 if inner.path.is_ident("to") {
10638 let s: LitStr = inner.value()?.parse()?;
10639 m2m.to = s.value();
10640 return Ok(());
10641 }
10642 if inner.path.is_ident("through") {
10643 let s: LitStr = inner.value()?.parse()?;
10644 m2m.through = s.value();
10645 return Ok(());
10646 }
10647 if inner.path.is_ident("src") {
10648 let s: LitStr = inner.value()?.parse()?;
10649 m2m.src = s.value();
10650 return Ok(());
10651 }
10652 if inner.path.is_ident("dst") {
10653 let s: LitStr = inner.value()?.parse()?;
10654 m2m.dst = s.value();
10655 return Ok(());
10656 }
10657 if inner.path.is_ident("auto_create") {
10658 let lit: syn::LitBool = inner.value()?.parse()?;
10659 m2m.auto_create = lit.value;
10660 return Ok(());
10661 }
10662 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10663 })?;
10664 if m2m.name.is_empty() {
10665 return Err(meta.error("m2m requires `name = \"...\"`"));
10666 }
10667 if m2m.to.is_empty() {
10668 return Err(meta.error("m2m requires `to = \"...\"`"));
10669 }
10670 if m2m.through.is_empty() {
10671 return Err(meta.error("m2m requires `through = \"...\"`"));
10672 }
10673 if m2m.src.is_empty() {
10674 return Err(meta.error("m2m requires `src = \"...\"`"));
10675 }
10676 if m2m.dst.is_empty() {
10677 return Err(meta.error("m2m requires `dst = \"...\"`"));
10678 }
10679 out.m2m.push(m2m);
10680 return Ok(());
10681 }
10682 if meta.path.is_ident("generic_m2m") {
10683 let mut gm = GenericM2MAttr {
10684 name: String::new(),
10685 through: String::new(),
10686 pk_column: String::new(),
10687 ct_column: String::new(),
10688 related_column: String::new(),
10689 };
10690 meta.parse_nested_meta(|inner| {
10691 let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10692 let s: LitStr = inner.value()?.parse()?;
10693 Ok(s.value())
10694 };
10695 if inner.path.is_ident("name") {
10696 gm.name = field(&inner)?;
10697 return Ok(());
10698 }
10699 if inner.path.is_ident("through") {
10700 gm.through = field(&inner)?;
10701 return Ok(());
10702 }
10703 if inner.path.is_ident("pk_column") {
10704 gm.pk_column = field(&inner)?;
10705 return Ok(());
10706 }
10707 if inner.path.is_ident("ct_column") {
10708 gm.ct_column = field(&inner)?;
10709 return Ok(());
10710 }
10711 if inner.path.is_ident("related_column") {
10712 gm.related_column = field(&inner)?;
10713 return Ok(());
10714 }
10715 Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10716 })?;
10717 for (val, label) in [
10718 (&gm.name, "name"),
10719 (&gm.through, "through"),
10720 (&gm.pk_column, "pk_column"),
10721 (&gm.ct_column, "ct_column"),
10722 (&gm.related_column, "related_column"),
10723 ] {
10724 if val.is_empty() {
10725 return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10726 }
10727 }
10728 out.generic_m2m.push(gm);
10729 return Ok(());
10730 }
10731 Err(unknown_key_error(&meta, "container", CONTAINER_ATTR_KEYS))
10732 })?;
10733 }
10734 Ok(out)
10735}
10736
10737/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10738/// owned field names, trimming whitespace and skipping empty entries.
10739/// Field-name validation against the model is done by the caller.
10740fn split_field_list(raw: &str) -> Vec<String> {
10741 raw.split(',')
10742 .map(str::trim)
10743 .filter(|s| !s.is_empty())
10744 .map(str::to_owned)
10745 .collect()
10746}
10747
10748/// Shared parser for `unique_together` and `index_together` container
10749/// attrs. Accepts both shapes:
10750///
10751/// * `attr = "col1, col2"` — auto-derived index name.
10752/// * `attr(columns = "col1, col2", name = "...")` — explicit name.
10753///
10754/// Returns `(columns, name)`.
10755fn parse_together_attr(
10756 meta: &syn::meta::ParseNestedMeta<'_>,
10757 attr: &str,
10758 written: &str,
10759) -> syn::Result<(Vec<String>, Option<String>)> {
10760 // Disambiguate by whether the next token is `=` (key-value) or
10761 // `(` (parenthesized).
10762 if meta.input.peek(syn::Token![=]) {
10763 let cols_lit: LitStr = meta.value()?.parse()?;
10764 let columns = split_field_list(&cols_lit.value());
10765 check_together_columns(meta, attr, written, &columns)?;
10766 return Ok((columns, None));
10767 }
10768 let mut columns: Option<Vec<String>> = None;
10769 let mut name: Option<String> = None;
10770 meta.parse_nested_meta(|inner| {
10771 if inner.path.is_ident("columns") {
10772 let s: LitStr = inner.value()?.parse()?;
10773 columns = Some(split_field_list(&s.value()));
10774 return Ok(());
10775 }
10776 if inner.path.is_ident("name") {
10777 let s: LitStr = inner.value()?.parse()?;
10778 name = Some(s.value());
10779 return Ok(());
10780 }
10781 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10782 })?;
10783 let columns = columns.ok_or_else(|| {
10784 meta.error(format!(
10785 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10786 ))
10787 })?;
10788 check_together_columns(meta, attr, written, &columns)?;
10789 Ok((columns, name))
10790}
10791
10792fn check_together_columns(
10793 meta: &syn::meta::ParseNestedMeta<'_>,
10794 attr: &str,
10795 written: &str,
10796 columns: &[String],
10797) -> syn::Result<()> {
10798 if columns.len() < 2 {
10799 let flag = if attr == "unique_together" {
10800 "unique"
10801 } else {
10802 "index"
10803 };
10804 let single = format!("#[{written}({flag})] on the field");
10805 return Err(meta.error(format!(
10806 "{attr} expects two or more columns; for a single-column equivalent use {single}",
10807 )));
10808 }
10809 Ok(())
10810}
10811
10812/// Parse the fieldsets DSL: pipe-separated sections, optional
10813/// `"Title:"` prefix on each, comma-separated field names after.
10814/// Examples:
10815/// * `"name, office"` → one untitled section with two fields
10816/// * `"Identity: name, office | Metadata: created_at"` → two titled
10817/// sections
10818///
10819/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10820fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10821 raw.split('|')
10822 .map(str::trim)
10823 .filter(|s| !s.is_empty())
10824 .map(|section| {
10825 // Split off an optional `Title:` prefix (first colon).
10826 let (title, rest) = match section.split_once(':') {
10827 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10828 _ => (String::new(), section),
10829 };
10830 let fields = split_field_list(rest);
10831 (title, fields)
10832 })
10833 .collect()
10834}
10835
10836/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10837/// comma-separated entry maps a target field to one or more source
10838/// fields joined with `+`. Whitespace around tokens is trimmed.
10839/// Entries missing `:` or with empty target/source lists are dropped.
10840fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10841 raw.split(',')
10842 .map(str::trim)
10843 .filter(|s| !s.is_empty())
10844 .filter_map(|entry| {
10845 let (target, sources_raw) = entry.split_once(':')?;
10846 let target = target.trim().to_owned();
10847 if target.is_empty() {
10848 return None;
10849 }
10850 let sources: Vec<String> = sources_raw
10851 .split('+')
10852 .map(|s| s.trim().to_owned())
10853 .filter(|s| !s.is_empty())
10854 .collect();
10855 if sources.is_empty() {
10856 return None;
10857 }
10858 Some((target, sources))
10859 })
10860 .collect()
10861}
10862
10863/// Parse `formfield_overrides` — `"field:widget,field2:widget2"`
10864/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10865/// and empty halves drop silently — the macro layer only enforces shape,
10866/// not field-name vs. widget-name validity (those checks happen at
10867/// `AdminConfig` consumption time). Issue #359.
10868fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10869 raw.split(',')
10870 .map(str::trim)
10871 .filter(|s| !s.is_empty())
10872 .filter_map(|entry| {
10873 let (field, widget) = entry.split_once(':')?;
10874 let field = field.trim().to_owned();
10875 let widget = widget.trim().to_owned();
10876 if field.is_empty() || widget.is_empty() {
10877 return None;
10878 }
10879 Some((field, widget))
10880 })
10881 .collect()
10882}
10883
10884/// Parse an ordering list — `"name"` is ASC, `"-name"` is DESC.
10885/// Returns `(field_name, desc)` pairs in the same order as the input.
10886fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10887 raw.split(',')
10888 .map(str::trim)
10889 .filter(|s| !s.is_empty())
10890 .map(|spec| {
10891 spec.strip_prefix('-')
10892 .map_or((spec.to_owned(), false), |rest| {
10893 (rest.trim().to_owned(), true)
10894 })
10895 })
10896 .collect()
10897}
10898
10899struct FieldAttrs {
10900 column: Option<String>,
10901 primary_key: bool,
10902 fk: Option<String>,
10903 o2o: Option<String>,
10904 on: Option<String>,
10905 /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10906 /// "set_default" | "no_action")]` — what happens to this row when
10907 /// the row it points at is deleted.
10908 /// Only meaningful on a `ForeignKey<T>` field or with `fk` / `o2o`
10909 /// set; the macro errors at compile time if applied to a
10910 /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10911 /// DDL writer renders `ON DELETE <action>` after the constraint
10912 /// clause when this is `Some`; `None` falls back to the database
10913 /// default (NO ACTION on every backend rustango supports).
10914 on_delete: Option<String>,
10915 /// `#[rustango(related_name = "...")]` — per-FK
10916 /// reverse-accessor override. When set, the derive emits
10917 /// `Parent::<related_name>[_pool]` instead of the container-level
10918 /// `default_related_name` or the `<child_snake>_set[_pool]`
10919 /// fallback. Only meaningful when `fk` / `o2o` is also set;
10920 /// silently ignored on non-FK fields. Follow-up to #816.
10921 /// `"+"` emits none, for a parent defined in another crate.
10922 related_name: Option<String>,
10923 max_length: Option<u32>,
10924 /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10925 /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10926 /// unconstrained `vector` column.
10927 vector_dims: Option<u32>,
10928 /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10929 /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10930 /// unconstrained `geometry(Point)` column (SRID 0).
10931 geometry_srid: Option<u32>,
10932 min: Option<i64>,
10933 max: Option<i64>,
10934 default: Option<String>,
10935 /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10936 /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10937 /// "gen_random_uuid()"`. The Rust field type must be
10938 /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10939 /// INSERTs so the DB DEFAULT fires.
10940 auto_uuid: bool,
10941 /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10942 /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10943 /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10944 /// the field is `Auto::Unset`, then bound as a normal parameter
10945 /// rather than relying on a per-dialect DB function. Issue #823
10946 /// (Eloquent `HasUuids`).
10947 ///
10948 /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10949 /// Composes with every backend (PG / MySQL / SQLite) — no
10950 /// `gen_random_uuid()` requirement on the database.
10951 default_uuid_v7: bool,
10952 /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10953 /// Server-set on insert, immutable from app code afterwards.
10954 /// Implies `auto + default = "now()"`. Field type must be
10955 /// `DateTime<Utc>`.
10956 auto_now_add: bool,
10957 /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10958 /// every insert AND every update. Implies `auto + default =
10959 /// "now()"`; the macro additionally rewrites `update_on` /
10960 /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10961 /// field value.
10962 auto_now: bool,
10963 /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10964 /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10965 /// `soft_delete_on(executor)` and `restore_on(executor)`
10966 /// methods on the model.
10967 soft_delete: bool,
10968 /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10969 /// the column in the generated DDL.
10970 unique: bool,
10971 /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10972 /// generates a `CREATE INDEX` for this column. `unique` here means
10973 /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10974 index: bool,
10975 index_unique: bool,
10976 index_name: Option<String>,
10977 /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10978 /// `"btree"`. Issue #34.
10979 index_method: String,
10980 /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10981 /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10982 /// the macro skips this column from every INSERT and UPDATE
10983 /// path, so the database always recomputes the value from
10984 /// `EXPR`. Backlog item #35.
10985 generated_as: Option<String>,
10986 /// `#[rustango(help_text = "…")]` — help text
10987 /// rendered below the admin form's input. Threaded into
10988 /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10989 /// layers can read it.
10990 help_text: Option<String>,
10991 /// `#[rustango(choices = "value:Label, value:Label")]` — the
10992 /// enumerated allowed values. Threaded into `FieldSchema::choices`
10993 /// as a `&'static [(&'static str, &'static str)]` slice. When
10994 /// present, the admin form renders a `<select>` instead of `<input>`
10995 /// and the validator rejects values not in the list. Only meaningful
10996 /// for `FieldType::String`; the macro errors at compile time if
10997 /// applied to a non-string field.
10998 choices: Option<Vec<(String, String)>>,
10999 /// `#[rustango(db_comment = "…")]` — DB-side column
11000 /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
11001 /// the comment in CREATE TABLE; Postgres emits a separate
11002 /// `COMMENT ON COLUMN` statement after the table is created;
11003 /// SQLite silently drops the value (no native column comments).
11004 db_comment: Option<String>,
11005 /// `#[rustango(verbose_name = "…")]` — human-readable
11006 /// label for the field. Threaded into `FieldSchema::verbose_name`
11007 /// so admin column headers, form labels, and other display
11008 /// surfaces can prefer the friendly caption over the Rust
11009 /// identifier. `None` means renderers fall back to the field name.
11010 verbose_name: Option<String>,
11011 /// `#[rustango(editable = false)]` — opt out of
11012 /// auto-generated form rendering. Defaults to `true` so existing
11013 /// fields keep their current admin / form behavior; setting
11014 /// `false` removes the field from the admin change-form entirely
11015 /// (the value is still visible on detail / list views, just not
11016 /// editable).
11017 editable: bool,
11018 /// `#[rustango(blank)]` / `#[rustango(blank = true)]` —
11019 /// "form may submit empty even when DB is NOT NULL". Threaded into
11020 /// `FieldSchema::blank`. Defaults to `false`.
11021 blank: bool,
11022 /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
11023 /// case-insensitive text column. Threaded into
11024 /// `FieldSchema::case_insensitive`. Only meaningful for `String`
11025 /// fields; the macro errors at derive time if applied elsewhere.
11026 case_insensitive: bool,
11027 /// `#[rustango(validators = "email,url")]` — the
11028 /// model-level validator chain. Comma-separated names that
11029 /// dispatch to the `validators::*` family in `validate_value`.
11030 /// Empty by default; fires on every typed INSERT/UPDATE.
11031 validators: Vec<String>,
11032}
11033
11034/// Keys `parse_field_attrs` accepts, listed in its unknown-key error.
11035const FIELD_ATTR_KEYS: &[&str] = &[
11036 "column",
11037 "primary_key",
11038 "fk",
11039 "o2o",
11040 "on",
11041 "on_delete",
11042 "related_name",
11043 "max_length",
11044 "vector",
11045 "geometry",
11046 "min",
11047 "max",
11048 "default",
11049 "generated_as",
11050 "help_text",
11051 "choices",
11052 "db_comment",
11053 "verbose_name",
11054 "editable",
11055 "blank",
11056 "citext",
11057 "validators",
11058 "auto_uuid",
11059 "default_uuid_v7",
11060 "auto_now_add",
11061 "auto_now",
11062 "soft_delete",
11063 "unique",
11064 "index",
11065];
11066
11067/// Keys `parse_container_attrs` accepts, listed in its unknown-key error.
11068const CONTAINER_ATTR_KEYS: &[&str] = &[
11069 "table",
11070 "display",
11071 "app",
11072 "scope",
11073 "admin",
11074 "manager",
11075 "manager_fn",
11076 "default_order",
11077 "global_scope",
11078 "through",
11079 "reverse_has",
11080 "generic_has",
11081 "audit",
11082 "permissions",
11083 "view",
11084 "managed",
11085 "verbose_name",
11086 "verbose_name_plural",
11087 "db_table_comment",
11088 "proxy",
11089 "order_with_respect_to",
11090 "required_db_features",
11091 "required_db_vendor",
11092 "base_manager_name",
11093 "default_related_name",
11094 "extra_permissions",
11095 "default_permissions",
11096 "get_latest_by",
11097 "unique_together",
11098 "index_together",
11099 "unique_when",
11100 "index_when",
11101 "index",
11102 "check",
11103 "exclude",
11104 "generic_fk",
11105 "fk_composite",
11106 "m2m",
11107 "generic_m2m",
11108];
11109
11110/// A flag attribute: `#[rustango(key)]` or `#[rustango(key = true)]`.
11111/// `= false` is refused: off is the default, so the attribute is a no-op.
11112fn parse_flag(meta: &syn::meta::ParseNestedMeta<'_>, key: &str) -> syn::Result<()> {
11113 if meta.input.peek(syn::Token![=]) {
11114 let lit: syn::LitBool = meta.value()?.parse()?;
11115 if !lit.value {
11116 return Err(syn::Error::new(
11117 lit.span(),
11118 format!("`{key} = false` is the default; remove the attribute"),
11119 ));
11120 }
11121 }
11122 Ok(())
11123}
11124
11125/// "unknown rustango <what> attribute `key`", with the nearest key and the full list.
11126fn unknown_key_error(
11127 meta: &syn::meta::ParseNestedMeta<'_>,
11128 what: &str,
11129 keys: &[&str],
11130) -> syn::Error {
11131 let path = &meta.path;
11132 let got = path
11133 .get_ident()
11134 .map_or_else(|| quote!(#path).to_string(), ToString::to_string);
11135 let mut msg = format!("unknown rustango {what} attribute `{got}`");
11136 if let Some(near) = keys
11137 .iter()
11138 .map(|k| (edit_distance(&got, k), *k))
11139 .filter(|(d, k)| *d <= 2.max(k.len() / 4))
11140 .min_by_key(|(d, _)| *d)
11141 .map(|(_, k)| k)
11142 {
11143 msg.push_str(&format!("; did you mean `{near}`?"));
11144 }
11145 msg.push_str(&format!(" Supported: {}", keys.join(", ")));
11146 meta.error(msg)
11147}
11148
11149/// Levenshtein distance, for "did you mean" hints.
11150fn edit_distance(a: &str, b: &str) -> usize {
11151 let b: Vec<char> = b.chars().collect();
11152 let mut prev: Vec<usize> = (0..=b.len()).collect();
11153 for (i, ca) in a.chars().enumerate() {
11154 let mut cur = vec![i + 1];
11155 for (j, cb) in b.iter().enumerate() {
11156 let sub = prev[j] + usize::from(ca != *cb);
11157 cur.push(sub.min(prev[j + 1] + 1).min(cur[j] + 1));
11158 }
11159 prev = cur;
11160 }
11161 prev[b.len()]
11162}
11163
11164fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
11165 let mut out = FieldAttrs {
11166 column: None,
11167 primary_key: false,
11168 fk: None,
11169 o2o: None,
11170 on: None,
11171 on_delete: None,
11172 related_name: None,
11173 max_length: None,
11174 vector_dims: None,
11175 geometry_srid: None,
11176 min: None,
11177 max: None,
11178 default: None,
11179 auto_uuid: false,
11180 default_uuid_v7: false,
11181 auto_now_add: false,
11182 auto_now: false,
11183 soft_delete: false,
11184 unique: false,
11185 index: false,
11186 index_unique: false,
11187 index_name: None,
11188 index_method: "btree".to_owned(),
11189 generated_as: None,
11190 help_text: None,
11191 choices: None,
11192 db_comment: None,
11193 verbose_name: None,
11194 editable: true,
11195 blank: false,
11196 case_insensitive: false,
11197 validators: Vec::new(),
11198 };
11199 for attr in model_attrs(&field.attrs)? {
11200 attr.parse_nested_meta(|meta| {
11201 if meta.path.is_ident("column") {
11202 let s: LitStr = meta.value()?.parse()?;
11203 let name = s.value();
11204 validate_sql_identifier(&name, "column", s.span())?;
11205 out.column = Some(name);
11206 return Ok(());
11207 }
11208 if meta.path.is_ident("primary_key") {
11209 parse_flag(&meta, "primary_key")?;
11210 out.primary_key = true;
11211 return Ok(());
11212 }
11213 if meta.path.is_ident("fk") {
11214 let s: LitStr = meta.value()?.parse()?;
11215 out.fk = Some(s.value());
11216 return Ok(());
11217 }
11218 if meta.path.is_ident("o2o") {
11219 let s: LitStr = meta.value()?.parse()?;
11220 out.o2o = Some(s.value());
11221 return Ok(());
11222 }
11223 if meta.path.is_ident("on") {
11224 let s: LitStr = meta.value()?.parse()?;
11225 out.on = Some(s.value());
11226 return Ok(());
11227 }
11228 if meta.path.is_ident("on_delete") {
11229 let s: LitStr = meta.value()?.parse()?;
11230 let raw = s.value();
11231 let normalized = raw.trim().to_ascii_lowercase();
11232 // Validate at parse time so the user gets a clear span
11233 // rather than a downstream compile error in the emit.
11234 match normalized.as_str() {
11235 "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
11236 _ => {
11237 return Err(syn::Error::new(
11238 s.span(),
11239 format!(
11240 "unknown on_delete action `{raw}`; expected one of \
11241 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
11242 ),
11243 ));
11244 }
11245 }
11246 out.on_delete = Some(normalized);
11247 return Ok(());
11248 }
11249 if meta.path.is_ident("related_name") {
11250 let s: LitStr = meta.value()?.parse()?;
11251 let raw = s.value();
11252 if raw.trim().is_empty() {
11253 return Err(syn::Error::new(
11254 s.span(),
11255 "`related_name` must be a non-empty identifier",
11256 ));
11257 }
11258 // Validate as a Rust-ident shape — the value becomes
11259 // a method name on the parent type. Same rule as
11260 // `default_related_name` so the two surfaces match.
11261 // `"+"` emits no accessor at all.
11262 let snake = raw
11263 .chars()
11264 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
11265 && !raw.starts_with(char::is_numeric);
11266 if !snake && raw != NO_REVERSE_ACCESSOR {
11267 return Err(syn::Error::new(
11268 s.span(),
11269 "`related_name` must be snake_case ASCII (lowercase letters, \
11270 digits, underscores; no leading digit), or \"+\" for none",
11271 ));
11272 }
11273 out.related_name = Some(raw);
11274 return Ok(());
11275 }
11276 if meta.path.is_ident("max_length") {
11277 let lit: syn::LitInt = meta.value()?.parse()?;
11278 out.max_length = Some(lit.base10_parse::<u32>()?);
11279 return Ok(());
11280 }
11281 // `#[rustango(vector(dims = N))]` — pgvector column
11282 // dimension (#824). Nested-meta form so it reads like the
11283 // other typed-column attrs.
11284 if meta.path.is_ident("vector") {
11285 meta.parse_nested_meta(|inner| {
11286 if inner.path.is_ident("dims") {
11287 let lit: syn::LitInt = inner.value()?.parse()?;
11288 out.vector_dims = Some(lit.base10_parse::<u32>()?);
11289 return Ok(());
11290 }
11291 Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
11292 })?;
11293 return Ok(());
11294 }
11295 // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
11296 // (#443). Nested-meta form, mirroring `vector(dims = N)`.
11297 if meta.path.is_ident("geometry") {
11298 meta.parse_nested_meta(|inner| {
11299 if inner.path.is_ident("srid") {
11300 let lit: syn::LitInt = inner.value()?.parse()?;
11301 out.geometry_srid = Some(lit.base10_parse::<u32>()?);
11302 return Ok(());
11303 }
11304 Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
11305 })?;
11306 return Ok(());
11307 }
11308 if meta.path.is_ident("min") {
11309 out.min = Some(parse_signed_i64(&meta)?);
11310 return Ok(());
11311 }
11312 if meta.path.is_ident("max") {
11313 out.max = Some(parse_signed_i64(&meta)?);
11314 return Ok(());
11315 }
11316 if meta.path.is_ident("default") {
11317 let s: LitStr = meta.value()?.parse()?;
11318 out.default = Some(s.value());
11319 return Ok(());
11320 }
11321 if meta.path.is_ident("generated_as") {
11322 let s: LitStr = meta.value()?.parse()?;
11323 out.generated_as = Some(s.value());
11324 return Ok(());
11325 }
11326 if meta.path.is_ident("help_text") {
11327 let s: LitStr = meta.value()?.parse()?;
11328 out.help_text = Some(s.value());
11329 return Ok(());
11330 }
11331 if meta.path.is_ident("choices") {
11332 let s: LitStr = meta.value()?.parse()?;
11333 let raw = s.value();
11334 let mut pairs: Vec<(String, String)> = Vec::new();
11335 for chunk in raw.split(',') {
11336 let chunk = chunk.trim();
11337 if chunk.is_empty() {
11338 continue;
11339 }
11340 let (value, label) = match chunk.split_once(':') {
11341 Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
11342 None => (chunk.to_owned(), chunk.to_owned()),
11343 };
11344 if value.is_empty() {
11345 return Err(syn::Error::new(
11346 s.span(),
11347 "`choices` entry has empty value before `:`",
11348 ));
11349 }
11350 pairs.push((value, label));
11351 }
11352 if pairs.is_empty() {
11353 return Err(syn::Error::new(
11354 s.span(),
11355 "`choices = \"…\"` must contain at least one value",
11356 ));
11357 }
11358 out.choices = Some(pairs);
11359 return Ok(());
11360 }
11361 if meta.path.is_ident("db_comment") {
11362 let s: LitStr = meta.value()?.parse()?;
11363 out.db_comment = Some(s.value());
11364 return Ok(());
11365 }
11366 if meta.path.is_ident("verbose_name") {
11367 let s: LitStr = meta.value()?.parse()?;
11368 out.verbose_name = Some(s.value());
11369 return Ok(());
11370 }
11371 if meta.path.is_ident("editable") {
11372 // Two forms accepted:
11373 // #[rustango(editable = false)] / true — explicit
11374 // #[rustango(editable)] — flag form (= true, the
11375 // default, so harmless; included for symmetry)
11376 if let Ok(v) = meta.value() {
11377 let lit: syn::LitBool = v.parse()?;
11378 out.editable = lit.value;
11379 } else {
11380 out.editable = true;
11381 }
11382 return Ok(());
11383 }
11384 if meta.path.is_ident("blank") {
11385 // Two forms accepted:
11386 // #[rustango(blank)] — flag form, true
11387 // #[rustango(blank = false)] / true — explicit
11388 if let Ok(v) = meta.value() {
11389 let lit: syn::LitBool = v.parse()?;
11390 out.blank = lit.value;
11391 } else {
11392 out.blank = true;
11393 }
11394 return Ok(());
11395 }
11396 if meta.path.is_ident("citext") {
11397 // Case-insensitive text column (#344). Two forms:
11398 // #[rustango(citext)] — flag form, true
11399 // #[rustango(citext = true)] — explicit
11400 // #[rustango(citext = false)] — explicit opt-out
11401 // String-only validation lives in the field-type
11402 // emitter (the FieldType discriminant is computed in
11403 // `detect_type`); the macro records the flag and the
11404 // DDL writer emits dialect-specific COLLATE / CITEXT.
11405 if let Ok(v) = meta.value() {
11406 let lit: syn::LitBool = v.parse()?;
11407 out.case_insensitive = lit.value;
11408 } else {
11409 out.case_insensitive = true;
11410 }
11411 return Ok(());
11412 }
11413 if meta.path.is_ident("validators") {
11414 let s: LitStr = meta.value()?.parse()?;
11415 let raw = s.value();
11416 out.validators = raw
11417 .split(',')
11418 .map(str::trim)
11419 .filter(|s| !s.is_empty())
11420 .map(str::to_owned)
11421 .collect();
11422 if out.validators.is_empty() {
11423 return Err(syn::Error::new(
11424 s.span(),
11425 "`validators = \"…\"` must list at least one name",
11426 ));
11427 }
11428 return Ok(());
11429 }
11430 if meta.path.is_ident("auto_uuid") {
11431 parse_flag(&meta, "auto_uuid")?;
11432 out.auto_uuid = true;
11433 // Implied: PK + auto + DEFAULT gen_random_uuid().
11434 // Each is also explicitly settable; the explicit
11435 // value wins if conflicting.
11436 out.primary_key = true;
11437 if out.default.is_none() {
11438 out.default = Some("gen_random_uuid()".into());
11439 }
11440 return Ok(());
11441 }
11442 if meta.path.is_ident("default_uuid_v7") {
11443 parse_flag(&meta, "default_uuid_v7")?;
11444 // Backend-neutral counterpart of `auto_uuid` — issue #823.
11445 // No SQL DEFAULT (the macro fills the value Rust-side
11446 // before binding); just mark the field as PK + Auto
11447 // so the insert path is the `Auto::Unset → generate`
11448 // branch.
11449 out.default_uuid_v7 = true;
11450 out.primary_key = true;
11451 return Ok(());
11452 }
11453 if meta.path.is_ident("auto_now_add") {
11454 parse_flag(&meta, "auto_now_add")?;
11455 out.auto_now_add = true;
11456 if out.default.is_none() {
11457 out.default = Some("now()".into());
11458 }
11459 return Ok(());
11460 }
11461 if meta.path.is_ident("auto_now") {
11462 parse_flag(&meta, "auto_now")?;
11463 out.auto_now = true;
11464 if out.default.is_none() {
11465 out.default = Some("now()".into());
11466 }
11467 return Ok(());
11468 }
11469 if meta.path.is_ident("soft_delete") {
11470 parse_flag(&meta, "soft_delete")?;
11471 out.soft_delete = true;
11472 return Ok(());
11473 }
11474 if meta.path.is_ident("unique") {
11475 parse_flag(&meta, "unique")?;
11476 out.unique = true;
11477 return Ok(());
11478 }
11479 if meta.path.is_ident("index") {
11480 parse_flag(&meta, "index")?;
11481 out.index = true;
11482 // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
11483 if meta.input.peek(syn::token::Paren) {
11484 meta.parse_nested_meta(|inner| {
11485 if inner.path.is_ident("unique") {
11486 out.index_unique = true;
11487 return Ok(());
11488 }
11489 if inner.path.is_ident("name") {
11490 let s: LitStr = inner.value()?.parse()?;
11491 out.index_name = Some(s.value());
11492 return Ok(());
11493 }
11494 if inner.path.is_ident("method") {
11495 let s: LitStr = inner.value()?.parse()?;
11496 let v = s.value();
11497 match v.as_str() {
11498 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
11499 out.index_method = v;
11500 }
11501 other => {
11502 return Err(inner.error(format!(
11503 "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
11504 )));
11505 }
11506 }
11507 return Ok(());
11508 }
11509 Err(inner.error(
11510 "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
11511 ))
11512 })?;
11513 }
11514 return Ok(());
11515 }
11516 Err(unknown_key_error(&meta, "field", FIELD_ATTR_KEYS))
11517 })?;
11518 }
11519 Ok(out)
11520}
11521
11522/// Parse a signed integer literal, accepting optional leading `-`.
11523fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
11524 let expr: syn::Expr = meta.value()?.parse()?;
11525 match expr {
11526 syn::Expr::Lit(syn::ExprLit {
11527 lit: syn::Lit::Int(lit),
11528 ..
11529 }) => lit.base10_parse::<i64>(),
11530 syn::Expr::Unary(syn::ExprUnary {
11531 op: syn::UnOp::Neg(_),
11532 expr,
11533 ..
11534 }) => {
11535 if let syn::Expr::Lit(syn::ExprLit {
11536 lit: syn::Lit::Int(lit),
11537 ..
11538 }) = *expr
11539 {
11540 let v: i64 = lit.base10_parse()?;
11541 Ok(-v)
11542 } else {
11543 Err(syn::Error::new_spanned(expr, "expected integer literal"))
11544 }
11545 }
11546 other => Err(syn::Error::new_spanned(
11547 other,
11548 "expected integer literal (signed)",
11549 )),
11550 }
11551}
11552
11553struct FieldInfo<'a> {
11554 ident: &'a syn::Ident,
11555 column: String,
11556 primary_key: bool,
11557 /// `true` when the Rust type was `Auto<T>` — the INSERT path will
11558 /// skip this column when `Auto::Unset` and emit it under
11559 /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
11560 auto: bool,
11561 /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
11562 /// the `Column::Value` associated type for typed-column tokens.
11563 value_ty: &'a Type,
11564 /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
11565 field_type_tokens: TokenStream2,
11566 schema: TokenStream2,
11567 from_row_init: TokenStream2,
11568 /// Variant of [`Self::from_row_init`] that reads the column via
11569 /// `format!("{prefix}__{col}")` so a model can be decoded out of
11570 /// the aliased columns of a JOINed row. Drives slice 9.0d's
11571 /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
11572 /// helper that `select_related` calls when stitching loaded FKs.
11573 from_aliased_row_init: TokenStream2,
11574 /// MySQL twins of the two inits above. They differ only for a UUID
11575 /// column, which MySQL keeps as `CHAR(36)` text (#1733).
11576 from_row_init_my: TokenStream2,
11577 from_aliased_row_init_my: TokenStream2,
11578 /// A bare or optional `Uuid` field.
11579 uuid_column: bool,
11580 /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
11581 /// relation helper emit (`Author::<child>_set`) needs to know `T`
11582 /// to point the generated method at the right child model.
11583 fk_inner: Option<Type>,
11584 /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
11585 /// `kind` (since ForeignKey detection sets `kind` to K's
11586 /// underlying type) but stored separately for clarity at the
11587 /// `FkRelation` construction site, which only sees the FK's
11588 /// surface fields.
11589 fk_pk_kind: DetectedKind,
11590 /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
11591 /// the bare `ForeignKey<T, K>`. Routes the load_related and
11592 /// fk_pk_access emitters to wrap assignments / accessors in
11593 /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
11594 /// FK column compiles end-to-end. The DDL writer reads this off
11595 /// the field schema (`nullable` flag); the macro just needs to
11596 /// keep the Rust-side codegen consistent.
11597 nullable: bool,
11598 /// `true` when this column was marked `#[rustango(auto_now)]` —
11599 /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
11600 /// column instead of the user-supplied value, so `updated_at`
11601 /// always reflects the latest write without the caller having
11602 /// to remember to set it.
11603 auto_now: bool,
11604 /// `true` when this column was marked `#[rustango(auto_now_add)]`
11605 /// — the column is server-set on INSERT (DB DEFAULT) and
11606 /// **immutable** afterwards. `update_on` / `save_on` skip the
11607 /// column entirely so a stale `created_at` value in memory never
11608 /// rewrites the persisted timestamp.
11609 auto_now_add: bool,
11610 /// `true` when this column was marked `#[rustango(soft_delete)]`.
11611 /// Triggers emission of `soft_delete_on(executor)` and
11612 /// `restore_on(executor)` on the model's inherent impl. There is
11613 /// at most one such column per model — emission asserts this.
11614 soft_delete: bool,
11615 /// `Some` when this column was marked
11616 /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
11617 /// every INSERT and UPDATE path; the database recomputes the
11618 /// value from `EXPR`. Backlog item #35.
11619 generated_as: Option<String>,
11620 /// `true` when this column was marked
11621 /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
11622 /// emit an `insert_push` that auto-fills an `Auto::Unset` value
11623 /// with `Uuid::now_v7()` before binding, so the PK is generated
11624 /// Rust-side and the column is always present in the INSERT
11625 /// statement (no DB DEFAULT requirement). Issue #823.
11626 default_uuid_v7: bool,
11627 /// `Some` when this FK field carried `#[rustango(related_name =
11628 /// "...")]`. Threaded into [`FkRelation::related_name`] and
11629 /// consumed by [`reverse_helper_tokens`] to override the default
11630 /// `<child_snake>_set` accessor name. Follow-up to #816.
11631 related_name: Option<String>,
11632}
11633
11634/// Reject table names that won't survive SQL identifier
11635/// derivation downstream. Postgres' regular-identifier rule
11636/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
11637/// through the framework's unquoted FK / index / constraint name
11638/// emission without surprises. We also disallow leading-digit and
11639/// the empty string for clarity.
11640///
11641/// Reserved-word collisions (`select`, `from`, …) aren't flagged
11642/// here — those produce a runtime error from the SQL parser,
11643/// which is loud enough; statically enumerating reserved words
11644/// across the three supported dialects is more friction than help.
11645///
11646/// Backlog item #65.
11647fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11648 validate_sql_identifier(name, "table", span)
11649}
11650
11651/// Reject SQL identifiers that compile but break downstream SQL
11652/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11653/// `kind` is "table" / "column" — used for the error message so users
11654/// see which attribute caused the failure.
11655fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11656 if name.is_empty() {
11657 return Err(syn::Error::new(
11658 span,
11659 format!("`{kind} = \"\"` is not a valid SQL identifier"),
11660 ));
11661 }
11662 let mut chars = name.chars();
11663 let first = chars.next().unwrap();
11664 if !(first.is_ascii_alphabetic() || first == '_') {
11665 return Err(syn::Error::new(
11666 span,
11667 format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11668 ));
11669 }
11670 for c in chars {
11671 if !(c.is_ascii_alphanumeric() || c == '_') {
11672 return Err(syn::Error::new(
11673 span,
11674 format!(
11675 "{kind} name `{name}` contains invalid character {c:?} — \
11676 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11677 Hyphens in particular break FK / index name derivation \
11678 downstream; use underscores instead (e.g. `{}`)",
11679 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11680 ),
11681 ));
11682 }
11683 }
11684 Ok(())
11685}
11686
11687fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11688 let root = rustango_root();
11689 let attrs = parse_field_attrs(field)?;
11690 let ident = field
11691 .ident
11692 .as_ref()
11693 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11694 let name = ident_name(ident);
11695 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11696 let primary_key = attrs.primary_key;
11697 let DetectedType {
11698 kind,
11699 nullable,
11700 auto: detected_auto,
11701 fk_inner,
11702 } = detect_type(&field.ty)?;
11703 check_bound_compatibility(field, &attrs, kind)?;
11704 let auto = detected_auto;
11705 // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11706 // INSERT path: the user must wrap the field in `Auto<T>`, which
11707 // marks the column as DB-default-supplied. The mixin attrs then
11708 // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11709 // for `auto_now`, force the value on UPDATE too.
11710 if attrs.auto_uuid {
11711 if kind != DetectedKind::Uuid {
11712 return Err(syn::Error::new_spanned(
11713 field,
11714 "`#[rustango(auto_uuid)]` requires the field type to be \
11715 `Auto<uuid::Uuid>`",
11716 ));
11717 }
11718 if !detected_auto {
11719 return Err(syn::Error::new_spanned(
11720 field,
11721 "`#[rustango(auto_uuid)]` requires the field type to be \
11722 wrapped in `Auto<...>` so the macro skips the column on \
11723 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11724 ));
11725 }
11726 }
11727 if attrs.default_uuid_v7 {
11728 if kind != DetectedKind::Uuid {
11729 return Err(syn::Error::new_spanned(
11730 field,
11731 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11732 `Auto<uuid::Uuid>`",
11733 ));
11734 }
11735 if !detected_auto {
11736 return Err(syn::Error::new_spanned(
11737 field,
11738 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11739 wrapped in `Auto<...>` so the macro can detect the \
11740 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11741 ));
11742 }
11743 if attrs.auto_uuid {
11744 return Err(syn::Error::new_spanned(
11745 field,
11746 "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11747 `#[rustango(auto_uuid)]` — the former generates the UUID \
11748 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11749 Pick one.",
11750 ));
11751 }
11752 }
11753 if attrs.auto_now_add || attrs.auto_now {
11754 if kind != DetectedKind::DateTime {
11755 return Err(syn::Error::new_spanned(
11756 field,
11757 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11758 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11759 ));
11760 }
11761 if !detected_auto {
11762 return Err(syn::Error::new_spanned(
11763 field,
11764 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11765 the field type to be wrapped in `Auto<...>` so the macro skips \
11766 the column on INSERT and the DB DEFAULT (`now()`) fires",
11767 ));
11768 }
11769 }
11770 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11771 return Err(syn::Error::new_spanned(
11772 field,
11773 "`#[rustango(soft_delete)]` requires the field type to be \
11774 `Option<chrono::DateTime<chrono::Utc>>`",
11775 ));
11776 }
11777 let is_mixin_auto =
11778 attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11779 if detected_auto && !primary_key && !is_mixin_auto {
11780 return Err(syn::Error::new_spanned(
11781 field,
11782 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11783 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11784 `auto_now`",
11785 ));
11786 }
11787 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11788 return Err(syn::Error::new_spanned(
11789 field,
11790 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11791 SERIAL / BIGSERIAL already supplies a default sequence.",
11792 ));
11793 }
11794 if fk_inner.is_some() && primary_key {
11795 return Err(syn::Error::new_spanned(
11796 field,
11797 "`ForeignKey<T>` is not allowed on a primary-key field — \
11798 a row's PK is its own identity, not a reference to a parent.",
11799 ));
11800 }
11801 if attrs.generated_as.is_some() {
11802 if primary_key {
11803 return Err(syn::Error::new_spanned(
11804 field,
11805 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11806 primary-key field — a PK must be writable so the row \
11807 has an identity at INSERT time.",
11808 ));
11809 }
11810 if attrs.default.is_some() {
11811 return Err(syn::Error::new_spanned(
11812 field,
11813 "`#[rustango(generated_as = \"…\")]` cannot combine with \
11814 `default = \"…\"` — Postgres rejects DEFAULT on \
11815 generated columns. The expression IS the default.",
11816 ));
11817 }
11818 if detected_auto {
11819 return Err(syn::Error::new_spanned(
11820 field,
11821 "`#[rustango(generated_as = \"…\")]` is not allowed on \
11822 an `Auto<T>` field — generated columns are computed \
11823 by the DB, not server-assigned via a sequence. Use a \
11824 plain Rust type (e.g. `f64`).",
11825 ));
11826 }
11827 if fk_inner.is_some() {
11828 return Err(syn::Error::new_spanned(
11829 field,
11830 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11831 ForeignKey field.",
11832 ));
11833 }
11834 }
11835 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11836 let column_lit = column.as_str();
11837 // pgvector (#824): the `vector(dims = N)` attribute supplies the
11838 // dimension that the bare `Vector` Rust type can't carry, so emit
11839 // `FieldType::Vector(N)` here rather than the `variant_tokens`
11840 // fallback of `Vector(0)`.
11841 let field_type_tokens = if kind == DetectedKind::Vector {
11842 let root = rustango_root();
11843 let dims = attrs.vector_dims.unwrap_or(0);
11844 quote!(#root::core::FieldType::Vector(#dims))
11845 } else if kind == DetectedKind::Geometry {
11846 // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11847 // SRID that the bare `Point` Rust type can't carry, so emit
11848 // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11849 let root = rustango_root();
11850 let srid = attrs.geometry_srid.unwrap_or(0);
11851 quote!(#root::core::FieldType::Geometry(#srid))
11852 } else {
11853 kind.variant_tokens()
11854 };
11855 let max_length = optional_u32(attrs.max_length);
11856 let min = optional_i64(attrs.min);
11857 let max = optional_i64(attrs.max);
11858 let default = optional_str(attrs.default.as_deref());
11859
11860 let unique = attrs.unique;
11861 let generated_as = optional_str(attrs.generated_as.as_deref());
11862 let help_text = optional_str(attrs.help_text.as_deref());
11863 let choices = optional_choices(attrs.choices.as_deref());
11864 let db_comment = optional_str(attrs.db_comment.as_deref());
11865 let verbose_name = optional_str(attrs.verbose_name.as_deref());
11866 let editable = attrs.editable;
11867 let blank = attrs.blank;
11868 let case_insensitive = attrs.case_insensitive;
11869 let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11870 if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() && fk_inner.is_none()
11871 {
11872 return Err(syn::Error::new_spanned(
11873 field,
11874 "`#[rustango(on_delete = \"…\")]` requires a `ForeignKey<T>` field \
11875 or `fk = \"<table>\"` / `o2o = \"<table>\"` on the same field — it \
11876 has no meaning on a non-FK column.",
11877 ));
11878 }
11879 if attrs.on_delete.as_deref() == Some("set_null") && !nullable {
11880 return Err(syn::Error::new_spanned(
11881 field,
11882 "`on_delete = \"set_null\"` needs a nullable column: use \
11883 `Option<ForeignKey<T>>` or `Option<…>` (spelled out; a type \
11884 alias for `Option` is not seen).",
11885 ));
11886 }
11887 let fk_on_delete = match attrs.on_delete.as_deref() {
11888 None => quote!(::core::option::Option::None),
11889 Some(action) => {
11890 let variant = match action {
11891 "cascade" => quote!(Cascade),
11892 "restrict" => quote!(Restrict),
11893 "set_null" => quote!(SetNull),
11894 "set_default" => quote!(SetDefault),
11895 "no_action" => quote!(NoAction),
11896 // parse_field_attrs already validated this — guard against future drift.
11897 other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11898 };
11899 quote!(::core::option::Option::Some(
11900 #root::core::OnDeleteAction::#variant
11901 ))
11902 }
11903 };
11904 let auto_now_flag = attrs.auto_now;
11905 let cast = match cast_type(&field.ty) {
11906 Some(ty) => quote!(::core::option::Option::Some(<#ty>::FIELD_CAST)),
11907 None => quote!(::core::option::Option::None),
11908 };
11909 let schema = quote! {{
11910 #root::core::FieldSchema::from_parts(#root::core::FieldSchemaParts {
11911 name: #name,
11912 column: #column_lit,
11913 ty: #field_type_tokens,
11914 nullable: #nullable,
11915 primary_key: #primary_key,
11916 relation: #relation,
11917 max_length: #max_length,
11918 min: #min,
11919 max: #max,
11920 default: #default,
11921 auto: #auto,
11922 auto_now: #auto_now_flag,
11923 unique: #unique,
11924 generated_as: #generated_as,
11925 help_text: #help_text,
11926 choices: #choices,
11927 db_comment: #db_comment,
11928 verbose_name: #verbose_name,
11929 editable: #editable,
11930 blank: #blank,
11931 case_insensitive: #case_insensitive,
11932 fk_on_delete: #fk_on_delete,
11933 validators: &[ #(#validators_lits),* ],
11934 cast: #cast,
11935 })
11936 }};
11937
11938 // sqlx-sqlite has no `Decimal` decode: it goes through `FlatScalar`'s cell.
11939 let decimal_column = kind == DetectedKind::Decimal && !detected_auto && fk_inner.is_none();
11940 let (from_row_init, from_aliased_row_init) = if decimal_column {
11941 (
11942 quote! { #ident: #root::sql::try_get_flat(row, #column_lit)? },
11943 quote! {
11944 #ident: #root::sql::try_get_flat(
11945 row,
11946 #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11947 )?
11948 },
11949 )
11950 } else {
11951 (
11952 quote! { #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)? },
11953 quote! {
11954 #ident: #root::sql::sqlx::Row::try_get(
11955 row,
11956 #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11957 )?
11958 },
11959 )
11960 };
11961 // `Auto<Uuid>` and `ForeignKey<_, Uuid>` decode through their own
11962 // MySQL impls; a bare or optional `Uuid` needs `FlatScalar`'s cell.
11963 let uuid_column = kind == DetectedKind::Uuid && !detected_auto && fk_inner.is_none();
11964 let (from_row_init_my, from_aliased_row_init_my) = if uuid_column {
11965 (
11966 quote! { #ident: #root::sql::try_get_flat_my(row, #column_lit)? },
11967 quote! {
11968 #ident: #root::sql::try_get_flat_my(
11969 row,
11970 #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11971 )?
11972 },
11973 )
11974 } else {
11975 (from_row_init.clone(), from_aliased_row_init.clone())
11976 };
11977
11978 Ok(FieldInfo {
11979 ident,
11980 column,
11981 primary_key,
11982 auto,
11983 value_ty: &field.ty,
11984 field_type_tokens,
11985 schema,
11986 from_row_init,
11987 from_aliased_row_init,
11988 from_row_init_my,
11989 from_aliased_row_init_my,
11990 uuid_column,
11991 fk_inner: fk_inner.cloned(),
11992 fk_pk_kind: kind,
11993 nullable,
11994 auto_now: attrs.auto_now,
11995 auto_now_add: attrs.auto_now_add,
11996 soft_delete: attrs.soft_delete,
11997 generated_as: attrs.generated_as.clone(),
11998 default_uuid_v7: attrs.default_uuid_v7,
11999 related_name: attrs.related_name.clone(),
12000 })
12001}
12002
12003fn check_bound_compatibility(
12004 field: &syn::Field,
12005 attrs: &FieldAttrs,
12006 kind: DetectedKind,
12007) -> syn::Result<()> {
12008 if attrs.max_length.is_some() && kind != DetectedKind::String {
12009 return Err(syn::Error::new_spanned(
12010 field,
12011 "`max_length` is only valid on `String` fields (or `Option<String>`)",
12012 ));
12013 }
12014 // These were silently dropped on the wrong type (#1937).
12015 if attrs.case_insensitive && kind != DetectedKind::String {
12016 return Err(syn::Error::new_spanned(
12017 field,
12018 "`citext` is only valid on `String` fields (or `Option<String>`)",
12019 ));
12020 }
12021 if attrs.vector_dims.is_some() && kind != DetectedKind::Vector {
12022 return Err(syn::Error::new_spanned(
12023 field,
12024 "`vector(dims = …)` is only valid on a pgvector `Vector` field",
12025 ));
12026 }
12027 if attrs.geometry_srid.is_some() && kind != DetectedKind::Geometry {
12028 return Err(syn::Error::new_spanned(
12029 field,
12030 "`geometry(srid = …)` is only valid on a PostGIS `Point` field",
12031 ));
12032 }
12033 if attrs.choices.is_some() && kind != DetectedKind::String {
12034 return Err(syn::Error::new_spanned(
12035 field,
12036 "`choices` is only valid on `String` fields (or `Option<String>`) — \
12037 integer-valued enumerations should be modeled with a Rust enum and \
12038 custom (de)serializer for now",
12039 ));
12040 }
12041 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
12042 return Err(syn::Error::new_spanned(
12043 field,
12044 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
12045 ));
12046 }
12047 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
12048 if min > max {
12049 return Err(syn::Error::new_spanned(
12050 field,
12051 format!("`min` ({min}) is greater than `max` ({max})"),
12052 ));
12053 }
12054 }
12055 if let Some(default) = &attrs.default {
12056 check_default_expr(default, kind).map_err(|msg| syn::Error::new_spanned(field, msg))?;
12057 }
12058 Ok(())
12059}
12060
12061/// SQL words a `default` may be on its own; any other bare word is a
12062/// column reference, which Postgres and MySQL refuse in a DEFAULT.
12063const DEFAULT_KEYWORDS: &[&str] = &[
12064 "NULL",
12065 "TRUE",
12066 "FALSE",
12067 "CURRENT_TIMESTAMP",
12068 "CURRENT_DATE",
12069 "CURRENT_TIME",
12070 "LOCALTIMESTAMP",
12071 "LOCALTIME",
12072];
12073
12074/// `default` is raw SQL. Refuse the shapes only SQLite accepts: a bare
12075/// word (read as a column on PG/MySQL) and a non-numeric literal on a
12076/// numeric field.
12077fn check_default_expr(default: &str, kind: DetectedKind) -> Result<(), String> {
12078 let expr = default.trim();
12079 let is_word = expr.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
12080 && expr
12081 .chars()
12082 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == ' ');
12083 let upper = expr.to_ascii_uppercase();
12084 if is_word && ["CURRENT_USER", "SESSION_USER", "USER"].contains(&upper.as_str()) {
12085 return Err(format!(
12086 "`default = \"{expr}\"` works on Postgres only: MySQL refuses it as a default \
12087 and SQLite stores the text `{expr}`; set the value in code instead"
12088 ));
12089 }
12090 if is_word && !DEFAULT_KEYWORDS.contains(&upper.as_str()) {
12091 return Err(format!(
12092 "`default = \"{expr}\"` is raw SQL, so `{expr}` is read as a column name; \
12093 for the text '{expr}' write `default = \"'{expr}'\"`"
12094 ));
12095 }
12096 let numeric = kind.is_integer()
12097 || matches!(
12098 kind,
12099 DetectedKind::F32 | DetectedKind::F64 | DetectedKind::Decimal
12100 );
12101 if numeric {
12102 let unquoted = expr
12103 .strip_prefix('\'')
12104 .and_then(|e| e.strip_suffix('\''))
12105 .unwrap_or(expr);
12106 let is_number = if kind.is_integer() {
12107 unquoted.parse::<i64>().is_ok()
12108 } else {
12109 unquoted.parse::<f64>().is_ok_and(f64::is_finite)
12110 };
12111 // Keep expressions (`(1 + 2)`, `abs(-1)`) and NULL; refuse a
12112 // literal that is no number. `""` renders as `''`.
12113 let is_literal = is_word || expr.is_empty() || expr.starts_with('\'');
12114 if is_literal && !is_number && upper != "NULL" {
12115 return Err(format!(
12116 "`default = \"{expr}\"` is not a number, but the field is numeric"
12117 ));
12118 }
12119 }
12120 Ok(())
12121}
12122
12123/// A `&'static [ty]` of `const fn` results. The slice needs its own
12124/// `const`: a `&[..]` assigned in a statement is not extended to `'static`.
12125fn const_slice(ty: &TokenStream2, items: impl Iterator<Item = TokenStream2>) -> TokenStream2 {
12126 quote!({
12127 const ITEMS: &[#ty] = &[ #(#items),* ];
12128 ITEMS
12129 })
12130}
12131
12132fn optional_u32(value: Option<u32>) -> TokenStream2 {
12133 if let Some(v) = value {
12134 quote!(::core::option::Option::Some(#v))
12135 } else {
12136 quote!(::core::option::Option::None)
12137 }
12138}
12139
12140fn optional_i64(value: Option<i64>) -> TokenStream2 {
12141 if let Some(v) = value {
12142 quote!(::core::option::Option::Some(#v))
12143 } else {
12144 quote!(::core::option::Option::None)
12145 }
12146}
12147
12148fn optional_str(value: Option<&str>) -> TokenStream2 {
12149 if let Some(v) = value {
12150 quote!(::core::option::Option::Some(#v))
12151 } else {
12152 quote!(::core::option::Option::None)
12153 }
12154}
12155
12156fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
12157 let Some(pairs) = pairs else {
12158 return quote!(::core::option::Option::None);
12159 };
12160 let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
12161 quote!(::core::option::Option::Some(&[#(#entries),*]))
12162}
12163
12164fn relation_tokens(
12165 field: &syn::Field,
12166 attrs: &FieldAttrs,
12167 fk_inner: Option<&syn::Type>,
12168 table: &str,
12169) -> syn::Result<TokenStream2> {
12170 let root = rustango_root();
12171 if let Some(inner) = fk_inner {
12172 if attrs.fk.is_some() || attrs.o2o.is_some() {
12173 return Err(syn::Error::new_spanned(
12174 field,
12175 "`ForeignKey<T>` already declares the FK target via the type parameter — \
12176 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
12177 ));
12178 }
12179 // No `on`: the target's PK column, which may be renamed.
12180 let on = match attrs.on.as_deref() {
12181 Some(on) => quote!(#on),
12182 None => quote! {
12183 match <#inner as #root::core::Model>::SCHEMA.pk_column() {
12184 ::core::option::Option::Some(c) => c,
12185 ::core::option::Option::None => "id",
12186 }
12187 },
12188 };
12189 return Ok(quote! {
12190 ::core::option::Option::Some(#root::core::Relation::fk(
12191 <#inner as #root::core::Model>::SCHEMA.table,
12192 #on,
12193 ))
12194 });
12195 }
12196 match (&attrs.fk, &attrs.o2o) {
12197 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
12198 field,
12199 "`fk` and `o2o` are mutually exclusive",
12200 )),
12201 (Some(to), None) => {
12202 let on = attrs.on.as_deref().unwrap_or("id");
12203 // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
12204 // the model's own table. Threaded as a literal string at
12205 // macro-expansion time to sidestep the const-eval cycle
12206 // that `Self::SCHEMA.table` would create when referenced
12207 // inside Self::SCHEMA's own initializer.
12208 let resolved = if to == "self" { table } else { to };
12209 Ok(quote! {
12210 ::core::option::Option::Some(#root::core::Relation::fk(#resolved, #on))
12211 })
12212 }
12213 (None, Some(to)) => {
12214 let on = attrs.on.as_deref().unwrap_or("id");
12215 let resolved = if to == "self" { table } else { to };
12216 Ok(quote! {
12217 ::core::option::Option::Some(#root::core::Relation::o2o(#resolved, #on))
12218 })
12219 }
12220 (None, None) => {
12221 if attrs.on.is_some() {
12222 return Err(syn::Error::new_spanned(
12223 field,
12224 "`on` requires `fk` or `o2o`",
12225 ));
12226 }
12227 Ok(quote!(::core::option::Option::None))
12228 }
12229 }
12230}
12231
12232/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
12233/// about kinds without depending on `rustango-core` (which would require a
12234/// proc-macro/normal split it doesn't have today).
12235#[derive(Clone, Copy, PartialEq, Eq)]
12236enum DetectedKind {
12237 I16,
12238 I32,
12239 I64,
12240 F32,
12241 F64,
12242 Bool,
12243 String,
12244 DateTime,
12245 Date,
12246 Time,
12247 Uuid,
12248 Json,
12249 Decimal,
12250 Binary,
12251 /// `Array<String>` → PG `text[]` (#341).
12252 ArrayText,
12253 /// `Array<i32>` → PG `integer[]` (#341).
12254 ArrayInt,
12255 /// `Array<i64>` → PG `bigint[]` (#341).
12256 ArrayBigInt,
12257 /// `Range<i32>` → PG `int4range` (#343).
12258 RangeInt,
12259 /// `Range<i64>` → PG `int8range` (#343).
12260 RangeBigInt,
12261 /// `Range<Decimal>` → PG `numrange` (#343).
12262 RangeNumeric,
12263 /// `Range<NaiveDate>` → PG `daterange` (#343).
12264 RangeDate,
12265 /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
12266 RangeDateTime,
12267 /// `HStore` → PG `hstore` (#342).
12268 HStore,
12269 /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
12270 /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
12271 /// in at the `FieldType` emission site (not carried on this enum).
12272 Vector,
12273 /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
12274 /// from the `#[rustango(geometry(srid = N))]` field attribute,
12275 /// threaded in at the `FieldType` emission site.
12276 Geometry,
12277}
12278
12279impl DetectedKind {
12280 fn variant_tokens(self) -> TokenStream2 {
12281 let root = rustango_root();
12282 match self {
12283 Self::I16 => quote!(#root::core::FieldType::I16),
12284 Self::I32 => quote!(#root::core::FieldType::I32),
12285 Self::I64 => quote!(#root::core::FieldType::I64),
12286 Self::F32 => quote!(#root::core::FieldType::F32),
12287 Self::F64 => quote!(#root::core::FieldType::F64),
12288 Self::Bool => quote!(#root::core::FieldType::Bool),
12289 Self::String => quote!(#root::core::FieldType::String),
12290 Self::DateTime => quote!(#root::core::FieldType::DateTime),
12291 Self::Date => quote!(#root::core::FieldType::Date),
12292 Self::Time => quote!(#root::core::FieldType::Time),
12293 Self::Uuid => quote!(#root::core::FieldType::Uuid),
12294 Self::Json => quote!(#root::core::FieldType::Json),
12295 Self::Decimal => quote!(#root::core::FieldType::Decimal),
12296 Self::Binary => quote!(#root::core::FieldType::Binary),
12297 Self::ArrayText => {
12298 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
12299 }
12300 Self::ArrayInt => {
12301 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
12302 }
12303 Self::ArrayBigInt => {
12304 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
12305 }
12306 Self::RangeInt => {
12307 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
12308 }
12309 Self::RangeBigInt => {
12310 quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
12311 }
12312 Self::RangeNumeric => {
12313 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
12314 }
12315 Self::RangeDate => {
12316 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
12317 }
12318 Self::RangeDateTime => {
12319 quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
12320 }
12321 Self::HStore => quote!(#root::core::FieldType::HStore),
12322 // Dimension comes from the `vector(dims = N)` attribute,
12323 // applied at the emission site; `0` here is just a fallback.
12324 Self::Vector => quote!(#root::core::FieldType::Vector(0)),
12325 // SRID comes from the `geometry(srid = N)` attribute, applied
12326 // at the emission site; `0` here is just a fallback.
12327 Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
12328 }
12329 }
12330
12331 fn is_integer(self) -> bool {
12332 matches!(self, Self::I16 | Self::I32 | Self::I64)
12333 }
12334}
12335
12336/// Result of walking a field's Rust type. `kind` is the underlying
12337/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
12338/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
12339/// `Some(<T>)` when the field was `ForeignKey<T>` (or
12340/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
12341#[derive(Clone, Copy)]
12342struct DetectedType<'a> {
12343 kind: DetectedKind,
12344 nullable: bool,
12345 auto: bool,
12346 fk_inner: Option<&'a syn::Type>,
12347}
12348
12349/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
12350/// non-`Auto` types — the caller should already have routed Auto-only
12351/// codegen through this helper, so a `None` indicates a macro-internal
12352/// invariant break.
12353fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
12354 let Type::Path(TypePath { path, qself: None }) = ty else {
12355 return None;
12356 };
12357 let last = path.segments.last()?;
12358 if last.ident != "Auto" {
12359 return None;
12360 }
12361 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
12362 return None;
12363 };
12364 args.args.iter().find_map(|a| match a {
12365 syn::GenericArgument::Type(t) => Some(t),
12366 _ => None,
12367 })
12368}
12369
12370/// The `Cast<C>` inside `ty` (bare or in an `Option`), for its schema record.
12371fn cast_type(ty: &syn::Type) -> Option<&syn::Type> {
12372 let Type::Path(TypePath { path, qself: None }) = ty else {
12373 return None;
12374 };
12375 let last = path.segments.last()?;
12376 if last.ident == "Cast" {
12377 return Some(ty);
12378 }
12379 if last.ident != "Option" {
12380 return None;
12381 }
12382 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
12383 return None;
12384 };
12385 match args.args.first()? {
12386 syn::GenericArgument::Type(inner) => cast_type(inner).filter(|_| args.args.len() == 1),
12387 _ => None,
12388 }
12389}
12390
12391fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
12392 let Type::Path(TypePath { path, qself: None }) = ty else {
12393 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
12394 };
12395 let last = path
12396 .segments
12397 .last()
12398 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
12399
12400 if last.ident == "Option" {
12401 let inner = generic_inner(ty, &last.arguments, "Option")?;
12402 let inner_det = detect_type(inner)?;
12403 if inner_det.nullable {
12404 return Err(syn::Error::new_spanned(
12405 ty,
12406 "nested Option is not supported",
12407 ));
12408 }
12409 if inner_det.auto {
12410 return Err(syn::Error::new_spanned(
12411 ty,
12412 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
12413 ));
12414 }
12415 return Ok(DetectedType {
12416 nullable: true,
12417 ..inner_det
12418 });
12419 }
12420
12421 if last.ident == "Auto" {
12422 let inner = generic_inner(ty, &last.arguments, "Auto")?;
12423 let inner_det = detect_type(inner)?;
12424 if inner_det.auto {
12425 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
12426 }
12427 if inner_det.nullable {
12428 return Err(syn::Error::new_spanned(
12429 ty,
12430 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
12431 ));
12432 }
12433 if inner_det.fk_inner.is_some() {
12434 return Err(syn::Error::new_spanned(
12435 ty,
12436 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
12437 ));
12438 }
12439 if !matches!(
12440 inner_det.kind,
12441 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
12442 ) {
12443 return Err(syn::Error::new_spanned(
12444 ty,
12445 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
12446 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
12447 (DEFAULT now())",
12448 ));
12449 }
12450 return Ok(DetectedType {
12451 auto: true,
12452 ..inner_det
12453 });
12454 }
12455
12456 if last.ident == "ForeignKey" {
12457 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
12458 // Resolve the FK column's underlying SQL type from `K`. When the
12459 // user wrote `ForeignKey<T>` without a key parameter, the type
12460 // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
12461 // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
12462 // recurse into K so the column DDL emits the right SQL type
12463 // (VARCHAR for String, UUID for Uuid, …) and the load_related
12464 // emitter knows which `SqlValue` variant to match.
12465 let kind = match key_ty {
12466 Some(k) => detect_type(k)?.kind,
12467 None => DetectedKind::I64,
12468 };
12469 return Ok(DetectedType {
12470 kind,
12471 nullable: false,
12472 auto: false,
12473 fk_inner: Some(inner),
12474 });
12475 }
12476
12477 let kind = match last.ident.to_string().as_str() {
12478 "i16" => DetectedKind::I16,
12479 "i32" => DetectedKind::I32,
12480 "i64" => DetectedKind::I64,
12481 "f32" => DetectedKind::F32,
12482 "f64" => DetectedKind::F64,
12483 "bool" => DetectedKind::Bool,
12484 "String" => DetectedKind::String,
12485 "DateTime" => DetectedKind::DateTime,
12486 "NaiveDate" => DetectedKind::Date,
12487 "NaiveTime" => DetectedKind::Time,
12488 "Uuid" => DetectedKind::Uuid,
12489 "Value" => DetectedKind::Json,
12490 "Decimal" => DetectedKind::Decimal,
12491 // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
12492 // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
12493 // — that would emit Binary DDL and decode-fail at runtime.
12494 "Vec" => {
12495 let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
12496 if let Type::Path(TypePath { path, qself: None }) = inner {
12497 if let Some(seg) = path.segments.last() {
12498 if seg.ident == "u8" && seg.arguments.is_empty() {
12499 return Ok(DetectedType {
12500 kind: DetectedKind::Binary,
12501 nullable: false,
12502 auto: false,
12503 fk_inner: None,
12504 });
12505 }
12506 }
12507 }
12508 return Err(syn::Error::new_spanned(
12509 ty,
12510 "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
12511 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
12512 ));
12513 }
12514 // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
12515 // `integer[]` / `bigint[]` (#341).
12516 "Array" => {
12517 let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
12518 let elem = match inner {
12519 Type::Path(TypePath { path, qself: None }) => {
12520 path.segments.last().map(|s| s.ident.to_string())
12521 }
12522 _ => None,
12523 };
12524 let kind = match elem.as_deref() {
12525 Some("String") => DetectedKind::ArrayText,
12526 Some("i32") => DetectedKind::ArrayInt,
12527 Some("i64") => DetectedKind::ArrayBigInt,
12528 _ => {
12529 return Err(syn::Error::new_spanned(
12530 ty,
12531 "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
12532 `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
12533 ));
12534 }
12535 };
12536 return Ok(DetectedType {
12537 kind,
12538 nullable: false,
12539 auto: false,
12540 fk_inner: None,
12541 });
12542 }
12543 // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
12544 // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
12545 // `int8range` / `numrange` / `daterange` / `tstzrange` (#343).
12546 "Range" => {
12547 let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
12548 let elem = match inner {
12549 Type::Path(TypePath { path, qself: None }) => {
12550 path.segments.last().map(|s| s.ident.to_string())
12551 }
12552 _ => None,
12553 };
12554 let kind = match elem.as_deref() {
12555 Some("i32") => DetectedKind::RangeInt,
12556 Some("i64") => DetectedKind::RangeBigInt,
12557 Some("Decimal") => DetectedKind::RangeNumeric,
12558 Some("NaiveDate") => DetectedKind::RangeDate,
12559 Some("DateTime") => DetectedKind::RangeDateTime,
12560 _ => {
12561 return Err(syn::Error::new_spanned(
12562 ty,
12563 "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
12564 `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
12565 `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
12566 (→ tstzrange) are supported (#343)",
12567 ));
12568 }
12569 };
12570 return Ok(DetectedType {
12571 kind,
12572 nullable: false,
12573 auto: false,
12574 fk_inner: None,
12575 });
12576 }
12577 // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
12578 // (the `CastValue` impl bridges logical↔stored); the field's own
12579 // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
12580 // schema just needs `FieldType::String`.
12581 "Cast" => {
12582 return Ok(DetectedType {
12583 kind: DetectedKind::String,
12584 nullable: false,
12585 auto: false,
12586 fk_inner: None,
12587 });
12588 }
12589 // `HStore` → PG `hstore` (#342). No generic
12590 // parameter — always a string→string map.
12591 "HStore" => {
12592 return Ok(DetectedType {
12593 kind: DetectedKind::HStore,
12594 nullable: false,
12595 auto: false,
12596 fk_inner: None,
12597 });
12598 }
12599 // `Vector` → pgvector `vector(N)` (#824). The dimension is
12600 // supplied by `#[rustango(vector(dims = N))]`, not the type.
12601 "Vector" => {
12602 return Ok(DetectedType {
12603 kind: DetectedKind::Vector,
12604 nullable: false,
12605 auto: false,
12606 fk_inner: None,
12607 });
12608 }
12609 // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
12610 // supplied by `#[rustango(geometry(srid = N))]`, not the type.
12611 "Point" => {
12612 return Ok(DetectedType {
12613 kind: DetectedKind::Geometry,
12614 nullable: false,
12615 auto: false,
12616 fk_inner: None,
12617 });
12618 }
12619 other => {
12620 return Err(syn::Error::new_spanned(
12621 ty,
12622 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)"),
12623 ));
12624 }
12625 };
12626 Ok(DetectedType {
12627 kind,
12628 nullable: false,
12629 auto: false,
12630 fk_inner: None,
12631 })
12632}
12633
12634fn generic_inner<'a>(
12635 ty: &'a Type,
12636 arguments: &'a PathArguments,
12637 wrapper: &str,
12638) -> syn::Result<&'a Type> {
12639 let PathArguments::AngleBracketed(args) = arguments else {
12640 return Err(syn::Error::new_spanned(
12641 ty,
12642 format!("{wrapper} requires a generic argument"),
12643 ));
12644 };
12645 args.args
12646 .iter()
12647 .find_map(|a| match a {
12648 GenericArgument::Type(t) => Some(t),
12649 _ => None,
12650 })
12651 .ok_or_else(|| {
12652 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12653 })
12654}
12655
12656/// Like [`generic_inner`] but pulls *two* type args — the first is
12657/// required, the second is optional. Used by the `ForeignKey<T, K>`
12658/// detection where K defaults to `i64` when omitted.
12659fn generic_pair<'a>(
12660 ty: &'a Type,
12661 arguments: &'a PathArguments,
12662 wrapper: &str,
12663) -> syn::Result<(&'a Type, Option<&'a Type>)> {
12664 let PathArguments::AngleBracketed(args) = arguments else {
12665 return Err(syn::Error::new_spanned(
12666 ty,
12667 format!("{wrapper} requires a generic argument"),
12668 ));
12669 };
12670 let mut types = args.args.iter().filter_map(|a| match a {
12671 GenericArgument::Type(t) => Some(t),
12672 _ => None,
12673 });
12674 let first = types.next().ok_or_else(|| {
12675 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12676 })?;
12677 let second = types.next();
12678 Ok((first, second))
12679}
12680
12681fn to_snake_case(s: &str) -> String {
12682 let mut out = String::with_capacity(s.len() + 4);
12683 for (i, ch) in s.chars().enumerate() {
12684 if ch.is_ascii_uppercase() {
12685 if i > 0 {
12686 out.push('_');
12687 }
12688 out.push(ch.to_ascii_lowercase());
12689 } else {
12690 out.push(ch);
12691 }
12692 }
12693 out
12694}
12695
12696// ============================================================
12697// #[derive(Form)] — slice 8.4B
12698// ============================================================
12699
12700/// Per-field `#[form(...)]` attributes recognised by the derive.
12701#[derive(Default)]
12702struct FormFieldAttrs {
12703 min: Option<i64>,
12704 max: Option<i64>,
12705 min_length: Option<u32>,
12706 max_length: Option<u32>,
12707 /// `#[form(clean = "fn_name")]` — per-field clean hook.
12708 /// The named static method on the form struct is called after the
12709 /// field's typed parse + length/range checks; it gets the parsed
12710 /// value by reference and returns `Result<<FieldType>, String>`.
12711 /// On Ok, the returned value replaces the parsed one; on Err, the
12712 /// message is attached to the field error list. Issue #372.
12713 clean: Option<syn::Ident>,
12714}
12715
12716/// Container-level `#[form(...)]` attributes. Currently only the
12717/// cross-field `validate` hook (issue #373).
12718#[derive(Default)]
12719struct FormContainerAttrs {
12720 /// `#[form(validate = "fn_name")]` — whole-form clean hook.
12721 /// After every per-field parse succeeds, the named method on the
12722 /// form struct is called with `&self` and may return
12723 /// `Result<(), FormErrors>`. Errors merge into the field error
12724 /// list. Issue #373.
12725 validate: Option<syn::Ident>,
12726}
12727
12728/// Detected shape of a form field's Rust type.
12729#[derive(Clone, Copy)]
12730enum FormFieldKind {
12731 String,
12732 I16,
12733 I32,
12734 I64,
12735 F32,
12736 F64,
12737 Bool,
12738}
12739
12740impl FormFieldKind {
12741 fn parse_method(self) -> &'static str {
12742 match self {
12743 Self::I16 => "i16",
12744 Self::I32 => "i32",
12745 Self::I64 => "i64",
12746 Self::F32 => "f32",
12747 Self::F64 => "f64",
12748 // String + Bool don't go through `str::parse`; the codegen
12749 // handles them inline.
12750 Self::String | Self::Bool => "",
12751 }
12752 }
12753}
12754
12755/// A generated `derive(Form)` local; mixed-site so a same-named field can't shadow it.
12756fn form_local(name: &str) -> syn::Ident {
12757 syn::Ident::new(name, proc_macro2::Span::mixed_site())
12758}
12759
12760fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
12761 let root = rustango_root();
12762 let (errs, cleaned, data_param) = (
12763 form_local("__errors"),
12764 form_local("__cleaned"),
12765 form_local("data"),
12766 );
12767 let struct_name = &input.ident;
12768
12769 let Data::Struct(data) = &input.data else {
12770 return Err(syn::Error::new_spanned(
12771 struct_name,
12772 "Form can only be derived on structs",
12773 ));
12774 };
12775 let Fields::Named(named) = &data.fields else {
12776 return Err(syn::Error::new_spanned(
12777 struct_name,
12778 "Form requires a struct with named fields",
12779 ));
12780 };
12781
12782 // #373 — container-level `#[form(validate = "fn")]` hook.
12783 let container = parse_form_container_attrs(input)?;
12784 let post_field_clean: Vec<TokenStream2> = Vec::new();
12785 let _ = post_field_clean;
12786
12787 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12788 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12789
12790 for field in &named.named {
12791 let ident = field
12792 .ident
12793 .as_ref()
12794 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12795 let attrs = parse_form_field_attrs(field)?;
12796 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12797 let is_string = matches!(kind, FormFieldKind::String);
12798 let is_number = !is_string && !matches!(kind, FormFieldKind::Bool);
12799 if !is_string && (attrs.min_length.is_some() || attrs.max_length.is_some()) {
12800 return Err(syn::Error::new(
12801 field.span(),
12802 "`min_length` / `max_length` need a String field",
12803 ));
12804 }
12805 if !is_number && (attrs.min.is_some() || attrs.max.is_some()) {
12806 return Err(syn::Error::new(
12807 field.span(),
12808 "`min` / `max` need a numeric field",
12809 ));
12810 }
12811
12812 let name_lit = ident_name(ident);
12813 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12814 // #372 — append the per-field `clean_<field>` call right after
12815 // the parse block when the attribute is set. The clean fn
12816 // takes &T and returns Result<T, String>; on Err we attach
12817 // the message to the field error list without aborting, so
12818 // the caller sees every field error at once.
12819 let clean_block = if let Some(clean_fn) = &attrs.clean {
12820 quote! {
12821 if #errs.fields().get(#name_lit).is_none() {
12822 match Self::#clean_fn(&#ident) {
12823 ::core::result::Result::Ok(#cleaned) => { #ident = #cleaned; }
12824 ::core::result::Result::Err(__msg) => {
12825 #errs.add(#name_lit, __msg);
12826 }
12827 }
12828 }
12829 }
12830 } else {
12831 quote! {}
12832 };
12833 field_blocks.push(quote! {
12834 #parse_block
12835 #clean_block
12836 });
12837 field_idents.push(ident);
12838 }
12839
12840 // #373 — after every per-field parse + clean succeeds, call the
12841 // cross-field validator if declared. Errors merge into the
12842 // outgoing FormErrors via the existing `FormErrors::merge` helper
12843 // (same primitive the serializer cross-field hook uses).
12844 let cross_field_call = if let Some(validate_fn) = &container.validate {
12845 quote! {
12846 if #errs.is_empty() {
12847 let __candidate = Self { #( #field_idents ),* };
12848 if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12849 #errs.merge(__other);
12850 }
12851 if !#errs.is_empty() {
12852 return ::core::result::Result::Err(#errs);
12853 }
12854 return ::core::result::Result::Ok(__candidate);
12855 }
12856 }
12857 } else {
12858 quote! {}
12859 };
12860
12861 Ok(quote! {
12862 impl #root::forms::Form for #struct_name {
12863 fn parse(
12864 #data_param: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12865 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12866 let mut #errs = #root::forms::FormErrors::default();
12867 #( #field_blocks )*
12868 #cross_field_call
12869 if !#errs.is_empty() {
12870 return ::core::result::Result::Err(#errs);
12871 }
12872 ::core::result::Result::Ok(Self {
12873 #( #field_idents ),*
12874 })
12875 }
12876 }
12877 })
12878}
12879
12880fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12881 let mut out = FormContainerAttrs::default();
12882 for attr in &input.attrs {
12883 if !attr.path().is_ident("form") {
12884 continue;
12885 }
12886 attr.parse_nested_meta(|meta| {
12887 if meta.path.is_ident("validate") {
12888 let s: LitStr = meta.value()?.parse()?;
12889 out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12890 return Ok(());
12891 }
12892 Err(meta.error("unknown form container attribute (supported: `validate`)"))
12893 })?;
12894 }
12895 Ok(out)
12896}
12897
12898fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12899 let mut out = FormFieldAttrs::default();
12900 for attr in &field.attrs {
12901 if !attr.path().is_ident("form") {
12902 continue;
12903 }
12904 attr.parse_nested_meta(|meta| {
12905 if meta.path.is_ident("min") {
12906 let lit: syn::LitInt = meta.value()?.parse()?;
12907 out.min = Some(lit.base10_parse::<i64>()?);
12908 return Ok(());
12909 }
12910 if meta.path.is_ident("max") {
12911 let lit: syn::LitInt = meta.value()?.parse()?;
12912 out.max = Some(lit.base10_parse::<i64>()?);
12913 return Ok(());
12914 }
12915 if meta.path.is_ident("min_length") {
12916 let lit: syn::LitInt = meta.value()?.parse()?;
12917 out.min_length = Some(lit.base10_parse::<u32>()?);
12918 return Ok(());
12919 }
12920 if meta.path.is_ident("max_length") {
12921 let lit: syn::LitInt = meta.value()?.parse()?;
12922 out.max_length = Some(lit.base10_parse::<u32>()?);
12923 return Ok(());
12924 }
12925 if meta.path.is_ident("clean") {
12926 let s: LitStr = meta.value()?.parse()?;
12927 out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12928 return Ok(());
12929 }
12930 Err(meta.error(
12931 "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12932 ))
12933 })?;
12934 }
12935 Ok(out)
12936}
12937
12938fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12939 let Type::Path(TypePath { path, qself: None }) = ty else {
12940 return Err(syn::Error::new(
12941 span,
12942 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12943 ));
12944 };
12945 let last = path
12946 .segments
12947 .last()
12948 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12949
12950 if last.ident == "Option" {
12951 let inner = generic_inner(ty, &last.arguments, "Option")?;
12952 let (kind, nested) = detect_form_field(inner, span)?;
12953 if nested {
12954 return Err(syn::Error::new(
12955 span,
12956 "nested Option in Form fields is not supported",
12957 ));
12958 }
12959 return Ok((kind, true));
12960 }
12961
12962 let kind = match last.ident.to_string().as_str() {
12963 "String" => FormFieldKind::String,
12964 "i16" => FormFieldKind::I16,
12965 "i32" => FormFieldKind::I32,
12966 "i64" => FormFieldKind::I64,
12967 "f32" => FormFieldKind::F32,
12968 "f64" => FormFieldKind::F64,
12969 "bool" => FormFieldKind::Bool,
12970 other => {
12971 return Err(syn::Error::new(
12972 span,
12973 format!(
12974 "Form field type `{other}` is not supported in v0.8 — use String / \
12975 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12976 ),
12977 ));
12978 }
12979 };
12980 Ok((kind, false))
12981}
12982
12983#[allow(clippy::too_many_lines)]
12984fn render_form_field_parse(
12985 ident: &syn::Ident,
12986 name_lit: &str,
12987 kind: FormFieldKind,
12988 nullable: bool,
12989 attrs: &FormFieldAttrs,
12990) -> TokenStream2 {
12991 let (errs, data_param) = (form_local("__errors"), form_local("data"));
12992 // Pull the raw &str from the payload `Form::parse(data: &HashMap<…>)`.
12993 let lookup = quote! {
12994 let __raw: ::core::option::Option<&::std::string::String> = #data_param.get(#name_lit);
12995 };
12996
12997 let parsed_value = match kind {
12998 // `Option<bool>`: absent or blank is `None`, as for the other types.
12999 FormFieldKind::Bool if nullable => quote! {
13000 let __v: ::core::option::Option<bool> = match __raw {
13001 ::core::option::Option::None => ::core::option::Option::None,
13002 ::core::option::Option::Some(__s) if __s.is_empty() => {
13003 ::core::option::Option::None
13004 }
13005 ::core::option::Option::Some(__s) => ::core::option::Option::Some(!matches!(
13006 __s.to_ascii_lowercase().as_str(),
13007 "false" | "0" | "off" | "no"
13008 )),
13009 };
13010 },
13011 FormFieldKind::Bool => quote! {
13012 let __v: bool = match __raw {
13013 ::core::option::Option::None => false,
13014 ::core::option::Option::Some(__s) => !matches!(
13015 __s.to_ascii_lowercase().as_str(),
13016 "" | "false" | "0" | "off" | "no"
13017 ),
13018 };
13019 },
13020 FormFieldKind::String => {
13021 if nullable {
13022 quote! {
13023 let __v: ::core::option::Option<::std::string::String> = match __raw {
13024 ::core::option::Option::None => ::core::option::Option::None,
13025 ::core::option::Option::Some(__s) if __s.is_empty() => {
13026 ::core::option::Option::None
13027 }
13028 ::core::option::Option::Some(__s) => {
13029 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
13030 }
13031 };
13032 }
13033 } else {
13034 quote! {
13035 let __v: ::std::string::String = match __raw {
13036 ::core::option::Option::Some(__s) if !__s.is_empty() => {
13037 ::core::clone::Clone::clone(__s)
13038 }
13039 _ => {
13040 #errs.add(#name_lit, "This field is required.");
13041 ::std::string::String::new()
13042 }
13043 };
13044 }
13045 }
13046 }
13047 FormFieldKind::I16
13048 | FormFieldKind::I32
13049 | FormFieldKind::I64
13050 | FormFieldKind::F32
13051 | FormFieldKind::F64 => {
13052 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
13053 let ty_lit = kind.parse_method();
13054 let default_val = match kind {
13055 FormFieldKind::I16 => quote! { 0i16 },
13056 FormFieldKind::I32 => quote! { 0i32 },
13057 FormFieldKind::I64 => quote! { 0i64 },
13058 FormFieldKind::F32 => quote! { 0f32 },
13059 FormFieldKind::F64 => quote! { 0f64 },
13060 _ => quote! { Default::default() },
13061 };
13062 // NaN and inf parse, then pass every min/max compare.
13063 let finite = if matches!(kind, FormFieldKind::F32 | FormFieldKind::F64) {
13064 quote! {
13065 .and_then(|__n| if __n.is_finite() {
13066 ::core::result::Result::Ok(__n)
13067 } else {
13068 ::core::result::Result::Err(::std::string::String::from("not a finite number"))
13069 })
13070 }
13071 } else {
13072 quote! {}
13073 };
13074 if nullable {
13075 quote! {
13076 let __v: ::core::option::Option<#parse_ty> = match __raw {
13077 ::core::option::Option::None => ::core::option::Option::None,
13078 ::core::option::Option::Some(__s) if __s.is_empty() => {
13079 ::core::option::Option::None
13080 }
13081 ::core::option::Option::Some(__s) => {
13082 match __s.parse::<#parse_ty>().map_err(|__e| ::std::string::ToString::to_string(&__e)) #finite {
13083 ::core::result::Result::Ok(__n) => {
13084 ::core::option::Option::Some(__n)
13085 }
13086 ::core::result::Result::Err(__e) => {
13087 #errs.add(
13088 #name_lit,
13089 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
13090 );
13091 ::core::option::Option::None
13092 }
13093 }
13094 }
13095 };
13096 }
13097 } else {
13098 quote! {
13099 let __v: #parse_ty = match __raw {
13100 ::core::option::Option::Some(__s) if !__s.is_empty() => {
13101 match __s.parse::<#parse_ty>().map_err(|__e| ::std::string::ToString::to_string(&__e)) #finite {
13102 ::core::result::Result::Ok(__n) => __n,
13103 ::core::result::Result::Err(__e) => {
13104 #errs.add(
13105 #name_lit,
13106 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
13107 );
13108 #default_val
13109 }
13110 }
13111 }
13112 _ => {
13113 #errs.add(#name_lit, "This field is required.");
13114 #default_val
13115 }
13116 };
13117 }
13118 }
13119 }
13120 };
13121
13122 let validators = render_form_validators(name_lit, kind, nullable, attrs);
13123
13124 quote! {
13125 // `mut` so the per-field `clean` hook (#372) can rewrite the
13126 // parsed value in-place when it returns Ok with a normalized
13127 // form (e.g. trim / lowercase).
13128 let mut #ident = {
13129 #lookup
13130 #parsed_value
13131 #validators
13132 __v
13133 };
13134 }
13135}
13136
13137fn render_form_validators(
13138 name_lit: &str,
13139 kind: FormFieldKind,
13140 nullable: bool,
13141 attrs: &FormFieldAttrs,
13142) -> TokenStream2 {
13143 let errs = form_local("__errors");
13144 let mut checks: Vec<TokenStream2> = Vec::new();
13145
13146 let val_ref = if nullable {
13147 quote! { __v.as_ref() }
13148 } else {
13149 quote! { ::core::option::Option::Some(&__v) }
13150 };
13151
13152 let is_string = matches!(kind, FormFieldKind::String);
13153 let is_numeric = matches!(
13154 kind,
13155 FormFieldKind::I16
13156 | FormFieldKind::I32
13157 | FormFieldKind::I64
13158 | FormFieldKind::F32
13159 | FormFieldKind::F64
13160 );
13161
13162 if is_string {
13163 if let Some(min_len) = attrs.min_length {
13164 let min_len_usize = min_len as usize;
13165 checks.push(quote! {
13166 if let ::core::option::Option::Some(__s) = #val_ref {
13167 if __s.chars().count() < #min_len_usize {
13168 #errs.add(
13169 #name_lit,
13170 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
13171 );
13172 }
13173 }
13174 });
13175 }
13176 if let Some(max_len) = attrs.max_length {
13177 let max_len_usize = max_len as usize;
13178 checks.push(quote! {
13179 if let ::core::option::Option::Some(__s) = #val_ref {
13180 if __s.chars().count() > #max_len_usize {
13181 #errs.add(
13182 #name_lit,
13183 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
13184 );
13185 }
13186 }
13187 });
13188 }
13189 }
13190
13191 if is_numeric {
13192 if let Some(min) = attrs.min {
13193 checks.push(quote! {
13194 if let ::core::option::Option::Some(__n) = #val_ref {
13195 if (*__n as f64) < (#min as f64) {
13196 #errs.add(
13197 #name_lit,
13198 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
13199 );
13200 }
13201 }
13202 });
13203 }
13204 if let Some(max) = attrs.max {
13205 checks.push(quote! {
13206 if let ::core::option::Option::Some(__n) = #val_ref {
13207 if (*__n as f64) > (#max as f64) {
13208 #errs.add(
13209 #name_lit,
13210 ::std::format!("Ensure this value is less than or equal to {}.", #max),
13211 );
13212 }
13213 }
13214 });
13215 }
13216 }
13217
13218 quote! { #( #checks )* }
13219}
13220
13221// ============================================================
13222// #[derive(ViewSet)]
13223// ============================================================
13224
13225struct ViewSetAttrs {
13226 model: syn::Path,
13227 fields: Vec<String>,
13228 filter_fields: Vec<String>,
13229 search_fields: Vec<String>,
13230 /// (field_name, desc)
13231 ordering: Vec<(String, bool)>,
13232 page_size: Option<usize>,
13233 read_only: bool,
13234 allow_anonymous: bool,
13235 perms: ViewSetPermsAttrs,
13236 /// `#[viewset(serializer = SomeSerializer)]` — render list /
13237 /// retrieve / create responses through this `#[derive(Serializer)]`
13238 /// type instead of the default field-level projection (requires the
13239 /// `serializer` feature). Tri-dialect.
13240 serializer: Option<syn::Path>,
13241}
13242
13243#[derive(Default)]
13244struct ViewSetPermsAttrs {
13245 list: Vec<String>,
13246 retrieve: Vec<String>,
13247 create: Vec<String>,
13248 update: Vec<String>,
13249 destroy: Vec<String>,
13250}
13251
13252fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
13253 let root = rustango_root();
13254 let struct_name = &input.ident;
13255
13256 // Must be a unit struct or an empty named struct.
13257 match &input.data {
13258 Data::Struct(s) => match &s.fields {
13259 Fields::Unit | Fields::Named(_) => {}
13260 Fields::Unnamed(_) => {
13261 return Err(syn::Error::new_spanned(
13262 struct_name,
13263 "ViewSet can only be derived on a unit struct or an empty named struct",
13264 ));
13265 }
13266 },
13267 _ => {
13268 return Err(syn::Error::new_spanned(
13269 struct_name,
13270 "ViewSet can only be derived on a struct",
13271 ));
13272 }
13273 }
13274
13275 let attrs = parse_viewset_attrs(input)?;
13276 let model_path = &attrs.model;
13277
13278 let field_lits = attrs.fields.iter().map(|f| f.as_str());
13279
13280 let filter_fields_call = if attrs.filter_fields.is_empty() {
13281 quote!()
13282 } else {
13283 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
13284 quote!(.filter_fields(&[ #(#lits),* ]))
13285 };
13286
13287 let search_fields_call = if attrs.search_fields.is_empty() {
13288 quote!()
13289 } else {
13290 let lits = attrs.search_fields.iter().map(|f| f.as_str());
13291 quote!(.search_fields(&[ #(#lits),* ]))
13292 };
13293
13294 let ordering_call = if attrs.ordering.is_empty() {
13295 quote!()
13296 } else {
13297 let pairs = attrs.ordering.iter().map(|(f, desc)| {
13298 let f = f.as_str();
13299 quote!((#f, #desc))
13300 });
13301 quote!(.ordering(&[ #(#pairs),* ]))
13302 };
13303
13304 let page_size_call = if let Some(n) = attrs.page_size {
13305 quote!(.page_size(#n))
13306 } else {
13307 quote!()
13308 };
13309
13310 let read_only_call = if attrs.read_only {
13311 quote!(.read_only())
13312 } else {
13313 quote!()
13314 };
13315 let allow_anonymous_call = if attrs.allow_anonymous {
13316 quote!(.allow_anonymous())
13317 } else {
13318 quote!()
13319 };
13320
13321 // `.serializer::<S>()` — reshape responses through a derived
13322 // serializer. Requires the downstream crate to enable the
13323 // `serializer` feature (the method is gated on it).
13324 let serializer_call = if let Some(ref ser) = attrs.serializer {
13325 quote!(.serializer::<#ser>())
13326 } else {
13327 quote!()
13328 };
13329
13330 let perms = &attrs.perms;
13331 let perms_call = if perms.list.is_empty()
13332 && perms.retrieve.is_empty()
13333 && perms.create.is_empty()
13334 && perms.update.is_empty()
13335 && perms.destroy.is_empty()
13336 {
13337 quote!()
13338 } else {
13339 let list_lits = perms.list.iter().map(|s| s.as_str());
13340 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
13341 let create_lits = perms.create.iter().map(|s| s.as_str());
13342 let update_lits = perms.update.iter().map(|s| s.as_str());
13343 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
13344 quote! {
13345 .permissions(#root::viewset::ViewSetPerms {
13346 list: ::std::vec![ #(#list_lits.to_owned()),* ],
13347 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
13348 create: ::std::vec![ #(#create_lits.to_owned()),* ],
13349 update: ::std::vec![ #(#update_lits.to_owned()),* ],
13350 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
13351 })
13352 }
13353 };
13354
13355 Ok(quote! {
13356 impl #struct_name {
13357 /// Build an `axum::Router` with the six standard REST endpoints
13358 /// for this ViewSet, mounted at `prefix`.
13359 ///
13360 /// Accepts any backend's pool — a `sqlx::PgPool`,
13361 /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
13362 /// enum — because they all `Into<Pool>` (#1273). This is why
13363 /// the derive no longer names `PgPool`, which would not
13364 /// exist on a project built without the `postgres` feature.
13365 pub fn router(
13366 prefix: &str,
13367 pool: impl ::core::convert::Into<#root::sql::Pool>,
13368 ) -> #root::__axum::Router {
13369 #root::viewset::ViewSet::for_model(
13370 <#model_path as #root::core::Model>::SCHEMA,
13371 &[ #(#field_lits),* ],
13372 )
13373 #filter_fields_call
13374 #search_fields_call
13375 #ordering_call
13376 #page_size_call
13377 #perms_call
13378 #read_only_call
13379 #allow_anonymous_call
13380 #serializer_call
13381 .router_pool(prefix, pool.into())
13382 }
13383 }
13384 })
13385}
13386
13387fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
13388 let mut model: Option<syn::Path> = None;
13389 let mut fields: Option<Vec<String>> = None;
13390 let mut filter_fields: Vec<String> = Vec::new();
13391 let mut search_fields: Vec<String> = Vec::new();
13392 let mut ordering: Vec<(String, bool)> = Vec::new();
13393 let mut page_size: Option<usize> = None;
13394 let mut read_only = false;
13395 let mut allow_anonymous = false;
13396 let mut perms = ViewSetPermsAttrs::default();
13397 let mut serializer: Option<syn::Path> = None;
13398
13399 for attr in &input.attrs {
13400 if !attr.path().is_ident("viewset") {
13401 continue;
13402 }
13403 attr.parse_nested_meta(|meta| {
13404 if meta.path.is_ident("model") {
13405 let path: syn::Path = meta.value()?.parse()?;
13406 model = Some(path);
13407 return Ok(());
13408 }
13409 if meta.path.is_ident("serializer") {
13410 let path: syn::Path = meta.value()?.parse()?;
13411 serializer = Some(path);
13412 return Ok(());
13413 }
13414 if meta.path.is_ident("fields") {
13415 let s: LitStr = meta.value()?.parse()?;
13416 fields = Some(split_field_list(&s.value()));
13417 return Ok(());
13418 }
13419 if meta.path.is_ident("filter_fields") {
13420 let s: LitStr = meta.value()?.parse()?;
13421 filter_fields = split_field_list(&s.value());
13422 return Ok(());
13423 }
13424 if meta.path.is_ident("search_fields") {
13425 let s: LitStr = meta.value()?.parse()?;
13426 search_fields = split_field_list(&s.value());
13427 return Ok(());
13428 }
13429 if meta.path.is_ident("ordering") {
13430 let s: LitStr = meta.value()?.parse()?;
13431 ordering = parse_ordering_list(&s.value());
13432 return Ok(());
13433 }
13434 if meta.path.is_ident("page_size") {
13435 let lit: syn::LitInt = meta.value()?.parse()?;
13436 page_size = Some(lit.base10_parse::<usize>()?);
13437 return Ok(());
13438 }
13439 if meta.path.is_ident("read_only") {
13440 read_only = true;
13441 return Ok(());
13442 }
13443 if meta.path.is_ident("allow_anonymous") {
13444 allow_anonymous = true;
13445 return Ok(());
13446 }
13447 if meta.path.is_ident("permissions") {
13448 meta.parse_nested_meta(|inner| {
13449 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
13450 let s: LitStr = inner.value()?.parse()?;
13451 Ok(split_field_list(&s.value()))
13452 };
13453 if inner.path.is_ident("list") {
13454 perms.list = parse_codenames(&inner)?;
13455 } else if inner.path.is_ident("retrieve") {
13456 perms.retrieve = parse_codenames(&inner)?;
13457 } else if inner.path.is_ident("create") {
13458 perms.create = parse_codenames(&inner)?;
13459 } else if inner.path.is_ident("update") {
13460 perms.update = parse_codenames(&inner)?;
13461 } else if inner.path.is_ident("destroy") {
13462 perms.destroy = parse_codenames(&inner)?;
13463 } else {
13464 return Err(inner.error(
13465 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
13466 ));
13467 }
13468 Ok(())
13469 })?;
13470 return Ok(());
13471 }
13472 Err(meta.error(
13473 "unknown viewset attribute (supported: model, fields, filter_fields, \
13474 search_fields, ordering, page_size, read_only, allow_anonymous, serializer, \
13475 permissions(...))",
13476 ))
13477 })?;
13478 }
13479
13480 let model = model.ok_or_else(|| {
13481 syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
13482 })?;
13483 // The field list is the write allow-list, so it is never implied.
13484 let fields = fields.ok_or_else(|| {
13485 syn::Error::new_spanned(
13486 &input.ident,
13487 "`#[viewset(fields = \"id, title\")]` is required: the fields the API renders and writes",
13488 )
13489 })?;
13490
13491 Ok(ViewSetAttrs {
13492 model,
13493 fields,
13494 filter_fields,
13495 search_fields,
13496 ordering,
13497 page_size,
13498 read_only,
13499 allow_anonymous,
13500 perms,
13501 serializer,
13502 })
13503}
13504
13505// ============================================================ #[derive(Serializer)]
13506
13507struct SerializerContainerAttrs {
13508 model: syn::Path,
13509 /// `#[serializer(validate = "fn_name")]` on the struct — the
13510 /// cross-field validation hook (#436). The named inherent method
13511 /// must take `&self` and return
13512 /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
13513 /// `validate()` runs every per-field validator first; then calls
13514 /// the cross-field method if declared; aggregates all errors into
13515 /// one `FormErrors`.
13516 cross_validate: Option<syn::Ident>,
13517}
13518
13519#[derive(Default)]
13520struct SerializerFieldAttrs {
13521 read_only: bool,
13522 write_only: bool,
13523 source: Option<String>,
13524 skip: bool,
13525 /// `#[serializer(method = "fn_name")]` — computed field. The macro
13526 /// emits a `from_model` initializer that calls
13527 /// `Self::fn_name(&model)` and stores the return value.
13528 method: Option<String>,
13529 /// `#[serializer(validate = "fn_name")]` — per-field validator
13530 /// callable run by `Self::validate(&self)`. Must return
13531 /// `Result<(), String>`. Errors land in `FormErrors` keyed by
13532 /// the field name.
13533 validate: Option<String>,
13534 /// `#[serializer(nested)]` on a field whose type is another
13535 /// `Serializer` — the macro emits `from_model` initializer that
13536 /// reads the parent via `model.<source>.value()` then calls the
13537 /// child serializer's `from_model(parent)`. When the FK is
13538 /// unloaded the field falls back to `Default::default()` (does
13539 /// NOT panic) so a missing prefetch in prod degrades gracefully.
13540 /// Source field on the model defaults to the field name; override
13541 /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
13542 /// panic-on-unloaded behavior for tests.
13543 nested: bool,
13544 /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
13545 /// strict behavior: panic when the FK isn't loaded. Useful in
13546 /// test code where forgetting select_related must trip a hard
13547 /// failure rather than render a blank nested object.
13548 nested_strict: bool,
13549 /// `#[serializer(many = TagSerializer)]` — declare the field as
13550 /// a list of nested serializers. Field type must be `Vec<S>`
13551 /// where `S` is the inner serializer. The macro initializes the
13552 /// field to `Vec::new()` in `from_model` and emits a typed
13553 /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
13554 /// maps each model row through `S::from_model`. Auto-load isn't
13555 /// possible (the M2M / one-to-many accessor is async); callers
13556 /// fetch the children + call the setter post-from_model.
13557 many: Option<syn::Type>,
13558 /// `#[serializer(slug = "name")]` — render a relation as one of
13559 /// its fields.
13560 /// Source field on the model must be a `ForeignKey<T>`; the
13561 /// macro emits `from_model` glue that walks
13562 /// `model.<source>.value()?.<slug>` and clones it. Field type on
13563 /// the serializer is typically `String` (whatever type the slug
13564 /// column has). When the FK is unloaded the field falls back to
13565 /// `Default::default()`, same graceful-degrade contract as
13566 /// `nested`. Source defaults to the field name; override with
13567 /// `source = "..."`. v0.44.
13568 slug: Option<String>,
13569 /// `#[serializer(max_length = N)]` — caps
13570 /// the character count of a string field on write. Overrides the
13571 /// model's `max_length`; when absent the model value is inherited.
13572 max_length: Option<u64>,
13573 /// `#[serializer(min_length = N)]` — minimum character count.
13574 /// Serializer-only (the model has no `min_length` column).
13575 min_length: Option<u64>,
13576 /// `#[serializer(min = N)]` — inclusive
13577 /// integer lower bound; overrides the model's `min` when given.
13578 min: Option<i64>,
13579 /// `#[serializer(max = N)]` — inclusive
13580 /// integer upper bound; overrides the model's `max` when given.
13581 max: Option<i64>,
13582}
13583
13584fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
13585 let mut model: Option<syn::Path> = None;
13586 let mut cross_validate: Option<syn::Ident> = None;
13587 for attr in &input.attrs {
13588 if !attr.path().is_ident("serializer") {
13589 continue;
13590 }
13591 attr.parse_nested_meta(|meta| {
13592 if meta.path.is_ident("model") {
13593 let _eq: syn::Token![=] = meta.input.parse()?;
13594 model = Some(meta.input.parse()?);
13595 return Ok(());
13596 }
13597 if meta.path.is_ident("validate") {
13598 // #436 — container-level `validate = "fn_name"` for
13599 // cross-field validation. Field-level
13600 // `#[serializer(validate = "...")]` on a field is
13601 // parsed separately in `parse_serializer_field_attrs`.
13602 let s: LitStr = meta.value()?.parse()?;
13603 cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
13604 return Ok(());
13605 }
13606 Err(meta.error(
13607 "unknown serializer container attribute \
13608 (supported: `model`, `validate`)",
13609 ))
13610 })?;
13611 }
13612 let model = model.ok_or_else(|| {
13613 syn::Error::new_spanned(
13614 &input.ident,
13615 "`#[serializer(model = SomeModel)]` is required",
13616 )
13617 })?;
13618 Ok(SerializerContainerAttrs {
13619 model,
13620 cross_validate,
13621 })
13622}
13623
13624/// The `#[serde(...)]` keys the Serializer derive honours. Its own
13625/// `Serialize` impl would otherwise drop them silently.
13626#[derive(Default)]
13627struct SerdeWireAttrs {
13628 rename_ser: Option<String>,
13629 rename_de: Option<String>,
13630 skip_ser: bool,
13631 skip_de: bool,
13632 skip_ser_if: Option<syn::ExprPath>,
13633}
13634
13635/// A serde `rename_all` rule, applied to a `snake_case` field name.
13636#[derive(Clone, Copy)]
13637enum SerdeRenameRule {
13638 Lower,
13639 Upper,
13640 Pascal,
13641 Camel,
13642 ScreamingSnake,
13643 Kebab,
13644 ScreamingKebab,
13645}
13646
13647impl SerdeRenameRule {
13648 fn parse(s: &LitStr) -> syn::Result<Self> {
13649 Ok(match s.value().as_str() {
13650 "lowercase" | "snake_case" => Self::Lower,
13651 "UPPERCASE" => Self::Upper,
13652 "PascalCase" => Self::Pascal,
13653 "camelCase" => Self::Camel,
13654 "SCREAMING_SNAKE_CASE" => Self::ScreamingSnake,
13655 "kebab-case" => Self::Kebab,
13656 "SCREAMING-KEBAB-CASE" => Self::ScreamingKebab,
13657 _ => {
13658 return Err(syn::Error::new_spanned(
13659 s,
13660 "unknown serde `rename_all` rule",
13661 ))
13662 }
13663 })
13664 }
13665
13666 /// Same output as serde's own field renaming.
13667 fn apply(self, field: &str) -> String {
13668 match self {
13669 Self::Lower => field.to_owned(),
13670 Self::Upper | Self::ScreamingSnake => field.to_ascii_uppercase(),
13671 Self::Pascal | Self::Camel => {
13672 let mut out = String::new();
13673 let mut cap = matches!(self, Self::Pascal);
13674 for ch in field.chars() {
13675 if ch == '_' {
13676 cap = !out.is_empty() || matches!(self, Self::Pascal);
13677 } else if cap {
13678 out.push(ch.to_ascii_uppercase());
13679 cap = false;
13680 } else {
13681 out.push(ch);
13682 }
13683 }
13684 out
13685 }
13686 Self::Kebab => field.replace('_', "-"),
13687 Self::ScreamingKebab => field.to_ascii_uppercase().replace('_', "-"),
13688 }
13689 }
13690}
13691
13692/// Consume the value of a `serde` key the derive does not act on.
13693fn skip_serde_meta_value(meta: &syn::meta::ParseNestedMeta) -> syn::Result<()> {
13694 if meta.input.peek(syn::Token![=]) {
13695 let _: syn::Expr = meta.value()?.parse()?;
13696 } else if meta.input.peek(syn::token::Paren) {
13697 let _: proc_macro2::Group = meta.input.parse()?;
13698 }
13699 Ok(())
13700}
13701
13702/// `rename = "x"` or `rename(serialize = "a", deserialize = "b")`.
13703fn parse_serde_rename(
13704 meta: &syn::meta::ParseNestedMeta,
13705 mut set: impl FnMut(bool, LitStr) -> syn::Result<()>,
13706) -> syn::Result<()> {
13707 if meta.input.peek(syn::Token![=]) {
13708 let s: LitStr = meta.value()?.parse()?;
13709 set(true, s.clone())?;
13710 return set(false, s);
13711 }
13712 meta.parse_nested_meta(|inner| {
13713 let is_ser = inner.path.is_ident("serialize");
13714 if !is_ser && !inner.path.is_ident("deserialize") {
13715 return Err(inner.error("expected `serialize` or `deserialize`"));
13716 }
13717 set(is_ser, inner.value()?.parse()?)
13718 })
13719}
13720
13721/// Container `rename_all` rules: (serialize, deserialize).
13722fn parse_serde_container_rules(
13723 input: &DeriveInput,
13724) -> syn::Result<(Option<SerdeRenameRule>, Option<SerdeRenameRule>)> {
13725 let (mut ser, mut de) = (None, None);
13726 for attr in input.attrs.iter().filter(|a| a.path().is_ident("serde")) {
13727 attr.parse_nested_meta(|meta| {
13728 if meta.path.is_ident("rename_all") {
13729 return parse_serde_rename(&meta, |is_ser, s| {
13730 let rule = Some(SerdeRenameRule::parse(&s)?);
13731 if is_ser {
13732 ser = rule;
13733 } else {
13734 de = rule;
13735 }
13736 Ok(())
13737 });
13738 }
13739 skip_serde_meta_value(&meta)
13740 })?;
13741 }
13742 Ok((ser, de))
13743}
13744
13745fn parse_serde_field_attrs(field: &syn::Field) -> syn::Result<SerdeWireAttrs> {
13746 let mut out = SerdeWireAttrs::default();
13747 for attr in field.attrs.iter().filter(|a| a.path().is_ident("serde")) {
13748 attr.parse_nested_meta(|meta| {
13749 if meta.path.is_ident("rename") {
13750 return parse_serde_rename(&meta, |is_ser, s| {
13751 if is_ser {
13752 out.rename_ser = Some(s.value());
13753 } else {
13754 out.rename_de = Some(s.value());
13755 }
13756 Ok(())
13757 });
13758 }
13759 if meta.path.is_ident("skip") {
13760 out.skip_ser = true;
13761 out.skip_de = true;
13762 return Ok(());
13763 }
13764 if meta.path.is_ident("skip_serializing") {
13765 out.skip_ser = true;
13766 return Ok(());
13767 }
13768 if meta.path.is_ident("skip_deserializing") {
13769 out.skip_de = true;
13770 return Ok(());
13771 }
13772 if meta.path.is_ident("skip_serializing_if") {
13773 let s: LitStr = meta.value()?.parse()?;
13774 out.skip_ser_if = Some(s.parse()?);
13775 return Ok(());
13776 }
13777 skip_serde_meta_value(&meta)
13778 })?;
13779 }
13780 Ok(out)
13781}
13782
13783fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
13784 let mut out = SerializerFieldAttrs::default();
13785 for attr in &field.attrs {
13786 if !attr.path().is_ident("serializer") {
13787 continue;
13788 }
13789 attr.parse_nested_meta(|meta| {
13790 if meta.path.is_ident("read_only") {
13791 out.read_only = true;
13792 return Ok(());
13793 }
13794 if meta.path.is_ident("write_only") {
13795 out.write_only = true;
13796 return Ok(());
13797 }
13798 if meta.path.is_ident("skip") {
13799 out.skip = true;
13800 return Ok(());
13801 }
13802 if meta.path.is_ident("source") {
13803 let s: LitStr = meta.value()?.parse()?;
13804 out.source = Some(s.value());
13805 return Ok(());
13806 }
13807 if meta.path.is_ident("method") {
13808 let s: LitStr = meta.value()?.parse()?;
13809 out.method = Some(s.value());
13810 return Ok(());
13811 }
13812 if meta.path.is_ident("validate") {
13813 let s: LitStr = meta.value()?.parse()?;
13814 out.validate = Some(s.value());
13815 return Ok(());
13816 }
13817 if meta.path.is_ident("many") {
13818 let _eq: syn::Token![=] = meta.input.parse()?;
13819 out.many = Some(meta.input.parse()?);
13820 return Ok(());
13821 }
13822 if meta.path.is_ident("nested") {
13823 out.nested = true;
13824 // Optional strict flag inside parentheses:
13825 // #[serializer(nested(strict))]
13826 if meta.input.peek(syn::token::Paren) {
13827 meta.parse_nested_meta(|inner| {
13828 if inner.path.is_ident("strict") {
13829 out.nested_strict = true;
13830 return Ok(());
13831 }
13832 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
13833 })?;
13834 }
13835 return Ok(());
13836 }
13837 if meta.path.is_ident("slug") {
13838 let s: LitStr = meta.value()?.parse()?;
13839 out.slug = Some(s.value());
13840 return Ok(());
13841 }
13842 if meta.path.is_ident("max_length") {
13843 let lit: syn::LitInt = meta.value()?.parse()?;
13844 out.max_length = Some(lit.base10_parse::<u64>()?);
13845 return Ok(());
13846 }
13847 if meta.path.is_ident("min_length") {
13848 let lit: syn::LitInt = meta.value()?.parse()?;
13849 out.min_length = Some(lit.base10_parse::<u64>()?);
13850 return Ok(());
13851 }
13852 if meta.path.is_ident("min") {
13853 let lit: syn::LitInt = meta.value()?.parse()?;
13854 out.min = Some(lit.base10_parse::<i64>()?);
13855 return Ok(());
13856 }
13857 if meta.path.is_ident("max") {
13858 let lit: syn::LitInt = meta.value()?.parse()?;
13859 out.max = Some(lit.base10_parse::<i64>()?);
13860 return Ok(());
13861 }
13862 Err(meta.error(
13863 "unknown serializer field attribute (supported: \
13864 `read_only`, `write_only`, `source`, `skip`, `method`, \
13865 `validate`, `nested`, `many`, `slug`, `max_length`, \
13866 `min_length`, `min`, `max`)",
13867 ))
13868 })?;
13869 }
13870 // Validate: read_only + write_only is nonsensical
13871 if out.read_only && out.write_only {
13872 return Err(syn::Error::new_spanned(
13873 field,
13874 "a field cannot be both `read_only` and `write_only`",
13875 ));
13876 }
13877 if out.method.is_some() && out.source.is_some() {
13878 return Err(syn::Error::new_spanned(
13879 field,
13880 "`method` and `source` are mutually exclusive — `method` computes \
13881 the value from a method, `source` reads it from a different model field",
13882 ));
13883 }
13884 if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
13885 return Err(syn::Error::new_spanned(
13886 field,
13887 "`slug` is mutually exclusive with `method`, `nested`, and `many` \
13888 — pick one strategy for populating the field",
13889 ));
13890 }
13891 Ok(out)
13892}
13893
13894fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
13895 let root = rustango_root();
13896 let struct_name = &input.ident;
13897 let struct_name_lit = struct_name.to_string();
13898
13899 let Data::Struct(data) = &input.data else {
13900 return Err(syn::Error::new_spanned(
13901 struct_name,
13902 "Serializer can only be derived on structs",
13903 ));
13904 };
13905 let Fields::Named(named) = &data.fields else {
13906 return Err(syn::Error::new_spanned(
13907 struct_name,
13908 "Serializer requires a struct with named fields",
13909 ));
13910 };
13911
13912 let container = parse_serializer_container_attrs(input)?;
13913 let model_path = &container.model;
13914
13915 // Classify each field. `ty` is only consumed by the
13916 // `#[cfg(feature = "openapi")]` block below, but we always
13917 // capture it to keep the field-info build a single pass.
13918 #[allow(dead_code)]
13919 struct FieldInfo {
13920 ident: syn::Ident,
13921 ty: syn::Type,
13922 attrs: SerializerFieldAttrs,
13923 serde: SerdeWireAttrs,
13924 /// JSON key in the output / in a request body.
13925 ser_name: String,
13926 de_name: String,
13927 }
13928 let (ser_rule, de_rule) = parse_serde_container_rules(input)?;
13929 let mut fields_info: Vec<FieldInfo> = Vec::new();
13930 for field in &named.named {
13931 let ident = field.ident.clone().expect("named field has ident");
13932 let attrs = parse_serializer_field_attrs(field)?;
13933 let serde = parse_serde_field_attrs(field)?;
13934 let name = ident_name(&ident);
13935 let wire = |rename: &Option<String>, rule: Option<SerdeRenameRule>| {
13936 rename
13937 .clone()
13938 .unwrap_or_else(|| rule.map_or_else(|| name.clone(), |r| r.apply(&name)))
13939 };
13940 fields_info.push(FieldInfo {
13941 ser_name: wire(&serde.rename_ser, ser_rule),
13942 de_name: wire(&serde.rename_de, de_rule),
13943 ident,
13944 ty: field.ty.clone(),
13945 attrs,
13946 serde,
13947 });
13948 }
13949
13950 // Generate from_model body: struct literal with each field assigned.
13951 let from_model_fields = fields_info.iter().map(|fi| {
13952 let ident = &fi.ident;
13953 if let Some(_inner) = &fi.attrs.many {
13954 // Many — collection field. Initialize empty; caller
13955 // populates via the macro-emitted set_<field> helper
13956 // after fetching the M2M children.
13957 quote! { #ident: ::std::vec::Vec::new() }
13958 } else if let Some(method) = &fi.attrs.method {
13959 // SerializerMethodField: call Self::<method>(&model) to
13960 // compute the value. Method signature must be
13961 // `fn <method>(model: &T) -> <field type>`.
13962 let method_ident = syn::Ident::new(method, ident.span());
13963 quote! { #ident: Self::#method_ident(model) }
13964 } else if let Some(slug_field) = &fi.attrs.slug {
13965 // v0.44 — SlugRelatedField. Source defaults to the field
13966 // name on this struct; override via `source = "..."`. The
13967 // source field on the model is expected to be a
13968 // `ForeignKey<T>`; the slug field on the parent is named
13969 // by the attribute value. When the FK is unloaded the
13970 // field falls back to `Default::default()` — same
13971 // graceful-degrade contract as `nested`; a NULL
13972 // `Option<ForeignKey>` renders `None`.
13973 let src_name = fi
13974 .attrs
13975 .source
13976 .as_deref()
13977 .unwrap_or(&ident_name(&fi.ident))
13978 .to_owned();
13979 let src_ident = ident_from_name(&src_name, ident.span());
13980 let slug_ident = syn::Ident::new(slug_field, ident.span());
13981 quote! {
13982 #ident: #root::serializer::LoadedRelation::render(
13983 &model.#src_ident,
13984 |__loaded| ::core::clone::Clone::clone(&__loaded.#slug_ident),
13985 ::core::default::Default::default,
13986 )
13987 }
13988 } else if fi.attrs.nested {
13989 // Nested serializer. Source defaults to the field name on
13990 // this struct; override via `source = "..."`. The source
13991 // field on the model is a `ForeignKey<T>` or
13992 // `Option<ForeignKey<T>>`, read through `LoadedRelation`.
13993 //
13994 // Behavior matrix (tweakable per-field):
13995 // * FK loaded → nested object materializes via
13996 // ChildSerializer::from_model(parent).
13997 // * FK unloaded → fall back to ChildSerializer::default()
13998 // (so prod doesn't crash on a missing
13999 // prefetch — just renders a blank nested
14000 // object). Add `#[serializer(nested(
14001 // strict))]` to keep the v0.18.1
14002 // panic-on-unloaded behavior for tests
14003 // that want hard guardrails.
14004 // * NULL `Option<ForeignKey>` → `None` in an `Option<Child>`
14005 // field (JSON null), strict or not.
14006 let src_name = fi.attrs.source.as_deref().unwrap_or(&ident_name(&fi.ident)).to_owned();
14007 let src_ident = ident_from_name(&src_name, ident.span());
14008 let unloaded = if fi.attrs.nested_strict {
14009 let panic_msg = format!(
14010 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
14011 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
14012 );
14013 quote! { || ::core::panic!("{}", #panic_msg) }
14014 } else {
14015 quote! { ::core::default::Default::default }
14016 };
14017 quote! {
14018 #ident: #root::serializer::LoadedRelation::render(
14019 &model.#src_ident,
14020 |__loaded| #root::serializer::ModelSerializer::from_model(__loaded),
14021 #unloaded,
14022 )
14023 }
14024 } else if fi.attrs.write_only || fi.attrs.skip {
14025 // Not read from model — use default
14026 quote! { #ident: ::core::default::Default::default() }
14027 } else if let Some(src) = &fi.attrs.source {
14028 let src_ident = ident_from_name(src, ident.span());
14029 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
14030 } else {
14031 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
14032 }
14033 });
14034
14035 // is_writable predicate (also used by writable_lits /
14036 // writable_source_lits below).
14037 let is_writable = |fi: &&FieldInfo| {
14038 !fi.attrs.read_only
14039 && !fi.attrs.skip
14040 && !fi.serde.skip_de
14041 && fi.attrs.method.is_none()
14042 && !fi.attrs.nested
14043 && fi.attrs.many.is_none()
14044 && fi.attrs.slug.is_none()
14045 };
14046
14047 // Declarative field constraints: one block
14048 // per writable field, run inside `validate()`. Each resolves its
14049 // bounds as the serializer attr when given (`#[serializer(max_length
14050 // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
14051 // `max` / `choices`), then dispatches on the field's JSON value via
14052 // `forms::validators::check_value` (string → length/choices, integer
14053 // → min/max).
14054 let opt_usize = |v: Option<u64>| match v {
14055 Some(n) => {
14056 let n = n as usize;
14057 quote!(::core::option::Option::Some(#n))
14058 }
14059 None => quote!(::core::option::Option::None),
14060 };
14061 #[cfg(feature = "openapi")]
14062 let opt_u64 = |v: Option<u64>| match v {
14063 Some(n) => quote!(::core::option::Option::Some(#n)),
14064 None => quote!(::core::option::Option::None),
14065 };
14066 let opt_i64 = |v: Option<i64>| match v {
14067 Some(n) => quote!(::core::option::Option::Some(#n)),
14068 None => quote!(::core::option::Option::None),
14069 };
14070 let constraint_blocks: Vec<_> = fields_info
14071 .iter()
14072 .filter(is_writable)
14073 .map(|fi| {
14074 let ident = &fi.ident;
14075 let fname = &fi.de_name;
14076 let mname = fi
14077 .attrs
14078 .source
14079 .clone()
14080 .unwrap_or_else(|| ident_name(&fi.ident));
14081 let attr_max_len = opt_usize(fi.attrs.max_length);
14082 let attr_min_len = opt_usize(fi.attrs.min_length);
14083 let attr_min = opt_i64(fi.attrs.min);
14084 let attr_max = opt_i64(fi.attrs.max);
14085 // A bound the field's type cannot take is a compile error, not a no-op.
14086 let ty = &fi.ty;
14087 let len_check = (fi.attrs.max_length.is_some() || fi.attrs.min_length.is_some())
14088 .then(|| quote! { #root::forms::validators::assert_length_bounded::<#ty>(); });
14089 let num_check = (fi.attrs.min.is_some() || fi.attrs.max.is_some())
14090 .then(|| quote! { #root::forms::validators::assert_number_bounded::<#ty>(); });
14091 quote! {
14092 #len_check
14093 #num_check
14094 if __sent(#fname) {
14095 let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
14096 let __max_length: ::core::option::Option<usize> = #attr_max_len
14097 .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
14098 let __min_length: ::core::option::Option<usize> = #attr_min_len;
14099 let __min: ::core::option::Option<i64> =
14100 #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
14101 let __max: ::core::option::Option<i64> =
14102 #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
14103 let __choices = __sf.and_then(|__f| __f.choices);
14104 let __v = #root::__serde_json::to_value(&self.#ident)
14105 .unwrap_or(#root::__serde_json::Value::Null);
14106 #root::forms::validators::check_value(
14107 #fname, &__v, __max_length, __min_length, __min, __max, __choices,
14108 &mut __errors,
14109 );
14110 }
14111 }
14112 })
14113 .collect();
14114 let has_constraints = !constraint_blocks.is_empty();
14115
14116 // Per-field validators. Emit a
14117 // `validate(&self)` method that runs each user-defined validator
14118 // and aggregates errors into `FormErrors`.
14119 let validator_calls: Vec<_> = fields_info
14120 .iter()
14121 .filter_map(|fi| {
14122 let ident = &fi.ident;
14123 let name_lit = &fi.de_name;
14124 let method = fi.attrs.validate.as_ref()?;
14125 let method_ident = syn::Ident::new(method, ident.span());
14126 Some(quote! {
14127 if __sent(#name_lit) {
14128 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
14129 __errors.add(#name_lit.to_owned(), __e);
14130 }
14131 }
14132 })
14133 })
14134 .collect();
14135 // #436 — cross-field `validate(&self)`. If the
14136 // container declared `#[serializer(validate = "fn_name")]`,
14137 // the macro-generated `validate(&self)` runs every per-field
14138 // validator first, then calls the user's cross-field method,
14139 // merging its `FormErrors` into the per-field errors. Either
14140 // alone is enough to emit the wrapper.
14141 let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
14142 quote! {
14143 // Merge cross-field errors into the per-field bucket so
14144 // a single .validate() call surfaces both layers.
14145 if let ::core::result::Result::Err(__cross) = self.#method_ident() {
14146 __errors.merge(__cross);
14147 }
14148 }
14149 });
14150 let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
14151 // Anything to run? Declarative constraints (for any writable field) +
14152 // per-field validators + cross-field hook.
14153 let has_run_validations = has_validators || has_constraints;
14154 // Shared body: declarative field constraints first (length / range /
14155 // choices, serializer-attr-or-model), then per-field validators, then
14156 // the cross-field hook. Field rules run only where `__sent(name)` (#1995).
14157 let checks = quote! {
14158 let mut __errors = #root::forms::FormErrors::default();
14159 #( #constraint_blocks )*
14160 #( #validator_calls )*
14161 #cross_validate_call
14162 if __errors.is_empty() {
14163 ::core::result::Result::Ok(())
14164 } else {
14165 ::core::result::Result::Err(__errors)
14166 }
14167 };
14168 let validate_body = quote! {
14169 let __sent = |_: &str| true;
14170 #checks
14171 };
14172 // PATCH: only the keys the body carries.
14173 let validate_patch_body = quote! {
14174 let __sent = |__n: &str| __obj.is_some_and(|__o| __o.contains_key(__n));
14175 #checks
14176 };
14177 // Inherent `validate(&self)` — kept for back-compat with direct
14178 // `serializer.validate()` calls that don't import `ModelSerializer`.
14179 // Emitted only when the serializer declares per-field/cross-field
14180 // validators (unchanged rule) so it never collides with a
14181 // hand-written inherent `validate`.
14182 let validate_method = if has_validators {
14183 quote! {
14184 impl #struct_name {
14185 /// Run the declarative field constraints, every
14186 /// `#[serializer(validate = "...")]` per-field validator,
14187 /// and (when declared) the container-level cross-field
14188 /// validator. Aggregates errors into `FormErrors` keyed by
14189 /// the field name. Returns `Ok(())` when all pass.
14190 pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
14191 #validate_body
14192 }
14193 }
14194 }
14195 } else {
14196 quote! {}
14197 };
14198 // Trait-method override so the type-erased ViewSet write path can
14199 // dispatch `ModelSerializer::validate` generically. Emitted whenever
14200 // there's anything to run (declarative constraints inherited from the
14201 // model count); otherwise the trait's default no-op is used.
14202 let trait_validate_override = if has_run_validations {
14203 quote! {
14204 fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
14205 #validate_body
14206 }
14207 }
14208 } else {
14209 quote! {}
14210 };
14211
14212 // For every `#[serializer(many = S)]` field, emit a
14213 // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
14214 // helper that maps the parents through `S::from_model`.
14215 let many_setters: Vec<_> = fields_info
14216 .iter()
14217 .filter_map(|fi| {
14218 let many_ty = fi.attrs.many.as_ref()?;
14219 let ident = &fi.ident;
14220 let setter = syn::Ident::new(&format!("set_{}", ident_name(ident)), ident.span());
14221 Some(quote! {
14222 /// Populate this `many` field by mapping each parent model
14223 /// through the inner serializer's `from_model`. Call after
14224 /// fetching the M2M / one-to-many children since
14225 /// `from_model` itself can't await an SQL query.
14226 pub fn #setter(
14227 &mut self,
14228 models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
14229 ) -> &mut Self {
14230 self.#ident = models.iter()
14231 .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
14232 .collect();
14233 self
14234 }
14235 })
14236 })
14237 .collect();
14238 let many_setters_impl = if many_setters.is_empty() {
14239 quote! {}
14240 } else {
14241 quote! {
14242 impl #struct_name {
14243 #( #many_setters )*
14244 }
14245 }
14246 };
14247
14248 // Generate custom Serialize: skip write_only and serde-skipped fields
14249 let output_fields: Vec<_> = fields_info
14250 .iter()
14251 .filter(|fi| !fi.attrs.write_only && !fi.serde.skip_ser)
14252 .collect();
14253 // `skip_serializing_if` fields count only when rendered.
14254 let output_field_count = output_fields.iter().map(|fi| {
14255 let ident = &fi.ident;
14256 if let Some(pred) = &fi.serde.skip_ser_if {
14257 quote! { + if #pred(&self.#ident) { 0 } else { 1 } }
14258 } else {
14259 quote! { + 1 }
14260 }
14261 });
14262 let output_field_count = quote! { 0usize #( #output_field_count )* };
14263 let serialize_fields = output_fields.iter().map(|fi| {
14264 let ident = &fi.ident;
14265 let name_lit = &fi.ser_name;
14266 if let Some(pred) = &fi.serde.skip_ser_if {
14267 quote! {
14268 if #pred(&self.#ident) {
14269 __state.skip_field(#name_lit)?;
14270 } else {
14271 __state.serialize_field(#name_lit, &self.#ident)?;
14272 }
14273 }
14274 } else {
14275 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
14276 }
14277 });
14278
14279 // writable_fields: normal + write_only.
14280 // Exclude:
14281 // - `read_only` — server-computed.
14282 // - `skip` — caller sets manually post-from_model.
14283 // - `method` — computed from a Self::fn(&model) call; accepting
14284 // it on write is meaningless.
14285 // - `nested` / `many` — populated from related-model data, not
14286 // from a field on the wire body.
14287 // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
14288 // `many` in `writable_fields()`, which made the ViewSet write
14289 // path accept those fields from the JSON body and try to bind
14290 // them to the SQL UPDATE — a silent no-op at best, a type
14291 // mismatch at worst.
14292 // (`is_writable` is defined above, near the constraint codegen.)
14293 let writable_lits: Vec<_> = fields_info
14294 .iter()
14295 .filter(is_writable)
14296 .map(|fi| fi.de_name.clone())
14297 .collect();
14298
14299 // `writable_source_fields`: the MODEL field names of writable
14300 // serializer fields (source-resolved). The ViewSet write path skips
14301 // every model column NOT in this set, so `read_only` / `method` /
14302 // computed fields a client posts are ignored instead of written.
14303 let writable_source_lits: Vec<String> = fields_info
14304 .iter()
14305 .filter(is_writable)
14306 .map(|fi| {
14307 fi.attrs
14308 .source
14309 .clone()
14310 .unwrap_or_else(|| ident_name(&fi.ident))
14311 })
14312 .collect();
14313
14314 // `readable_source_fields`: model columns the JSON output copies
14315 // straight from the model, so `?ordering=` cannot sort on a column
14316 // the API never shows (#1845).
14317 let readable: Vec<&FieldInfo> = fields_info
14318 .iter()
14319 .filter(|fi| {
14320 !fi.attrs.write_only
14321 && !fi.serde.skip_ser
14322 && !fi.attrs.skip
14323 && fi.attrs.method.is_none()
14324 && !fi.attrs.nested
14325 && fi.attrs.many.is_none()
14326 && fi.attrs.slug.is_none()
14327 })
14328 .collect();
14329 let readable_source_lits: Vec<String> = readable
14330 .iter()
14331 .map(|fi| {
14332 fi.attrs
14333 .source
14334 .clone()
14335 .unwrap_or_else(|| ident_name(&fi.ident))
14336 })
14337 .collect();
14338 // Their JSON keys, so the ViewSet finds a renamed cursor column.
14339 let readable_lits: Vec<String> = readable.iter().map(|fi| fi.ser_name.clone()).collect();
14340
14341 // `from_writable_json`: build a partial instance for input
14342 // validation. Writable fields are parsed from the JSON body (keyed
14343 // by serializer field name); every other field defaults. Per-field
14344 // type errors collect into `FormErrors` keyed by the field name.
14345 // Construction is field-by-field (not `Self::default()`) so it works
14346 // for serializers regardless of a struct-level `Default`.
14347 let from_writable_json_inits: Vec<_> = fields_info
14348 .iter()
14349 .map(|fi| {
14350 let ident = &fi.ident;
14351 let fname = &fi.de_name;
14352 let ty = &fi.ty;
14353 if is_writable(&fi) {
14354 quote! {
14355 #ident: match __obj.and_then(|__o| __o.get(#fname)) {
14356 ::core::option::Option::Some(__v) => {
14357 match #root::__serde_json::from_value::<#ty>(
14358 ::core::clone::Clone::clone(__v),
14359 ) {
14360 ::core::result::Result::Ok(__x) => __x,
14361 ::core::result::Result::Err(__e) => {
14362 __errors.add(#fname.to_owned(), __e.to_string());
14363 ::core::default::Default::default()
14364 }
14365 }
14366 }
14367 ::core::option::Option::None => ::core::default::Default::default(),
14368 }
14369 }
14370 } else {
14371 quote! { #ident: ::core::default::Default::default() }
14372 }
14373 })
14374 .collect();
14375 // `validate_patch`: overlay each sent writable field on the stored row.
14376 let patch_overlays: Vec<_> = fields_info
14377 .iter()
14378 .filter(is_writable)
14379 .map(|fi| {
14380 let ident = &fi.ident;
14381 let fname = &fi.de_name;
14382 let ty = &fi.ty;
14383 quote! {
14384 if let ::core::option::Option::Some(__v) = __obj.and_then(|__o| __o.get(#fname)) {
14385 match #root::__serde_json::from_value::<#ty>(::core::clone::Clone::clone(__v)) {
14386 ::core::result::Result::Ok(__x) => __merged.#ident = __x,
14387 ::core::result::Result::Err(__e) => {
14388 __errors.add(#fname.to_owned(), __e.to_string());
14389 }
14390 }
14391 }
14392 }
14393 })
14394 .collect();
14395 let patch_validate_call = if has_run_validations {
14396 quote! { __merged.__rustango_validate_patch(__obj) }
14397 } else {
14398 quote! { let _ = &__merged; ::core::result::Result::Ok(()) }
14399 };
14400 let patch_validate_helper = if has_run_validations {
14401 quote! {
14402 impl #struct_name {
14403 #[doc(hidden)]
14404 fn __rustango_validate_patch(
14405 &self,
14406 __obj: ::core::option::Option<&#root::__serde_json::Map<::std::string::String, #root::__serde_json::Value>>,
14407 ) -> ::core::result::Result<(), #root::forms::FormErrors> {
14408 #validate_patch_body
14409 }
14410 }
14411 }
14412 } else {
14413 quote! {}
14414 };
14415
14416 // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
14417 // Only includes fields shown in JSON output (skips write_only). For each
14418 // `Option<T>` field, omit from `required` and add `.nullable()`.
14419 let openapi_impl = {
14420 #[cfg(feature = "openapi")]
14421 {
14422 let property_calls = output_fields.iter().map(|fi| {
14423 let name_lit = &fi.ser_name;
14424 let ty = &fi.ty;
14425 let nullable_call = if is_option(ty) {
14426 quote! { .nullable() }
14427 } else {
14428 quote! {}
14429 };
14430 // The bounds `validate()` enforces on a written field.
14431 let limits_call = is_writable(fi).then(|| {
14432 let mname = fi
14433 .attrs
14434 .source
14435 .clone()
14436 .unwrap_or_else(|| ident_name(&fi.ident));
14437 let (a, b, c, d) = (
14438 opt_u64(fi.attrs.max_length),
14439 opt_u64(fi.attrs.min_length),
14440 opt_i64(fi.attrs.min),
14441 opt_i64(fi.attrs.max),
14442 );
14443 quote! {
14444 .field_limits(
14445 <#model_path as #root::core::Model>::SCHEMA.field(#mname),
14446 #a, #b, #c, #d,
14447 )
14448 }
14449 });
14450 quote! {
14451 .property(
14452 #name_lit,
14453 <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
14454 #limits_call
14455 #nullable_call,
14456 )
14457 }
14458 });
14459 let required_lits: Vec<_> = output_fields
14460 .iter()
14461 .filter(|fi| !is_option(&fi.ty) && fi.serde.skip_ser_if.is_none())
14462 .map(|fi| fi.ser_name.clone())
14463 .collect();
14464 quote! {
14465 impl #root::openapi::OpenApiSchema for #struct_name {
14466 fn openapi_schema() -> #root::openapi::Schema {
14467 #root::openapi::Schema::object()
14468 #( #property_calls )*
14469 .required([ #( #required_lits ),* ])
14470 }
14471 }
14472 }
14473 }
14474 #[cfg(not(feature = "openapi"))]
14475 {
14476 quote! {}
14477 }
14478 };
14479
14480 Ok(quote! {
14481 impl #root::serializer::ModelSerializer for #struct_name {
14482 type Model = #model_path;
14483
14484 fn from_model(model: &Self::Model) -> Self {
14485 Self {
14486 #( #from_model_fields ),*
14487 }
14488 }
14489
14490 fn writable_fields() -> &'static [&'static str] {
14491 &[ #( #writable_lits ),* ]
14492 }
14493
14494 fn writable_source_fields() -> &'static [&'static str] {
14495 &[ #( #writable_source_lits ),* ]
14496 }
14497
14498 fn readable_source_fields() -> &'static [&'static str] {
14499 &[ #( #readable_source_lits ),* ]
14500 }
14501
14502 fn readable_fields() -> &'static [&'static str] {
14503 &[ #( #readable_lits ),* ]
14504 }
14505
14506 fn from_writable_json(
14507 __body: &#root::__serde_json::Value,
14508 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
14509 let mut __errors = #root::forms::FormErrors::default();
14510 let __obj = __body.as_object();
14511 let __out = Self {
14512 #( #from_writable_json_inits ),*
14513 };
14514 if __errors.is_empty() {
14515 ::core::result::Result::Ok(__out)
14516 } else {
14517 ::core::result::Result::Err(__errors)
14518 }
14519 }
14520
14521 fn validate_patch(
14522 __stored: &Self::Model,
14523 __body: &#root::__serde_json::Value,
14524 ) -> ::core::result::Result<(), #root::forms::FormErrors> {
14525 let mut __errors = #root::forms::FormErrors::default();
14526 let __obj = __body.as_object();
14527 #[allow(unused_mut)]
14528 let mut __merged = <Self as #root::serializer::ModelSerializer>::from_model(__stored);
14529 #( #patch_overlays )*
14530 if !__errors.is_empty() {
14531 return ::core::result::Result::Err(__errors);
14532 }
14533 #patch_validate_call
14534 }
14535
14536 #trait_validate_override
14537 }
14538
14539 #patch_validate_helper
14540
14541 impl #root::__serde::Serialize for #struct_name {
14542 fn serialize<S>(&self, serializer: S)
14543 -> ::core::result::Result<S::Ok, S::Error>
14544 where
14545 S: #root::__serde::Serializer,
14546 {
14547 use #root::__serde::ser::SerializeStruct;
14548 let mut __state = serializer.serialize_struct(
14549 #struct_name_lit,
14550 #output_field_count,
14551 )?;
14552 #( #serialize_fields )*
14553 __state.end()
14554 }
14555 }
14556
14557 #openapi_impl
14558
14559 #validate_method
14560
14561 #many_setters_impl
14562 })
14563}
14564
14565/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
14566/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
14567/// when the feature is off.
14568#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
14569fn is_option(ty: &syn::Type) -> bool {
14570 if let syn::Type::Path(p) = ty {
14571 if let Some(last) = p.path.segments.last() {
14572 return last.ident == "Option";
14573 }
14574 }
14575 false
14576}
14577
14578#[cfg(test)]
14579mod main_attr_tests {
14580 use super::*;
14581
14582 fn expand(args: &str) -> String {
14583 let body = quote! { async fn main() { run().await } };
14584 expand_main(args.parse().unwrap(), body)
14585 .expect("expansion failed")
14586 .to_string()
14587 }
14588
14589 /// Asserts on the expansion rather than on `parse_logging`,
14590 /// because the defect was a subscriber reaching the output, not
14591 /// an argument being misread (#1465).
14592 #[test]
14593 fn logging_false_emits_no_subscriber() {
14594 let out = expand("logging = false");
14595 assert!(
14596 !out.contains("logging :: Setup"),
14597 "`logging = false` must not install a subscriber:\n{out}",
14598 );
14599 assert!(
14600 out.contains("block_on"),
14601 "the runtime must still be built:\n{out}",
14602 );
14603 }
14604
14605 #[test]
14606 fn default_still_installs_a_subscriber() {
14607 // The control. Without it, an `expand_main` that dropped the
14608 // prologue unconditionally would pass the test above while
14609 // leaving every default project with no logging at all.
14610 let out = expand("");
14611 assert!(
14612 out.contains("logging :: Setup"),
14613 "the default entrypoint must still install one:\n{out}",
14614 );
14615 // `./app serve` or `./app --port 8080` must keep logging to stdout:
14616 // only `Cli`, which knows its verbs, moves logs to stderr.
14617 assert!(
14618 !out.contains("args ()"),
14619 "the entrypoint must not route logs by argv:\n{out}",
14620 );
14621 }
14622
14623 #[test]
14624 fn logging_false_composes_with_flavor() {
14625 let out = expand("flavor = \"current_thread\", logging = false");
14626 assert!(!out.contains("logging :: Setup"), "{out}");
14627 assert!(out.contains("new_current_thread"), "{out}");
14628 }
14629
14630 #[test]
14631 fn logging_true_is_the_default_shape() {
14632 // `logging = true` is accepted and means the default, so the
14633 // match must not fire on the substring `logging`.
14634 assert!(parse_logging(&"logging = true".parse().unwrap()));
14635 assert!(!parse_logging(&"logging=false".parse().unwrap()));
14636 }
14637}
14638
14639#[cfg(test)]
14640mod main_arg_warning_tests {
14641 use super::*;
14642
14643 fn expand(args: &str) -> String {
14644 let body = quote! { async fn main() { run().await } };
14645 expand_main(args.parse().unwrap(), body)
14646 .expect("expansion failed")
14647 .to_string()
14648 }
14649
14650 /// A typo or unknown key used to be dropped without a word.
14651 #[test]
14652 fn unknown_or_misspelt_args_warn() {
14653 for args in ["logging = flase", "multi_thread", "workers = 4"] {
14654 let out = expand(args);
14655 assert!(out.contains("unknown argument"), "{args}:\n{out}");
14656 }
14657 }
14658
14659 #[test]
14660 fn supported_args_do_not_warn() {
14661 for args in [
14662 "",
14663 r#"flavor = "current_thread""#,
14664 r#"flavor = "multi_thread", logging = false"#,
14665 "logging = true",
14666 "worker_threads = 4, start_paused = true",
14667 r#"crate = "tokio", unhandled_panic = "shutdown_runtime""#,
14668 r#"flavor = "curent_thread""#,
14669 ] {
14670 assert!(!expand(args).contains("RUSTANGO_WARNING"), "{args}");
14671 }
14672 }
14673}
14674
14675#[cfg(test)]
14676mod embed_migrations_tests {
14677 use super::*;
14678
14679 fn expand(path: &str) -> String {
14680 expand_embed_migrations(quote! { #path })
14681 .expect("expansion failed")
14682 .to_string()
14683 }
14684
14685 #[test]
14686 fn missing_dir_warns() {
14687 let out = expand("./no_such_migrations_dir");
14688 assert!(out.contains("deprecated"), "missing dir must warn:\n{out}");
14689 assert!(out.contains("no_such_migrations_dir"), "{out}");
14690 }
14691
14692 #[test]
14693 fn existing_dir_does_not_warn() {
14694 let out = expand("./src");
14695 assert!(!out.contains("deprecated"), "{out}");
14696 }
14697}
14698
14699#[cfg(test)]
14700mod field_attr_tests {
14701 use super::*;
14702
14703 fn error_for(field: syn::Field) -> Option<String> {
14704 let attrs = parse_field_attrs(&field).unwrap();
14705 let kind = detect_type(&field.ty).unwrap().kind;
14706 check_bound_compatibility(&field, &attrs, kind)
14707 .err()
14708 .map(|e| e.to_string())
14709 }
14710
14711 /// `citext`, `vector` and `geometry` on the wrong type are errors, not no-ops (#1937).
14712 #[test]
14713 fn type_specific_attrs_reject_other_types() {
14714 let f: syn::FieldsNamed = syn::parse_quote!({
14715 #[rustango(citext)] a: i64,
14716 #[rustango(vector(dims = 3))] b: String,
14717 #[rustango(geometry(srid = 4326))] c: String,
14718 #[rustango(citext)] d: String,
14719 });
14720 let errs: Vec<_> = f.named.into_iter().map(error_for).collect();
14721 assert!(errs[0].as_deref().unwrap().contains("citext"), "{errs:?}");
14722 assert!(errs[1].as_deref().unwrap().contains("vector"), "{errs:?}");
14723 assert!(errs[2].as_deref().unwrap().contains("geometry"), "{errs:?}");
14724 assert_eq!(errs[3], None);
14725 }
14726
14727 /// `manager(ext)` emitted a trait nobody could add methods to; it is refused (#2132).
14728 #[test]
14729 fn manager_ext_is_refused() {
14730 let input: DeriveInput = syn::parse_quote! {
14731 #[rustango(table = "p", manager(ext = "PostManagerExt"))]
14732 struct Post { id: i64 }
14733 };
14734 let err = parse_container_attrs(&input).err().expect("refused");
14735 assert!(err.to_string().contains("#2132"), "{err}");
14736 }
14737
14738 /// `set_null` on a NOT NULL `ForeignKey<T>` would fail every parent delete.
14739 #[test]
14740 fn set_null_needs_an_optional_foreign_key() {
14741 let input: DeriveInput = syn::parse_quote! {
14742 #[rustango(table = "c")]
14743 struct Child {
14744 #[rustango(primary_key)] id: i64,
14745 #[rustango(on_delete = "set_null")] parent: ForeignKey<Parent>,
14746 }
14747 };
14748 let err = expand(&input).err().expect("refused");
14749 assert!(err.to_string().contains("set_null"), "{err}");
14750 let input: DeriveInput = syn::parse_quote! {
14751 #[rustango(table = "c")]
14752 struct Child {
14753 #[rustango(primary_key)] id: i64,
14754 #[rustango(on_delete = "set_null")] parent: Option<ForeignKey<Parent>>,
14755 }
14756 };
14757 assert!(expand(&input).is_ok());
14758 }
14759
14760 /// The same for `fk = "…"` / `o2o = "…"` on a plain column.
14761 #[test]
14762 fn set_null_needs_a_nullable_fk_column() {
14763 let input: DeriveInput = syn::parse_quote! {
14764 #[rustango(table = "c")]
14765 struct Child {
14766 #[rustango(primary_key)] id: i64,
14767 #[rustango(fk = "p", on_delete = "set_null")] parent_id: i64,
14768 #[rustango(o2o = "q", on_delete = "set_null")] other_id: Option<i64>,
14769 }
14770 };
14771 let err = expand(&input).err().expect("refused");
14772 assert!(err.to_string().contains("set_null"), "{err}");
14773 let input: DeriveInput = syn::parse_quote! {
14774 #[rustango(table = "c")]
14775 struct Child {
14776 #[rustango(primary_key)] id: i64,
14777 #[rustango(fk = "p", on_delete = "set_null")] parent_id: std::option::Option<i64>,
14778 #[rustango(o2o = "q", on_delete = "set_null")] other_id: ::core::option::Option<i64>,
14779 #[rustango(on_delete = "set_null")] parent: core::option::Option<ForeignKey<Parent>>,
14780 }
14781 };
14782 assert!(expand(&input).is_ok());
14783 }
14784
14785 /// An index or `get_latest_by` naming no column warns; it does not fail.
14786 #[test]
14787 fn unknown_index_columns_warn() {
14788 let input: DeriveInput = syn::parse_quote! {
14789 #[rustango(
14790 table = "w",
14791 unique_together = "a, nope",
14792 index_together = "a, b_typo",
14793 index("missing"),
14794 get_latest_by = "-when_typo"
14795 )]
14796 struct W { #[rustango(primary_key)] id: i64, a: i64, b: i64 }
14797 };
14798 let out = expand(&input).expect("still compiles").to_string();
14799 for name in ["nope", "b_typo", "missing", "when_typo"] {
14800 assert!(out.contains(&format!("`{name}`")), "{name}:\n{out}");
14801 }
14802 }
14803
14804 /// Real columns, field names for `get_latest_by` and expression
14805 /// indexes emit nothing new.
14806 #[test]
14807 fn known_index_columns_do_not_warn() {
14808 let input: DeriveInput = syn::parse_quote! {
14809 #[rustango(
14810 table = "w",
14811 unique_together = "a, b_col",
14812 index("lower(a)"),
14813 get_latest_by = "-b"
14814 )]
14815 struct W {
14816 #[rustango(primary_key)] id: i64,
14817 a: i64,
14818 #[rustango(column = "b_col")] b: i64,
14819 }
14820 };
14821 let out = expand(&input).expect("compiles").to_string();
14822 assert!(!out.contains("RUSTANGO_WARNING"), "{out}");
14823 }
14824
14825 /// Two FKs to one parent would emit the same reverse accessor twice.
14826 #[test]
14827 fn reverse_accessor_clash_names_related_name() {
14828 let input: DeriveInput = syn::parse_quote! {
14829 #[rustango(table = "m")]
14830 struct Message {
14831 #[rustango(primary_key)] id: i64,
14832 sender: ForeignKey<User>,
14833 recipient: ForeignKey<User>,
14834 }
14835 };
14836 let err = expand(&input).err().expect("refused");
14837 assert!(err.to_string().contains("related_name"), "{err}");
14838 }
14839
14840 /// `related_name = "+"` emits no reverse accessor, so a parent from
14841 /// another crate, which cannot take an inherent impl, still derives.
14842 #[test]
14843 fn related_name_plus_skips_the_reverse_accessor() {
14844 let input: DeriveInput = syn::parse_quote! {
14845 #[rustango(table = "m")]
14846 struct Message {
14847 #[rustango(primary_key)] id: i64,
14848 sender: ForeignKey<User>,
14849 #[rustango(related_name = "+")] recipient: ForeignKey<User>,
14850 #[rustango(related_name = "+")] owner: ForeignKey<other::Account>,
14851 }
14852 };
14853 let out = expand(&input).expect("derives").to_string();
14854 assert_eq!(out.matches("message_set_pool").count(), 1, "{out}");
14855 assert!(!out.contains("impl other :: Account {"), "{out}");
14856 }
14857
14858 fn field_error(src: &str) -> Option<String> {
14859 let f: syn::FieldsNamed = syn::parse_str(&format!("{{ {src} }}")).unwrap();
14860 let field = f.named.into_iter().next().unwrap();
14861 match parse_field_attrs(&field) {
14862 Err(e) => Some(e.to_string()),
14863 Ok(_) => error_for(field),
14864 }
14865 }
14866
14867 /// Flag attrs take `= true` like `blank`; `= false` is refused by name.
14868 #[test]
14869 fn flag_attrs_accept_eq_true() {
14870 for key in [
14871 "primary_key",
14872 "unique",
14873 "soft_delete",
14874 "index",
14875 "auto_now",
14876 "auto_now_add",
14877 ] {
14878 let f: syn::FieldsNamed =
14879 syn::parse_str(&format!("{{ #[rustango({key} = true)] a: i64 }}")).unwrap();
14880 let field = f.named.into_iter().next().unwrap();
14881 assert!(parse_field_attrs(&field).is_ok(), "{key} = true");
14882 let err = field_error(&format!("#[rustango({key} = false)] a: i64")).unwrap();
14883 assert!(err.contains(&format!("`{key} = false`")), "{err}");
14884 }
14885 let f: syn::Field =
14886 syn::parse_quote!(#[rustango(primary_key = true, unique = true)] a: i64);
14887 let attrs = parse_field_attrs(&f).unwrap();
14888 assert!(attrs.primary_key && attrs.unique);
14889 }
14890
14891 /// An unknown key is named, the nearest key suggested and the list shown.
14892 #[test]
14893 fn unknown_key_is_named_with_a_suggestion() {
14894 let err = field_error("#[rustango(max_lenght = 5)] a: String").unwrap();
14895 assert!(err.contains("`max_lenght`"), "{err}");
14896 assert!(err.contains("did you mean `max_length`"), "{err}");
14897 assert!(err.contains("Supported: column"), "{err}");
14898 let input: DeriveInput = syn::parse_quote! {
14899 #[rustango(tabel = "p")]
14900 struct P { id: i64 }
14901 };
14902 let err = parse_container_attrs(&input).err().unwrap().to_string();
14903 assert!(
14904 err.contains("`tabel`") && err.contains("did you mean `table`"),
14905 "{err}"
14906 );
14907 let err = field_error("#[rustango(zzzzzz)] a: i64").unwrap();
14908 assert!(!err.contains("did you mean"), "{err}");
14909 }
14910
14911 /// The listed keys are the parsed keys; a stale list would mislead.
14912 #[test]
14913 fn listed_keys_are_all_known() {
14914 for key in FIELD_ATTR_KEYS {
14915 let err = field_error(&format!("#[rustango({key})] a: i64")).unwrap_or_default();
14916 assert!(!err.contains("unknown rustango field"), "{key}: {err}");
14917 }
14918 for key in CONTAINER_ATTR_KEYS {
14919 let input: DeriveInput =
14920 syn::parse_str(&format!("#[rustango({key})] struct P {{ id: i64 }}")).unwrap();
14921 let err = parse_container_attrs(&input)
14922 .err()
14923 .map(|e| e.to_string())
14924 .unwrap_or_default();
14925 assert!(!err.contains("unknown rustango container"), "{key}: {err}");
14926 }
14927 }
14928
14929 /// `default` is raw SQL: a bare word or a non-number on a numeric field is refused.
14930 #[test]
14931 fn default_shapes_only_sqlite_takes_are_refused() {
14932 for bad in [
14933 r#"#[rustango(default = "draft")] s: String"#,
14934 r#"#[rustango(default = "draft")] s: Option<String>"#,
14935 r#"#[rustango(default = "abc")] n: i32"#,
14936 r#"#[rustango(default = "'abc'")] n: i64"#,
14937 r#"#[rustango(default = "true")] n: i32"#,
14938 r#"#[rustango(default = "")] n: i32"#,
14939 r#"#[rustango(default = "'x'")] n: f64"#,
14940 ] {
14941 assert!(field_error(bad).is_some(), "{bad} must be refused");
14942 }
14943 let err = field_error(r#"#[rustango(default = "draft")] s: String"#).unwrap();
14944 assert!(err.contains(r#"default = "'draft'""#), "{err}");
14945 // Valid on Postgres alone, so the message says that, not "a column".
14946 let err = field_error(r#"#[rustango(default = "current_user")] s: String"#).unwrap();
14947 assert!(
14948 err.contains("Postgres only") && !err.contains("column"),
14949 "{err}"
14950 );
14951 for good in [
14952 r#"#[rustango(default = "'draft'")] s: String"#,
14953 r#"#[rustango(default = "")] s: String"#,
14954 r#"#[rustango(default = "now()")] s: String"#,
14955 r#"#[rustango(default = "CURRENT_TIMESTAMP")] s: String"#,
14956 r#"#[rustango(default = "NULL")] s: Option<String>"#,
14957 r#"#[rustango(default = "0")] n: i32"#,
14958 r#"#[rustango(default = "-1")] n: i64"#,
14959 r#"#[rustango(default = "'5'")] n: i64"#,
14960 r#"#[rustango(default = "(1 + 2)")] n: i64"#,
14961 r#"#[rustango(default = "1.5")] n: f64"#,
14962 r#"#[rustango(default = "true")] b: bool"#,
14963 ] {
14964 assert_eq!(field_error(good), None, "{good}");
14965 }
14966 }
14967
14968 /// A raw ident derives names without `r#`; this used to panic (#1937).
14969 #[test]
14970 fn raw_idents_derive_plain_names() {
14971 let ident: syn::Ident = syn::parse_quote!(r#type);
14972 assert_eq!(column_type_ident(&ident).to_string(), "type_col");
14973 assert_eq!(ident_from_name("type", ident.span()).to_string(), "r#type");
14974 }
14975}
14976
14977#[cfg(test)]
14978mod model_attr_name_tests {
14979 use super::*;
14980
14981 fn container_error(item: proc_macro2::TokenStream) -> Option<String> {
14982 let input: DeriveInput = syn::parse2(item).unwrap();
14983 parse_container_attrs(&input).err().map(|e| e.to_string())
14984 }
14985
14986 fn field_error(field: proc_macro2::TokenStream) -> Option<String> {
14987 let f: syn::FieldsNamed = syn::parse2(quote!({ #field })).unwrap();
14988 let field = f.named.into_iter().next().unwrap();
14989 parse_field_attrs(&field).err().map(|e| e.to_string())
14990 }
14991
14992 /// `#[rgo(...)]` reads exactly as `#[rustango(...)]`.
14993 #[test]
14994 fn rgo_is_read_like_rustango() {
14995 let long: DeriveInput = syn::parse_quote! {
14996 #[rustango(table = "t", display = "name")] struct S;
14997 };
14998 let short: DeriveInput = syn::parse_quote! {
14999 #[rgo(table = "t", display = "name")] struct S;
15000 };
15001 let (long, short) = (
15002 parse_container_attrs(&long).unwrap(),
15003 parse_container_attrs(&short).unwrap(),
15004 );
15005 assert_eq!(long.table, short.table);
15006 assert_eq!(short.table.as_deref(), Some("t"));
15007 assert_eq!(long.display.map(|d| d.0), short.display.map(|d| d.0));
15008 }
15009
15010 #[test]
15011 fn mixing_names_on_one_item_or_field_is_refused() {
15012 let err = container_error(quote! {
15013 #[rustango(table = "t")] #[rgo(display = "name")] struct S;
15014 })
15015 .expect("a mix on an item must fail");
15016 assert!(
15017 err.contains("`#[rustango(...)]` and `#[rgo(...)]`"),
15018 "{err}"
15019 );
15020 let err = field_error(quote! { #[rgo(primary_key)] #[rustango(column = "c")] a: i64 })
15021 .expect("a mix on a field must fail");
15022 assert!(
15023 err.contains("`#[rgo(...)]` and `#[rustango(...)]`"),
15024 "{err}"
15025 );
15026 // Either name repeated alone is fine.
15027 assert_eq!(
15028 container_error(quote! { #[rgo(table = "t")] #[rgo(display = "name")] struct S; }),
15029 None
15030 );
15031 assert_eq!(
15032 field_error(quote! { #[rustango(primary_key)] #[rustango(column = "c")] a: i64 }),
15033 None
15034 );
15035 }
15036
15037 /// A hint quotes the attribute under the name the user wrote.
15038 #[test]
15039 fn hints_use_the_written_name() {
15040 let err = container_error(quote! { #[rgo(table = "t", unique_together = "a")] struct S; })
15041 .expect("one column must fail");
15042 assert!(err.contains("#[rgo(unique)]"), "{err}");
15043 let err =
15044 container_error(quote! { #[rustango(table = "t", index_together = "a")] struct S; })
15045 .expect("one column must fail");
15046 assert!(err.contains("#[rustango(index)]"), "{err}");
15047 }
15048}