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 _batches = #root::sql::__rustango_in_batches::<Self>(pool, _values)?;
5285 let _split = _batches.len() > 1;
5286 let mut _rows = ::std::vec::Vec::new();
5287 for _keys in _batches {
5288 _rows.extend(
5289 #root::query::QuerySet::<Self>::default()
5290 .filter(&_key, #root::core::SqlValue::List(_keys))
5291 .fetch(pool)
5292 .await?,
5293 );
5294 }
5295 if _split {
5296 _rows = #root::sql::__rustango_dedupe_by_pk(_rows);
5297 }
5298 ::core::result::Result::Ok(_rows)
5299 }
5300
5301 /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
5302 /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
5303 /// `vals` returns every row (matches SQL's empty-NOT-IN
5304 /// semantics — vacuously true for every row).
5305 ///
5306 /// # Errors
5307 /// As [`FetcherPool::fetch`]; `ExecError::InListUnsplittable`
5308 /// when `vals` pass the backend's bind cap.
5309 ///
5310 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5311 pub async fn where_not_in<V>(
5312 col: &str,
5313 vals: impl ::core::iter::IntoIterator<Item = V>,
5314 pool: &#root::sql::Pool,
5315 ) -> ::core::result::Result<
5316 ::std::vec::Vec<Self>,
5317 #root::sql::ExecError,
5318 >
5319 where
5320 V: ::core::convert::Into<#root::core::SqlValue>,
5321 {
5322 use #root::sql::FetcherPool as _;
5323 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5324 vals.into_iter().map(::core::convert::Into::into).collect();
5325 if _values.is_empty() {
5326 return #root::query::QuerySet::<Self>::default()
5327 .fetch(pool)
5328 .await;
5329 }
5330 let _key = ::std::format!("{}__not_in", col);
5331 // A `NOT IN` cannot be split: past the bind cap it is refused.
5332 let _values = #root::sql::__rustango_one_in_list::<Self>(pool, _values)?;
5333 #root::query::QuerySet::<Self>::default()
5334 .filter(&_key, #root::core::SqlValue::List(_values))
5335 .fetch(pool)
5336 .await
5337 }
5338
5339 /// Fetch every row where `<col> IS NULL`. Eloquent
5340 /// `Model::whereNull($col)->get()` parity.
5341 ///
5342 /// # Errors
5343 /// As [`FetcherPool::fetch`].
5344 ///
5345 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5346 pub async fn where_null(
5347 col: &str,
5348 pool: &#root::sql::Pool,
5349 ) -> ::core::result::Result<
5350 ::std::vec::Vec<Self>,
5351 #root::sql::ExecError,
5352 > {
5353 use #root::sql::FetcherPool as _;
5354 let _key = ::std::format!("{}__isnull", col);
5355 #root::query::QuerySet::<Self>::default()
5356 .filter(&_key, #root::core::SqlValue::Bool(true))
5357 .fetch(pool)
5358 .await
5359 }
5360
5361 /// Fetch every row where `<col> IS NOT NULL`. Eloquent
5362 /// `Model::whereNotNull($col)->get()` parity.
5363 ///
5364 /// # Errors
5365 /// As [`FetcherPool::fetch`].
5366 ///
5367 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5368 pub async fn where_not_null(
5369 col: &str,
5370 pool: &#root::sql::Pool,
5371 ) -> ::core::result::Result<
5372 ::std::vec::Vec<Self>,
5373 #root::sql::ExecError,
5374 > {
5375 use #root::sql::FetcherPool as _;
5376 let _key = ::std::format!("{}__isnull", col);
5377 #root::query::QuerySet::<Self>::default()
5378 .filter(&_key, #root::core::SqlValue::Bool(false))
5379 .fetch(pool)
5380 .await
5381 }
5382
5383 /// Fetch up to `n` rows in random order. Eloquent
5384 /// `Model::inRandomOrder()->limit($n)->get()` /
5385 /// `Model::query()->inRandomOrder()->get()->take($n)`
5386 /// parity. **Performance caveat**: random ordering
5387 /// forces a full table scan + per-row random key sort;
5388 /// the optimizer cannot use an index. Prefer a
5389 /// `pk >= rand_offset LIMIT N` walk for huge tables.
5390 ///
5391 /// # Errors
5392 /// As [`FetcherPool::fetch`].
5393 ///
5394 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5395 pub async fn random_n(
5396 n: i64,
5397 pool: &#root::sql::Pool,
5398 ) -> ::core::result::Result<
5399 ::std::vec::Vec<Self>,
5400 #root::sql::ExecError,
5401 > {
5402 use #root::sql::FetcherPool as _;
5403 #root::query::QuerySet::<Self>::default()
5404 .order_random()
5405 .limit(n)
5406 .fetch(pool)
5407 .await
5408 }
5409
5410 /// Fetch one row in random order. Eloquent
5411 /// `Model::inRandomOrder()->first()` parity. Same
5412 /// performance caveat as [`Self::random_n`].
5413 ///
5414 /// # Errors
5415 /// As [`FetcherPool::fetch`].
5416 ///
5417 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5418 pub async fn random(
5419 pool: &#root::sql::Pool,
5420 ) -> ::core::result::Result<
5421 ::core::option::Option<Self>,
5422 #root::sql::ExecError,
5423 > {
5424 ::core::result::Result::Ok(
5425 Self::random_n(1, pool).await?.into_iter().next(),
5426 )
5427 }
5428
5429 /// Fetch every row ordered ASC by `field`. Eloquent
5430 /// `Model::oldest($field)->get()` parity — the multi-row
5431 /// counterpart of [`Self::earliest`].
5432 ///
5433 /// # Errors
5434 /// As [`FetcherPool::fetch`].
5435 ///
5436 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5437 pub async fn oldest(
5438 field: &str,
5439 pool: &#root::sql::Pool,
5440 ) -> ::core::result::Result<
5441 ::std::vec::Vec<Self>,
5442 #root::sql::ExecError,
5443 > {
5444 use #root::sql::FetcherPool as _;
5445 #root::query::QuerySet::<Self>::default()
5446 .order_by(&[(field, false)])
5447 .fetch(pool)
5448 .await
5449 }
5450
5451 /// Fetch every row ordered DESC by `field`. Eloquent
5452 /// `Model::latest($field)->get()` parity — the multi-row
5453 /// counterpart of [`Self::latest`].
5454 ///
5455 /// # Errors
5456 /// As [`FetcherPool::fetch`].
5457 ///
5458 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5459 pub async fn newest(
5460 field: &str,
5461 pool: &#root::sql::Pool,
5462 ) -> ::core::result::Result<
5463 ::std::vec::Vec<Self>,
5464 #root::sql::ExecError,
5465 > {
5466 use #root::sql::FetcherPool as _;
5467 #root::query::QuerySet::<Self>::default()
5468 .order_by(&[(field, true)])
5469 .fetch(pool)
5470 .await
5471 }
5472
5473 /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5474 /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5475 /// Routes through the existing `__year` lookup suffix
5476 /// (issue #829).
5477 ///
5478 /// # Errors
5479 /// As [`FetcherPool::fetch`].
5480 ///
5481 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5482 pub async fn where_year(
5483 col: &str,
5484 year: i64,
5485 pool: &#root::sql::Pool,
5486 ) -> ::core::result::Result<
5487 ::std::vec::Vec<Self>,
5488 #root::sql::ExecError,
5489 > {
5490 use #root::sql::FetcherPool as _;
5491 let _key = ::std::format!("{}__year", col);
5492 #root::query::QuerySet::<Self>::default()
5493 .filter(&_key, #root::core::SqlValue::I64(year))
5494 .fetch(pool)
5495 .await
5496 }
5497
5498 /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5499 /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5500 /// `month` is 1–12.
5501 ///
5502 /// # Errors
5503 /// As [`FetcherPool::fetch`].
5504 ///
5505 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5506 pub async fn where_month(
5507 col: &str,
5508 month: i64,
5509 pool: &#root::sql::Pool,
5510 ) -> ::core::result::Result<
5511 ::std::vec::Vec<Self>,
5512 #root::sql::ExecError,
5513 > {
5514 use #root::sql::FetcherPool as _;
5515 let _key = ::std::format!("{}__month", col);
5516 #root::query::QuerySet::<Self>::default()
5517 .filter(&_key, #root::core::SqlValue::I64(month))
5518 .fetch(pool)
5519 .await
5520 }
5521
5522 /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5523 /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5524 /// `day` is 1–31.
5525 ///
5526 /// # Errors
5527 /// As [`FetcherPool::fetch`].
5528 ///
5529 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5530 pub async fn where_day(
5531 col: &str,
5532 day: i64,
5533 pool: &#root::sql::Pool,
5534 ) -> ::core::result::Result<
5535 ::std::vec::Vec<Self>,
5536 #root::sql::ExecError,
5537 > {
5538 use #root::sql::FetcherPool as _;
5539 let _key = ::std::format!("{}__day", col);
5540 #root::query::QuerySet::<Self>::default()
5541 .filter(&_key, #root::core::SqlValue::I64(day))
5542 .fetch(pool)
5543 .await
5544 }
5545
5546 /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5547 /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5548 /// `hour` is 0–23.
5549 ///
5550 /// # Errors
5551 /// As [`FetcherPool::fetch`].
5552 ///
5553 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5554 pub async fn where_hour(
5555 col: &str,
5556 hour: i64,
5557 pool: &#root::sql::Pool,
5558 ) -> ::core::result::Result<
5559 ::std::vec::Vec<Self>,
5560 #root::sql::ExecError,
5561 > {
5562 use #root::sql::FetcherPool as _;
5563 let _key = ::std::format!("{}__hour", col);
5564 #root::query::QuerySet::<Self>::default()
5565 .filter(&_key, #root::core::SqlValue::I64(hour))
5566 .fetch(pool)
5567 .await
5568 }
5569
5570 /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5571 /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5572 /// `minute` is 0–59.
5573 ///
5574 /// # Errors
5575 /// As [`FetcherPool::fetch`].
5576 ///
5577 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5578 pub async fn where_minute(
5579 col: &str,
5580 minute: i64,
5581 pool: &#root::sql::Pool,
5582 ) -> ::core::result::Result<
5583 ::std::vec::Vec<Self>,
5584 #root::sql::ExecError,
5585 > {
5586 use #root::sql::FetcherPool as _;
5587 let _key = ::std::format!("{}__minute", col);
5588 #root::query::QuerySet::<Self>::default()
5589 .filter(&_key, #root::core::SqlValue::I64(minute))
5590 .fetch(pool)
5591 .await
5592 }
5593
5594 /// Fetch every row where `<col> LIKE <pattern>` —
5595 /// caller-supplied pattern (must include `%` / `_`
5596 /// wildcards manually). Eloquent `Model::whereLike`
5597 /// parity.
5598 ///
5599 /// # Errors
5600 /// As [`FetcherPool::fetch`].
5601 ///
5602 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5603 pub async fn where_like(
5604 col: &str,
5605 pattern: impl ::core::convert::Into<::std::string::String>,
5606 pool: &#root::sql::Pool,
5607 ) -> ::core::result::Result<
5608 ::std::vec::Vec<Self>,
5609 #root::sql::ExecError,
5610 > {
5611 use #root::sql::FetcherPool as _;
5612 let _key = ::std::format!("{}__like", col);
5613 #root::query::QuerySet::<Self>::default()
5614 .filter(
5615 &_key,
5616 #root::core::SqlValue::String(pattern.into()),
5617 )
5618 .fetch(pool)
5619 .await
5620 }
5621
5622 /// Fetch every row where `<col> ILIKE <pattern>` —
5623 /// case-insensitive LIKE (PG native, MySQL/SQLite
5624 /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5625 ///
5626 /// # Errors
5627 /// As [`FetcherPool::fetch`].
5628 ///
5629 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5630 pub async fn where_ilike(
5631 col: &str,
5632 pattern: impl ::core::convert::Into<::std::string::String>,
5633 pool: &#root::sql::Pool,
5634 ) -> ::core::result::Result<
5635 ::std::vec::Vec<Self>,
5636 #root::sql::ExecError,
5637 > {
5638 use #root::sql::FetcherPool as _;
5639 let _key = ::std::format!("{}__ilike", col);
5640 #root::query::QuerySet::<Self>::default()
5641 .filter(
5642 &_key,
5643 #root::core::SqlValue::String(pattern.into()),
5644 )
5645 .fetch(pool)
5646 .await
5647 }
5648
5649 /// Fetch every row where `<col>` starts with `prefix`
5650 /// (auto-appends `%`). Eloquent
5651 /// `whereLike("col", "$prefix%")` parity.
5652 ///
5653 /// # Errors
5654 /// As [`FetcherPool::fetch`].
5655 ///
5656 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5657 pub async fn where_starts_with(
5658 col: &str,
5659 prefix: impl ::core::convert::Into<::std::string::String>,
5660 pool: &#root::sql::Pool,
5661 ) -> ::core::result::Result<
5662 ::std::vec::Vec<Self>,
5663 #root::sql::ExecError,
5664 > {
5665 use #root::sql::FetcherPool as _;
5666 let _key = ::std::format!("{}__startswith", col);
5667 #root::query::QuerySet::<Self>::default()
5668 .filter(
5669 &_key,
5670 #root::core::SqlValue::String(prefix.into()),
5671 )
5672 .fetch(pool)
5673 .await
5674 }
5675
5676 /// Fetch every row where `<col>` ends with `suffix`
5677 /// (auto-prepends `%`). Eloquent
5678 /// `whereLike("col", "%$suffix")` parity.
5679 ///
5680 /// # Errors
5681 /// As [`FetcherPool::fetch`].
5682 ///
5683 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5684 pub async fn where_ends_with(
5685 col: &str,
5686 suffix: impl ::core::convert::Into<::std::string::String>,
5687 pool: &#root::sql::Pool,
5688 ) -> ::core::result::Result<
5689 ::std::vec::Vec<Self>,
5690 #root::sql::ExecError,
5691 > {
5692 use #root::sql::FetcherPool as _;
5693 let _key = ::std::format!("{}__endswith", col);
5694 #root::query::QuerySet::<Self>::default()
5695 .filter(
5696 &_key,
5697 #root::core::SqlValue::String(suffix.into()),
5698 )
5699 .fetch(pool)
5700 .await
5701 }
5702
5703 /// Fetch every row where `<col>` contains `substr`
5704 /// (auto-wraps with `%`).
5705 /// Eloquent `whereLike("col", "%$substr%")` parity.
5706 ///
5707 /// # Errors
5708 /// As [`FetcherPool::fetch`].
5709 ///
5710 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5711 pub async fn where_contains(
5712 col: &str,
5713 substr: impl ::core::convert::Into<::std::string::String>,
5714 pool: &#root::sql::Pool,
5715 ) -> ::core::result::Result<
5716 ::std::vec::Vec<Self>,
5717 #root::sql::ExecError,
5718 > {
5719 use #root::sql::FetcherPool as _;
5720 let _key = ::std::format!("{}__contains", col);
5721 #root::query::QuerySet::<Self>::default()
5722 .filter(
5723 &_key,
5724 #root::core::SqlValue::String(substr.into()),
5725 )
5726 .fetch(pool)
5727 .await
5728 }
5729
5730 /// Fetch every row where `<col> > val`. Eloquent
5731 /// `Model::where($col, ">", $val)->get()` parity.
5732 ///
5733 /// # Errors
5734 /// As [`FetcherPool::fetch`].
5735 ///
5736 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5737 pub async fn where_gt(
5738 col: &str,
5739 val: impl ::core::convert::Into<#root::core::SqlValue>,
5740 pool: &#root::sql::Pool,
5741 ) -> ::core::result::Result<
5742 ::std::vec::Vec<Self>,
5743 #root::sql::ExecError,
5744 > {
5745 use #root::sql::FetcherPool as _;
5746 let _key = ::std::format!("{}__gt", col);
5747 #root::query::QuerySet::<Self>::default()
5748 .filter(&_key, val)
5749 .fetch(pool)
5750 .await
5751 }
5752
5753 /// Fetch every row where `<col> >= val`. Eloquent
5754 /// `Model::where($col, ">=", $val)->get()` parity.
5755 ///
5756 /// # Errors
5757 /// As [`FetcherPool::fetch`].
5758 ///
5759 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5760 pub async fn where_gte(
5761 col: &str,
5762 val: impl ::core::convert::Into<#root::core::SqlValue>,
5763 pool: &#root::sql::Pool,
5764 ) -> ::core::result::Result<
5765 ::std::vec::Vec<Self>,
5766 #root::sql::ExecError,
5767 > {
5768 use #root::sql::FetcherPool as _;
5769 let _key = ::std::format!("{}__gte", col);
5770 #root::query::QuerySet::<Self>::default()
5771 .filter(&_key, val)
5772 .fetch(pool)
5773 .await
5774 }
5775
5776 /// Fetch every row where `<col> < val`. Eloquent
5777 /// `Model::where($col, "<", $val)->get()` parity.
5778 ///
5779 /// # Errors
5780 /// As [`FetcherPool::fetch`].
5781 ///
5782 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5783 pub async fn where_lt(
5784 col: &str,
5785 val: impl ::core::convert::Into<#root::core::SqlValue>,
5786 pool: &#root::sql::Pool,
5787 ) -> ::core::result::Result<
5788 ::std::vec::Vec<Self>,
5789 #root::sql::ExecError,
5790 > {
5791 use #root::sql::FetcherPool as _;
5792 let _key = ::std::format!("{}__lt", col);
5793 #root::query::QuerySet::<Self>::default()
5794 .filter(&_key, val)
5795 .fetch(pool)
5796 .await
5797 }
5798
5799 /// Fetch every row where `<col> <= val`. Eloquent
5800 /// `Model::where($col, "<=", $val)->get()` parity.
5801 ///
5802 /// # Errors
5803 /// As [`FetcherPool::fetch`].
5804 ///
5805 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5806 pub async fn where_lte(
5807 col: &str,
5808 val: impl ::core::convert::Into<#root::core::SqlValue>,
5809 pool: &#root::sql::Pool,
5810 ) -> ::core::result::Result<
5811 ::std::vec::Vec<Self>,
5812 #root::sql::ExecError,
5813 > {
5814 use #root::sql::FetcherPool as _;
5815 let _key = ::std::format!("{}__lte", col);
5816 #root::query::QuerySet::<Self>::default()
5817 .filter(&_key, val)
5818 .fetch(pool)
5819 .await
5820 }
5821
5822 /// Fetch every row where `<col> <> val`. Eloquent
5823 /// `Model::where($col, "!=", $val)->get()` parity.
5824 ///
5825 /// # Errors
5826 /// As [`FetcherPool::fetch`].
5827 ///
5828 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5829 pub async fn where_ne(
5830 col: &str,
5831 val: impl ::core::convert::Into<#root::core::SqlValue>,
5832 pool: &#root::sql::Pool,
5833 ) -> ::core::result::Result<
5834 ::std::vec::Vec<Self>,
5835 #root::sql::ExecError,
5836 > {
5837 use #root::sql::FetcherPool as _;
5838 let _key = ::std::format!("{}__ne", col);
5839 #root::query::QuerySet::<Self>::default()
5840 .filter(&_key, val)
5841 .fetch(pool)
5842 .await
5843 }
5844
5845 /// Fetch every row matching `<col> = val` for ANY of the
5846 /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5847 /// parity. Empty `cols` returns no rows.
5848 ///
5849 /// Resolves each `&str` column to its SCHEMA-registered
5850 /// `&'static str` once and builds a single OR-composed
5851 /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5852 ///
5853 /// # Errors
5854 /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5855 /// when any column is not declared on the model.
5856 ///
5857 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5858 pub async fn where_any(
5859 cols: &[&str],
5860 val: impl ::core::convert::Into<#root::core::SqlValue>,
5861 pool: &#root::sql::Pool,
5862 ) -> ::core::result::Result<
5863 ::std::vec::Vec<Self>,
5864 #root::sql::ExecError,
5865 > {
5866 Self::__where_multi(cols, val, false, pool).await
5867 }
5868
5869 /// Fetch every row matching `<col> = val` for ALL listed
5870 /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5871 /// Empty `cols` returns every row (vacuous AND).
5872 ///
5873 /// # Errors
5874 /// As [`Self::where_any`].
5875 pub async fn where_all(
5876 cols: &[&str],
5877 val: impl ::core::convert::Into<#root::core::SqlValue>,
5878 pool: &#root::sql::Pool,
5879 ) -> ::core::result::Result<
5880 ::std::vec::Vec<Self>,
5881 #root::sql::ExecError,
5882 > {
5883 Self::__where_multi(cols, val, true, pool).await
5884 }
5885
5886 /// Internal: build a Q expression composing `cols` via
5887 /// AND (`all`) or OR (`!all`), then fetch. Backs
5888 /// `where_any` / `where_all`.
5889 #[doc(hidden)]
5890 pub async fn __where_multi(
5891 cols: &[&str],
5892 val: impl ::core::convert::Into<#root::core::SqlValue>,
5893 all: bool,
5894 pool: &#root::sql::Pool,
5895 ) -> ::core::result::Result<
5896 ::std::vec::Vec<Self>,
5897 #root::sql::ExecError,
5898 > {
5899 #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5900 .await
5901 }
5902
5903 /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5904 /// parity. PK-ordered
5905 /// is NOT guaranteed without an explicit `order_by` —
5906 /// drop into `Self::query()` for that.
5907 ///
5908 /// # Errors
5909 /// As [`FetcherPool::fetch`].
5910 ///
5911 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5912 pub async fn take(
5913 n: i64,
5914 pool: &#root::sql::Pool,
5915 ) -> ::core::result::Result<
5916 ::std::vec::Vec<Self>,
5917 #root::sql::ExecError,
5918 > {
5919 use #root::sql::FetcherPool as _;
5920 #root::query::QuerySet::<Self>::default()
5921 .limit(n)
5922 .fetch(pool)
5923 .await
5924 }
5925
5926 /// Fetch the page-th window of `per_page` rows
5927 /// (1-indexed). Eloquent
5928 /// `Model::query()->forPage($page, $perPage)->get()`
5929 /// parity. The DB scans an `OFFSET (page - 1) * per_page
5930 /// LIMIT per_page`; for large offsets this is O(N) —
5931 /// prefer keyset pagination via PK on hot paths.
5932 ///
5933 /// # Errors
5934 /// As [`FetcherPool::fetch`].
5935 ///
5936 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5937 pub async fn for_page(
5938 page: i64,
5939 per_page: i64,
5940 pool: &#root::sql::Pool,
5941 ) -> ::core::result::Result<
5942 ::std::vec::Vec<Self>,
5943 #root::sql::ExecError,
5944 > {
5945 use #root::sql::FetcherPool as _;
5946 let _offset = #root::list_params::page_offset(page, per_page);
5947 #root::query::QuerySet::<Self>::default()
5948 .limit(per_page)
5949 .offset(_offset)
5950 .fetch(pool)
5951 .await
5952 }
5953
5954 /// Eloquent `Model::paginate($per_page, $page)` — fetch
5955 /// one page of rows AND the total row count in one
5956 /// call. Returns `(rows, total)`. Two queries: a
5957 /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5958 /// for the total.
5959 ///
5960 /// Useful for paginated UIs that need both the visible
5961 /// rows AND a "Page X of Y" / total-count widget. For
5962 /// large tables prefer keyset pagination + cached count;
5963 /// every call to `paginate` re-counts the full table.
5964 ///
5965 /// Same 1-indexed `page` convention as [`Self::for_page`].
5966 ///
5967 /// # Errors
5968 /// As [`Self::for_page`] and [`Self::count`].
5969 pub async fn paginate(
5970 page: i64,
5971 per_page: i64,
5972 pool: &#root::sql::Pool,
5973 ) -> ::core::result::Result<
5974 (::std::vec::Vec<Self>, i64),
5975 #root::sql::ExecError,
5976 > {
5977 // Route through the queryset rather than `Self::count`:
5978 // the `count` inherent method is suppressed on models
5979 // that declare a field named `count` (field/shortcut
5980 // collision guard), and `paginate` must still compile
5981 // for those models.
5982 let total = {
5983 use #root::sql::CounterPool as _;
5984 #root::query::QuerySet::<Self>::default()
5985 .count(pool)
5986 .await?
5987 };
5988 let rows = Self::for_page(page, per_page, pool).await?;
5989 ::core::result::Result::Ok((rows, total))
5990 }
5991
5992 /// Bulk-update — set `set_col = set_val` on every row
5993 /// matching `where_col = where_val`. Returns affected row
5994 /// count. Eloquent
5995 /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5996 /// parity.
5997 ///
5998 /// For multi-column updates drop into the queryset
5999 /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
6000 ///
6001 /// # Errors
6002 /// As [`UpdaterPool::execute_pool`].
6003 ///
6004 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
6005 pub async fn update_where(
6006 where_col: &str,
6007 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
6008 set_col: &str,
6009 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
6010 pool: &#root::sql::Pool,
6011 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6012 let _query = #root::query::QuerySet::<Self>::default()
6013 .filter(where_col, where_val)
6014 .update()
6015 .set(set_col, set_val)
6016 .compile()?;
6017 #bulk_update_run
6018 }
6019
6020 /// Bulk-delete — remove every row matching
6021 /// `where_col = where_val`. Returns affected row count.
6022 /// Eloquent
6023 /// `Model::where($where_col, $where_val)->delete()` parity.
6024 ///
6025 /// # Errors
6026 /// As [`#root::sql::delete_pool`].
6027 pub async fn delete_where(
6028 where_col: &str,
6029 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
6030 pool: &#root::sql::Pool,
6031 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6032 let _query = #root::query::QuerySet::<Self>::default()
6033 .filter_op(where_col, #root::core::Op::Eq, where_val)
6034 .compile_delete()?;
6035 #bulk_delete_run
6036 }
6037
6038 /// Bulk-update — set `set_col = set_val` on EVERY row of
6039 /// this model's table. **No WHERE clause** — use with
6040 /// care. Eloquent `Model::query()->update([$col => $val])`
6041 /// parity.
6042 ///
6043 /// Use for backfills, one-shot reset flows, etc. The
6044 /// returned count is rows affected.
6045 ///
6046 /// # Errors
6047 /// As [`UpdaterPool::execute_pool`].
6048 ///
6049 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
6050 pub async fn update_all(
6051 set_col: &str,
6052 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
6053 pool: &#root::sql::Pool,
6054 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6055 let _query = #root::query::QuerySet::<Self>::default()
6056 .update()
6057 .set(set_col, set_val)
6058 .compile()?;
6059 #bulk_update_run
6060 }
6061
6062 /// Fetch every row where `<col> NOT LIKE <pattern>`.
6063 /// Eloquent `Model::whereNotLike` parity. Pattern is
6064 /// passed verbatim — caller controls `%` / `_`.
6065 ///
6066 /// # Errors
6067 /// As [`FetcherPool::fetch`].
6068 ///
6069 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6070 pub async fn where_not_like(
6071 col: &str,
6072 pattern: impl ::core::convert::Into<::std::string::String>,
6073 pool: &#root::sql::Pool,
6074 ) -> ::core::result::Result<
6075 ::std::vec::Vec<Self>,
6076 #root::sql::ExecError,
6077 > {
6078 use #root::sql::FetcherPool as _;
6079 let _key = ::std::format!("{}__not_like", col);
6080 #root::query::QuerySet::<Self>::default()
6081 .filter(
6082 &_key,
6083 #root::core::SqlValue::String(pattern.into()),
6084 )
6085 .fetch(pool)
6086 .await
6087 }
6088
6089 /// Fetch every row where `<col> NOT ILIKE <pattern>` —
6090 /// case-insensitive `NOT LIKE` (PG native, MySQL /
6091 /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
6092 ///
6093 /// # Errors
6094 /// As [`FetcherPool::fetch`].
6095 ///
6096 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6097 pub async fn where_not_ilike(
6098 col: &str,
6099 pattern: impl ::core::convert::Into<::std::string::String>,
6100 pool: &#root::sql::Pool,
6101 ) -> ::core::result::Result<
6102 ::std::vec::Vec<Self>,
6103 #root::sql::ExecError,
6104 > {
6105 use #root::sql::FetcherPool as _;
6106 let _key = ::std::format!("{}__not_ilike", col);
6107 #root::query::QuerySet::<Self>::default()
6108 .filter(
6109 &_key,
6110 #root::core::SqlValue::String(pattern.into()),
6111 )
6112 .fetch(pool)
6113 .await
6114 }
6115
6116 /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
6117 /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
6118 /// parity.
6119 ///
6120 /// # Errors
6121 /// As [`FetcherPool::fetch`].
6122 ///
6123 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6124 pub async fn where_not_between(
6125 col: &str,
6126 lo: impl ::core::convert::Into<#root::core::SqlValue>,
6127 hi: impl ::core::convert::Into<#root::core::SqlValue>,
6128 pool: &#root::sql::Pool,
6129 ) -> ::core::result::Result<
6130 ::std::vec::Vec<Self>,
6131 #root::sql::ExecError,
6132 > {
6133 use #root::sql::FetcherPool as _;
6134 let _key = ::std::format!("{}__not_between", col);
6135 let _vals = #root::core::SqlValue::List(::std::vec![
6136 ::core::convert::Into::into(lo),
6137 ::core::convert::Into::into(hi),
6138 ]);
6139 #root::query::QuerySet::<Self>::default()
6140 .filter(&_key, _vals)
6141 .fetch(pool)
6142 .await
6143 }
6144
6145 /// Returns the SQL table name for this model. Eloquent
6146 /// `$model->getTable()` parity.
6147 #[must_use]
6148 pub fn table_name() -> &'static str {
6149 <Self as #root::core::Model>::SCHEMA.table
6150 }
6151
6152 /// Returns the SQL column name of this model's primary
6153 /// key, or `None` when the model has no
6154 /// `#[rustango(primary_key)]`. Eloquent
6155 /// `$model->getKeyName()` parity.
6156 #[must_use]
6157 pub fn primary_key_column() -> ::core::option::Option<&'static str> {
6158 <Self as #root::core::Model>::SCHEMA
6159 .primary_key()
6160 .map(|f| f.column)
6161 }
6162
6163 /// Fetch every row where `<col> BETWEEN lo AND hi`
6164 /// (inclusive on both ends — same as SQL). Eloquent
6165 /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
6166 ///
6167 /// # Errors
6168 /// As [`FetcherPool::fetch`].
6169 ///
6170 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6171 pub async fn where_between(
6172 col: &str,
6173 lo: impl ::core::convert::Into<#root::core::SqlValue>,
6174 hi: impl ::core::convert::Into<#root::core::SqlValue>,
6175 pool: &#root::sql::Pool,
6176 ) -> ::core::result::Result<
6177 ::std::vec::Vec<Self>,
6178 #root::sql::ExecError,
6179 > {
6180 use #root::sql::FetcherPool as _;
6181 let _key = ::std::format!("{}__between", col);
6182 let _vals = #root::core::SqlValue::List(::std::vec![
6183 ::core::convert::Into::into(lo),
6184 ::core::convert::Into::into(hi),
6185 ]);
6186 #root::query::QuerySet::<Self>::default()
6187 .filter(&_key, _vals)
6188 .fetch(pool)
6189 .await
6190 }
6191
6192 /// Fetch the first row where `<col> = <val>`. Returns
6193 /// `Ok(None)` when no row matches. Eloquent
6194 /// `Model::firstWhere($col, $val)` parity.
6195 ///
6196 /// Thin wrapper over `QuerySet::<Self>::default()
6197 /// .filter(col, val).first(pool)`. Use this when you
6198 /// want one row identified by a non-PK column (e.g.
6199 /// `User::first_where_pool("email", "x@y.com", &pool)`).
6200 ///
6201 /// `val` accepts any value `Into<SqlValue>` so plain
6202 /// strings, ints, UUIDs, etc. all work.
6203 ///
6204 /// # Errors
6205 /// As `QuerySet::first`.
6206 pub async fn first_where(
6207 col: &str,
6208 val: impl ::core::convert::Into<#root::core::SqlValue>,
6209 pool: &#root::sql::Pool,
6210 ) -> ::core::result::Result<
6211 ::core::option::Option<Self>,
6212 #root::sql::ExecError,
6213 > {
6214 #root::query::QuerySet::<Self>::default()
6215 .filter(col, val)
6216 .first(pool)
6217 .await
6218 }
6219
6220 #value_method
6221
6222 /// Fetch the row with the largest non-NULL `field` value.
6223 /// Returns `Ok(None)` when the table is empty or every
6224 /// `field` is NULL. Thin wrapper over
6225 /// `QuerySet::<Self>::default().latest(field, pool)`.
6226 ///
6227 /// **Field name** is the Rust field ident as a string
6228 /// (not the SQL column). It is a runtime `&str`, so an
6229 /// unknown one compiles and surfaces as
6230 /// `ExecError::Query(QueryError::UnknownField)` when the
6231 /// query runs.
6232 ///
6233 /// # Errors
6234 /// As `QuerySet::latest`.
6235 pub async fn latest(
6236 field: &str,
6237 pool: &#root::sql::Pool,
6238 ) -> ::core::result::Result<
6239 ::core::option::Option<Self>,
6240 #root::sql::ExecError,
6241 > {
6242 #root::query::QuerySet::<Self>::default()
6243 .latest(field, pool)
6244 .await
6245 }
6246
6247 /// Sibling of [`Self::latest`]: fetches the row with the
6248 /// smallest non-NULL `field` value.
6249 ///
6250 /// # Errors
6251 /// As [`Self::latest`].
6252 pub async fn earliest(
6253 field: &str,
6254 pool: &#root::sql::Pool,
6255 ) -> ::core::result::Result<
6256 ::core::option::Option<Self>,
6257 #root::sql::ExecError,
6258 > {
6259 #root::query::QuerySet::<Self>::default()
6260 .earliest(field, pool)
6261 .await
6262 }
6263
6264 #count_method
6265
6266 /// `true` when the table contains at least one row.
6267 /// Eloquent `Model::query()->exists()` parity. Thin wrapper
6268 /// over `QuerySet::<Self>::default().exists(pool)`.
6269 ///
6270 /// # Errors
6271 /// As [`ExistsPool::exists`].
6272 ///
6273 /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
6274 pub async fn exists(
6275 pool: &#root::sql::Pool,
6276 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6277 use #root::sql::ExistsPool as _;
6278 #root::query::QuerySet::<Self>::default()
6279 .exists(pool)
6280 .await
6281 }
6282
6283 /// Inverse of [`Self::exists`] — returns `true` when
6284 /// the table has zero rows. Eloquent
6285 /// `Model::doesntExist()` parity.
6286 ///
6287 /// # Errors
6288 /// As [`Self::exists`].
6289 pub async fn doesnt_exist(
6290 pool: &#root::sql::Pool,
6291 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6292 Self::exists(pool).await.map(|e| !e)
6293 }
6294
6295 /// Eloquent `Model::query()->whereKey($pk)->exists()` —
6296 /// `true` when a row with primary key `pk` exists in
6297 /// the table. Sugar over
6298 /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
6299 ///
6300 /// Differs from [`Self::exists`] (which checks "any row
6301 /// in the table") by checking a specific PK existence.
6302 /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
6303 /// because the row is never materialized — the SQL is
6304 /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
6305 ///
6306 /// # Errors
6307 /// As [`#root::sql::ExistsPool::contains_pk`].
6308 pub async fn contains_pk(
6309 pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
6310 pool: &#root::sql::Pool,
6311 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
6312 use #root::sql::ExistsPool as _;
6313 #root::query::QuerySet::<Self>::default()
6314 .contains_pk(pool, pk)
6315 .await
6316 }
6317
6318 #sum_method
6319 #avg_method
6320 #min_method
6321 #max_method
6322
6323 /// Fetch every row of this model from `pool`. Eloquent
6324 /// `Model::all()` parity — a thin wrapper over
6325 /// `QuerySet::<Self>::default().fetch(pool)`.
6326 ///
6327 /// **Use with care on large tables** — there's no
6328 /// pagination or limit; the entire table is materialized
6329 /// into memory. For anything beyond fixture / lookup
6330 /// tables, page through `QuerySet::<Self>::default()
6331 /// .order_by(...).limit(N).offset(M).fetch(pool)`
6332 /// or stream via `.iterator(chunk_size)`.
6333 ///
6334 /// # Errors
6335 /// As [`FetcherPool::fetch`].
6336 ///
6337 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6338 pub async fn all(
6339 pool: &#root::sql::Pool,
6340 ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
6341 {
6342 use #root::sql::FetcherPool as _;
6343 #root::query::QuerySet::<Self>::default()
6344 .fetch(pool)
6345 .await
6346 }
6347
6348 /// Look up every row whose primary key is in `pks`.
6349 /// Returns the matching rows in **inventory** order — NOT
6350 /// the order of `pks`. Empty `pks` returns an empty
6351 /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
6352 /// with a list) parity.
6353 ///
6354 /// Thin wrapper over `QuerySet::<Self>::default()
6355 /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
6356 /// Caller-supplied PKs that don't match a row are
6357 /// silently skipped (the returned vec is shorter than
6358 /// the input list). For an order-preserving / "fail
6359 /// when any missing" variant, build the queryset
6360 /// explicitly with `in_bulk` instead.
6361 ///
6362 /// Accepts any iterable whose elements are
6363 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
6364 /// `Vec<Uuid>`, etc.
6365 ///
6366 /// # Errors
6367 /// As [`FetcherPool::fetch`].
6368 ///
6369 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6370 pub async fn find_many<V>(
6371 pks: impl ::core::iter::IntoIterator<Item = V>,
6372 pool: &#root::sql::Pool,
6373 ) -> ::core::result::Result<
6374 ::std::vec::Vec<Self>,
6375 #root::sql::ExecError,
6376 >
6377 where
6378 V: ::core::convert::Into<#root::core::SqlValue>,
6379 {
6380 use #root::sql::FetcherPool as _;
6381 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6382 pks.into_iter().map(::core::convert::Into::into).collect();
6383 if _values.is_empty() {
6384 return ::core::result::Result::Ok(::std::vec::Vec::new());
6385 }
6386 let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
6387 let _batches = #root::sql::__rustango_in_batches::<Self>(pool, _values)?;
6388 let _split = _batches.len() > 1;
6389 let mut _rows = ::std::vec::Vec::new();
6390 for _keys in _batches {
6391 _rows.extend(
6392 #root::query::QuerySet::<Self>::default()
6393 .filter(&_key, #root::core::SqlValue::List(_keys))
6394 .fetch(pool)
6395 .await?,
6396 );
6397 }
6398 if _split {
6399 _rows = #root::sql::__rustango_dedupe_by_pk(_rows);
6400 }
6401 ::core::result::Result::Ok(_rows)
6402 }
6403
6404 /// Look up the row whose primary key equals `pk`. Returns
6405 /// `Ok(None)` when no row matches, rather than erroring.
6406 /// Eloquent `Model::find`
6407 /// shape — accepts any value `Into<SqlValue>`.
6408 ///
6409 /// One-liner shortcut for the common
6410 /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6411 /// .limit(1).fetch(pool).await?.into_iter().next()`
6412 /// dance.
6413 ///
6414 /// # Errors
6415 /// As [`FetcherPool::fetch`].
6416 ///
6417 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6418 pub async fn find(
6419 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6420 pool: &#root::sql::Pool,
6421 ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6422 {
6423 use #root::sql::FetcherPool as _;
6424 let _pk_val: #root::core::SqlValue = pk.into();
6425 let mut _rows: ::std::vec::Vec<Self> =
6426 #root::query::QuerySet::<Self>::default()
6427 .filter(::core::stringify!(#pk_ident), _pk_val)
6428 .limit(1)
6429 .fetch(pool)
6430 .await?;
6431 ::core::result::Result::Ok(_rows.into_iter().next())
6432 }
6433
6434 /// Look up the row whose primary key equals `pk`. Errors
6435 /// when no row matches — the throwing counterpart of
6436 /// [`Self::find`]. Eloquent `Model::findOrFail`
6437 /// parity.
6438 ///
6439 /// Translates the miss into
6440 /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6441 /// so callers can `?`-bubble straight through the typical
6442 /// `ExecError` error chain.
6443 ///
6444 /// # Errors
6445 /// As [`Self::find`]; additionally
6446 /// [`sqlx::Error::RowNotFound`] when no row matches.
6447 ///
6448 /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6449 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6450 pub async fn find_or_fail(
6451 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6452 pool: &#root::sql::Pool,
6453 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6454 match Self::find(pk, pool).await? {
6455 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6456 ::core::option::Option::None => ::core::result::Result::Err(
6457 #root::sql::ExecError::Driver(
6458 #root::sql::sqlx::Error::RowNotFound,
6459 ),
6460 ),
6461 }
6462 }
6463
6464 /// Look up multiple rows by primary key, **failing** if
6465 /// any requested PK is missing. Eloquent
6466 /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6467 /// inventory order (NOT request order — same as
6468 /// [`Self::find_many`]).
6469 ///
6470 /// Empty `pks` returns an empty `Vec` (no rows requested
6471 /// → nothing to fail on).
6472 ///
6473 /// Differs from [`Self::find_many`] in that this method
6474 /// requires every PK to resolve. If even one is missing,
6475 /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6476 /// in `ExecError::Driver`). A key counts as found when the
6477 /// database matches it to a row, so repeats and keys its
6478 /// collation folds together (`abc` / `ABC` on MySQL) pass.
6479 ///
6480 /// # Errors
6481 /// As [`Self::find_many`], plus `RowNotFound` when a
6482 /// requested PK matches no row.
6483 pub async fn find_many_or_fail<V>(
6484 pks: impl ::core::iter::IntoIterator<Item = V>,
6485 pool: &#root::sql::Pool,
6486 ) -> ::core::result::Result<
6487 ::std::vec::Vec<Self>,
6488 #root::sql::ExecError,
6489 >
6490 where
6491 V: ::core::convert::Into<#root::core::SqlValue>,
6492 {
6493 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6494 pks.into_iter().map(::core::convert::Into::into).collect();
6495 if _values.is_empty() {
6496 return ::core::result::Result::Ok(::std::vec::Vec::new());
6497 }
6498 use #root::sql::ExistsPool as _;
6499 let mut _seen: ::std::collections::HashSet<
6500 ::std::string::String,
6501 > = ::std::collections::HashSet::new();
6502 let mut _keys: ::std::vec::Vec<#root::core::SqlValue> =
6503 ::std::vec::Vec::new();
6504 for v in &_values {
6505 if _seen.insert(v.to_display_string()) {
6506 _keys.push(::core::clone::Clone::clone(v));
6507 }
6508 }
6509 let _rows = Self::find_many(_values, pool).await?;
6510 let _found: ::std::collections::HashSet<::std::string::String> = _rows
6511 .iter()
6512 .map(|r| r.__rustango_pk_value().to_display_string())
6513 .collect();
6514 // A key with no exact twin among the rows may still match one
6515 // under the column's collation: the database decides.
6516 for _key in _keys {
6517 if _found.contains(&_key.to_display_string()) {
6518 continue;
6519 }
6520 let _hit = #root::query::QuerySet::<Self>::default()
6521 .filter(::core::stringify!(#pk_ident), _key)
6522 .exists(pool)
6523 .await?;
6524 if !_hit {
6525 return ::core::result::Result::Err(
6526 #root::sql::ExecError::Driver(
6527 #root::sql::sqlx::Error::RowNotFound,
6528 ),
6529 );
6530 }
6531 }
6532 ::core::result::Result::Ok(_rows)
6533 }
6534
6535 /// Find by primary key or run `fallback` to produce a
6536 /// default row to return. Eloquent
6537 /// `Model::findOr($pk, fn() => …)` parity.
6538 ///
6539 /// Unlike [`Self::find_or_fail`] (which raises on
6540 /// miss), this is the "give me something sensible"
6541 /// branch: typical use is "fetch the user's row, else
6542 /// fall back to an anonymous/guest stub".
6543 ///
6544 /// `fallback` runs only when no row matches — the DB
6545 /// round-trip happens unconditionally.
6546 ///
6547 /// # Errors
6548 /// As [`Self::find`].
6549 pub async fn find_or<F>(
6550 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6551 pool: &#root::sql::Pool,
6552 fallback: F,
6553 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6554 where
6555 F: ::core::ops::FnOnce() -> Self,
6556 {
6557 ::core::result::Result::Ok(
6558 Self::find(pk, pool).await?.unwrap_or_else(fallback),
6559 )
6560 }
6561
6562 /// Same as [`Self::find_or`] but also returns a `bool`
6563 /// indicating whether the row was found in the DB
6564 /// (`true`) or freshly built from `fallback` (`false`).
6565 /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6566 /// in PHP the returned model also exposes `->exists`;
6567 /// here that's surfaced as the second tuple element.
6568 ///
6569 /// Useful for edit-or-create form handlers where the
6570 /// caller needs to know whether to PATCH or POST when
6571 /// the user submits the form.
6572 ///
6573 /// # Errors
6574 /// As [`Self::find`].
6575 pub async fn find_or_new<F>(
6576 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6577 pool: &#root::sql::Pool,
6578 fallback: F,
6579 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6580 where
6581 F: ::core::ops::FnOnce() -> Self,
6582 {
6583 match Self::find(pk, pool).await? {
6584 ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6585 ::core::option::Option::None => {
6586 ::core::result::Result::Ok((fallback(), false))
6587 }
6588 }
6589 }
6590
6591 /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6592 /// [`Self::find_or_new`] but **persists** the new row
6593 /// when the PK isn't found. Returns `(row, exists: bool)`
6594 /// — `exists=true` when the PK was found,
6595 /// `false` when a fresh row was inserted.
6596 ///
6597 /// ```ignore
6598 /// let (post, found) = Post::find_or_insert(
6599 /// pk,
6600 /// &pool,
6601 /// || Post { id: Auto::default(), title: "new".into() },
6602 /// ).await?;
6603 /// // `found` true → returned existing row;
6604 /// // `found` false → fallback was inserted, `post.id` populated.
6605 /// ```
6606 ///
6607 /// **Caveat**: two concurrent calls that both miss the
6608 /// find can both INSERT, violating uniqueness. Wrap in a
6609 /// transaction or rely on a UNIQUE constraint + handle
6610 /// the conflict error if you need race-free semantics.
6611 ///
6612 /// # Errors
6613 /// As [`Self::find`] and [`Self::save_pool`].
6614 pub async fn find_or_insert<F>(
6615 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6616 pool: &#root::sql::Pool,
6617 fallback: F,
6618 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6619 where
6620 F: ::core::ops::FnOnce() -> Self,
6621 {
6622 if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6623 return ::core::result::Result::Ok((_row, true));
6624 }
6625 let mut _new = fallback();
6626 _new.save_pool(pool).await?;
6627 ::core::result::Result::Ok((_new, false))
6628 }
6629
6630 /// Fetch the first row of the table, or run `fallback`
6631 /// when the table is empty. Eloquent
6632 /// `Model::firstOr(fn() => …)` parity.
6633 ///
6634 /// # Errors
6635 /// As [`Self::first`].
6636 pub async fn first_or<F>(
6637 pool: &#root::sql::Pool,
6638 fallback: F,
6639 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6640 where
6641 F: ::core::ops::FnOnce() -> Self,
6642 {
6643 // Route through the queryset rather than `Self::first`,
6644 // which is suppressed on models with a field named
6645 // `first` (field/shortcut collision guard).
6646 ::core::result::Result::Ok(
6647 #root::query::QuerySet::<Self>::default()
6648 .first(pool)
6649 .await?
6650 .unwrap_or_else(fallback),
6651 )
6652 }
6653
6654 /// Fetch exactly one row matching `<col> = <val>`. Errors
6655 /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6656 /// or more than one matches
6657 /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6658 /// Eloquent `Model::sole($col, $val)` parity.
6659 ///
6660 /// # Errors
6661 /// As [`Self::where_`] plus the explicit
6662 /// `RowNotFound` / `MultipleRowsReturned` cases above.
6663 pub async fn sole(
6664 col: &str,
6665 val: impl ::core::convert::Into<#root::core::SqlValue>,
6666 pool: &#root::sql::Pool,
6667 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6668 let mut _rows = Self::where_(col, val, pool).await?;
6669 match _rows.len() {
6670 0 => ::core::result::Result::Err(
6671 #root::sql::ExecError::Driver(
6672 #root::sql::sqlx::Error::RowNotFound,
6673 ),
6674 ),
6675 1 => ::core::result::Result::Ok(_rows.remove(0)),
6676 n => ::core::result::Result::Err(
6677 #root::sql::ExecError::MultipleRowsReturned {
6678 op: "sole",
6679 table: <Self as #root::core::Model>::SCHEMA.name,
6680 count: n,
6681 },
6682 ),
6683 }
6684 }
6685 }
6686 } else {
6687 quote!()
6688 };
6689
6690 // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6691 // the non-audited `_pool` methods but execute against an open
6692 // `PoolTx` so the writes participate in the caller's transaction.
6693 // Auditing inside TX is deferred; these always use the plain
6694 // executor primitives regardless of whether the model is audited.
6695 let tx_insert_method = if fields.has_auto {
6696 let pushes = &fields.insert_pushes;
6697 let returning_cols = &fields.returning_cols;
6698 quote! {
6699 /// Insert this row inside an open transaction, populating
6700 /// any `Auto<T>` PK from the auto-assigned value. Works
6701 /// against any backend that `tx` wraps.
6702 ///
6703 /// # Errors
6704 /// As [`Self::insert_pool`].
6705 pub async fn insert_tx(
6706 &mut self,
6707 tx: &mut #root::sql::PoolTx<'_>,
6708 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6709 let mut _columns: ::std::vec::Vec<&'static str> =
6710 ::std::vec::Vec::new();
6711 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6712 ::std::vec::Vec::new();
6713 #( #pushes )*
6714 let _query = #root::core::InsertQuery::new(
6715 <Self as #root::core::Model>::SCHEMA,
6716 _columns,
6717 _values,
6718 )
6719 .returning(::std::vec![ #( #returning_cols ),* ]);
6720 let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6721 #root::sql::apply_auto_pk(_result, self)
6722 }
6723 }
6724 } else {
6725 let insert_columns = &fields.insert_columns;
6726 let insert_values = &fields.insert_values;
6727 quote! {
6728 /// Insert this row inside an open transaction.
6729 ///
6730 /// # Errors
6731 /// As [`Self::insert_pool`].
6732 pub async fn insert_tx(
6733 &self,
6734 tx: &mut #root::sql::PoolTx<'_>,
6735 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6736 let _query = #root::core::InsertQuery::new(
6737 <Self as #root::core::Model>::SCHEMA,
6738 ::std::vec![ #( #insert_columns ),* ],
6739 ::std::vec![ #( #insert_values ),* ],
6740 );
6741 #root::sql::insert_tx(tx, &_query).await
6742 }
6743 }
6744 };
6745
6746 let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6747 let pk_column_lit = pk_col.as_str();
6748 let assignments = &fields.update_assignments;
6749 let dispatch_unset = if fields.pk_is_auto {
6750 quote! {
6751 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6752 return self.insert_tx(tx).await.map(|()| 1u64);
6753 }
6754 }
6755 } else {
6756 quote!()
6757 };
6758 let write = restore_auto_now_unless(
6759 &fields.auto_now_idents,
6760 &save_ret,
6761 &save_keep,
6762 "e! {
6763 let _query = #root::core::UpdateQuery::new(
6764 <Self as #root::core::Model>::SCHEMA,
6765 ::std::vec![ #( #assignments ),* ],
6766 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6767 #pk_column_lit,
6768 #root::core::Op::Eq,
6769 ::core::convert::Into::<#root::core::SqlValue>::into(
6770 ::core::clone::Clone::clone(&self.#pk_ident)
6771 ),
6772 )),
6773 );
6774 let _affected = #root::sql::update_tx(tx, &_query).await?;
6775 ::core::result::Result::Ok(_affected)
6776 },
6777 );
6778 quote! {
6779 /// Save this row inside an open transaction. `INSERT` when
6780 /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6781 /// PK. Works against any backend that `tx` wraps.
6782 ///
6783 /// # Errors
6784 /// As [`Self::save_pool`].
6785 pub async fn save_tx(
6786 &mut self,
6787 tx: &mut #root::sql::PoolTx<'_>,
6788 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6789 #dispatch_unset
6790 #write
6791 }
6792 }
6793 } else {
6794 quote!()
6795 };
6796
6797 let tx_delete_method = {
6798 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6799 let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6800 if let Some(pk_ident) = pk_ident_for_tx {
6801 quote! {
6802 /// Delete the row identified by this instance's PK
6803 /// inside an open transaction. Works against any backend
6804 /// that `tx` wraps.
6805 ///
6806 /// # Errors
6807 /// As [`Self::delete_pool`].
6808 pub async fn delete_tx(
6809 &self,
6810 tx: &mut #root::sql::PoolTx<'_>,
6811 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6812 let _query = #root::core::DeleteQuery::by_pk(
6813 <Self as #root::core::Model>::SCHEMA,
6814 #pk_column_lit,
6815 ::core::convert::Into::<#root::core::SqlValue>::into(
6816 ::core::clone::Clone::clone(&self.#pk_ident)
6817 ),
6818 );
6819 #root::sql::delete_tx(tx, &_query).await
6820 }
6821 }
6822 } else {
6823 quote!()
6824 }
6825 };
6826
6827 // Update emission captures both BEFORE and AFTER state — runs an
6828 // extra SELECT against `_executor` BEFORE the UPDATE, captures
6829 // each tracked field's prior value, then after the UPDATE diffs
6830 // against the in-memory `&self`. `diff_changes` drops unchanged
6831 // columns so the JSON only contains the actual delta.
6832 //
6833 // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6834 // and binds `_audit_before_pairs`; `audit_update_post` runs
6835 // after the UPDATE and emits the PendingEntry.
6836 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6837 audited_fields
6838 {
6839 if tracked.is_empty() {
6840 (quote!(), quote!())
6841 } else {
6842 let tracked_cols: Vec<&str> = tracked.iter().map(|c| c.column.as_str()).collect();
6843 let pk_column_lit_for_select =
6844 primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6845 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6846 if fields.pk_is_auto {
6847 quote!(self.#pk_ident.get().copied().unwrap_or_default())
6848 } else {
6849 quote!(::core::clone::Clone::clone(&self.#pk_ident))
6850 }
6851 } else {
6852 quote!(0_i64)
6853 };
6854 let before_pairs = tracked.iter().map(|c| {
6855 let column_lit = c.column.as_str();
6856 let value_ty = &c.value_ty;
6857 quote! {
6858 (
6859 #column_lit,
6860 match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6861 &_audit_before_row, #column_lit,
6862 ) {
6863 ::core::result::Result::Ok(v) => #root::core::__json_value(&v),
6864 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6865 },
6866 )
6867 }
6868 });
6869 let pre = quote! {
6870 let _audit_before_query = #root::audit::before_image_query(
6871 <Self as #root::core::Model>::SCHEMA,
6872 #pk_column_lit_for_select,
6873 ::core::convert::Into::<#root::core::SqlValue>::into(#pk_value_for_bind),
6874 &[ #( #tracked_cols ),* ],
6875 );
6876 // A failed pre-read must not let the UPDATE run unaudited (#1907).
6877 let _audit_before_pairs:
6878 ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6879 #root::sql::select_rows_on(&mut *_executor, &_audit_before_query)
6880 .await?
6881 .into_iter()
6882 .next()
6883 .map(|_audit_before_row| ::std::vec![ #( #before_pairs ),* ]);
6884 };
6885 let post = quote! {
6886 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6887 let _audit_after = self.__rustango_audit_pairs();
6888 if let ::core::option::Option::Some(_audit_entry) =
6889 #root::audit::PendingEntry::update_diff(
6890 <Self as #root::core::Model>::SCHEMA.table,
6891 self.__rustango_audit_pk(),
6892 &_audit_before,
6893 &_audit_after,
6894 )
6895 {
6896 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6897 }
6898 }
6899 };
6900 (pre, post)
6901 }
6902 } else {
6903 (quote!(), quote!())
6904 };
6905
6906 // Bulk-insert audit: one `Create` entry per row once RETURNING has
6907 // set its PK, written in one batched INSERT.
6908 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6909 quote! {
6910 let _audit_entries: ::std::vec::Vec<#root::audit::PendingEntry> = rows
6911 .iter()
6912 .map(|_row| _row.__rustango_audit_entry(#root::audit::AuditOp::Create))
6913 .collect();
6914 #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6915 }
6916 } else {
6917 quote!()
6918 };
6919
6920 let save_method = if fields.pk_is_auto {
6921 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6922 let pk_column_lit = pk_column.as_str();
6923 let assignments = &fields.update_assignments;
6924 let upsert_cols = &fields.upsert_update_columns;
6925 let upsert_pushes = &fields.insert_pushes;
6926 // Read before the pushes, which fill each unset id.
6927 let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
6928 fields.uuid_v7_fields.iter().cloned().unzip();
6929 let upsert_returning = &fields.returning_cols;
6930 let upsert_auto_assigns = &fields.auto_assigns;
6931 let auto_now_idents = &fields.auto_now_idents;
6932 // Conflict target: prefer the first declared `unique_together`
6933 // when it exists. Plain `Auto<T>` PKs are server-assigned via
6934 // `BIGSERIAL` and never collide on insert, so a PK-only target
6935 // would silently turn `upsert()` into "always-insert" for
6936 // surrogate-PK models with composite UNIQUE constraints — see
6937 // `RolePermission` / `UserRole` / `UserPermission` in the
6938 // tenancy permission engine. When no `unique_together` is
6939 // declared we keep the PK target (the original behaviour).
6940 let upsert_target_columns: Vec<String> =
6941 upsert_unique.map_or_else(|| vec![pk_column.clone()], <[String]>::to_vec);
6942 let upsert_target_lits = upsert_target_columns
6943 .iter()
6944 .map(String::as_str)
6945 .collect::<Vec<_>>();
6946 let conflict_clause = if fields.upsert_update_columns.is_empty() {
6947 quote!(#root::core::ConflictClause::DoNothing)
6948 } else {
6949 quote!(#root::core::ConflictClause::DoUpdate {
6950 target: ::std::vec![ #( #upsert_target_lits ),* ],
6951 update_columns: ::std::vec![ #( #upsert_cols ),* ],
6952 })
6953 };
6954 // Audited: the upsert reports whether it inserted or updated (#1795).
6955 let (upsert_run, audit_upsert_emit) = if audited_fields.is_some() {
6956 (
6957 quote! {
6958 let (_returning_row_v, _audit_op) =
6959 #root::audit::upsert_returning_on(&mut *_executor, &query).await?;
6960 },
6961 quote! {
6962 let _audit_entry = self.__rustango_audit_entry(_audit_op);
6963 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6964 },
6965 )
6966 } else {
6967 (
6968 quote! {
6969 let _returning_row_v =
6970 #root::sql::__macro_internals::insert_returning_on(_executor, &query)
6971 .await?;
6972 },
6973 quote!(),
6974 )
6975 };
6976 let save_on_write = restore_auto_now_unless(
6977 auto_now_idents,
6978 &save_ret,
6979 &save_keep,
6980 "e! {
6981 #audit_update_pre
6982 let _query = #root::core::UpdateQuery::new(
6983 <Self as #root::core::Model>::SCHEMA,
6984 ::std::vec![ #( #assignments ),* ],
6985 #root::core::WhereExpr::Predicate(#root::core::Filter::new(
6986 #pk_column_lit,
6987 #root::core::Op::Eq,
6988 ::core::convert::Into::<#root::core::SqlValue>::into(
6989 ::core::clone::Clone::clone(&self.#pk_ident)
6990 ),
6991 )),
6992 );
6993 let _affected = #root::sql::__macro_internals::update_on(
6994 #executor_passes_to_data_write,
6995 &_query,
6996 ).await?;
6997 #audit_update_post
6998 ::core::result::Result::Ok(_affected)
6999 },
7000 );
7001 let upsert_write = restore_auto_now_unless(
7002 auto_now_idents,
7003 "e!(::core::result::Result<(), #root::sql::ExecError>),
7004 "e!(_saved.is_ok()),
7005 "e! {
7006 // The UPDATE branch writes `auto_now` too, so stamp it like `save`.
7007 #( self.#auto_now_idents = #root::sql::Auto::Set(#root::__chrono::Utc::now()); )*
7008 #( #upsert_pushes )*
7009 let query = #root::core::InsertQuery::new(
7010 <Self as #root::core::Model>::SCHEMA,
7011 _columns,
7012 _values,
7013 )
7014 .returning(::std::vec![ #( #upsert_returning ),* ])
7015 .on_conflict(#conflict_clause)
7016 .generated(_generated);
7017 #upsert_run
7018 let _returning_row = &_returning_row_v;
7019 #( #upsert_auto_assigns )*
7020 #audit_upsert_emit
7021 ::core::result::Result::Ok(())
7022 },
7023 );
7024 Some(quote! {
7025 /// Insert this row if its `Auto<T>` primary key is
7026 /// `Unset`, otherwise update the existing row matching the
7027 /// PK. The caller doesn't need to pick `insert` vs the
7028 /// bulk-update path manually.
7029 ///
7030 /// On the insert branch, populates the PK from `RETURNING`
7031 /// (same behavior as `insert`). On the update branch,
7032 /// writes every non-PK column back; if no row matches the
7033 /// PK, returns `Ok(())` silently.
7034 ///
7035 /// Only generated when the primary key is declared as
7036 /// `Auto<T>`. Models with a manually-managed PK must use
7037 /// `insert` or the QuerySet update builder.
7038 ///
7039 /// **Postgres only — prefer [`Self::save_pool`].** This takes a
7040 /// driver-specific `PgPool`, so it does not exist at all on a
7041 /// build without the `postgres` feature: selecting `sqlite`
7042 /// makes the method vanish rather than fail with anything that
7043 /// names the cause.
7044 ///
7045 /// [`Self::save_pool`] takes `rustango::sql::Pool` and works on
7046 /// all three backends. The naming is inverted — the short name
7047 /// is the narrow one — and that is tracked in #1293.
7048 ///
7049 /// # Errors
7050 /// Returns [`#root::sql::ExecError`] for SQL-writing
7051 /// or driver failures.
7052 #[cfg(feature = "postgres")]
7053 pub async fn save(
7054 &mut self,
7055 pool: &#root::sql::sqlx::PgPool,
7056 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7057 #pool_to_save_on
7058 }
7059
7060 /// Like [`Self::save`] but accepts any sqlx executor —
7061 /// `&PgPool`, `&mut PgConnection`, or a transaction. The
7062 /// escape hatch for tenant-scoped writes: schema-mode
7063 /// tenants share the registry pool but rely on a per-
7064 /// checkout `SET search_path`, so passing `&PgPool` would
7065 /// silently hit the wrong schema. Acquire a connection
7066 /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
7067 ///
7068 /// # Errors
7069 /// As [`Self::save`].
7070 #[cfg(feature = "postgres")]
7071 pub async fn save_on #executor_generics (
7072 &mut self,
7073 #executor_param,
7074 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7075 #executor_where
7076 {
7077 // #1029 — INSERT writes exactly one row → 1; UPDATE returns
7078 // the rows-affected count (0 when the PK no longer exists).
7079 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
7080 return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
7081 }
7082 #save_on_write
7083 }
7084
7085 /// Per-call override for the audit source. Runs
7086 /// [`Self::save_on`] inside an [`#root::audit::with_source`]
7087 /// scope so the resulting audit entry records `source`
7088 /// instead of the task-local default. Useful for seed
7089 /// scripts and one-off CLI tools that don't sit inside an
7090 /// admin handler. The override applies only to this call;
7091 /// no global state changes.
7092 ///
7093 /// # Errors
7094 /// As [`Self::save_on`].
7095 #[cfg(feature = "postgres")]
7096 pub async fn save_on_with #executor_generics (
7097 &mut self,
7098 #executor_param,
7099 source: #root::audit::AuditSource,
7100 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7101 #executor_where
7102 {
7103 #root::audit::with_source(source, self.save_on(_executor)).await
7104 }
7105
7106 /// Insert this row or update it in-place if the primary key already
7107 /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
7108 ///
7109 /// With `Auto::Unset` PK the server assigns a new key and no conflict
7110 /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
7111 /// inserted if absent or all non-PK columns are overwritten if present.
7112 ///
7113 /// # Errors
7114 /// As [`Self::insert_on`].
7115 #[cfg(feature = "postgres")]
7116 pub async fn upsert(
7117 &mut self,
7118 pool: &#root::sql::sqlx::PgPool,
7119 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7120 #pool_to_upsert_on
7121 }
7122
7123 /// Like [`Self::upsert`] but accepts any sqlx executor.
7124 /// See [`Self::save_on`] for tenancy-scoped rationale.
7125 ///
7126 /// # Errors
7127 /// As [`Self::upsert`].
7128 #[cfg(feature = "postgres")]
7129 pub async fn upsert_on #executor_generics (
7130 &mut self,
7131 #executor_param,
7132 ) -> ::core::result::Result<(), #root::sql::ExecError>
7133 #executor_where
7134 {
7135 let mut _columns: ::std::vec::Vec<&'static str> =
7136 ::std::vec::Vec::new();
7137 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7138 ::std::vec::Vec::new();
7139 #[allow(unused_mut)]
7140 let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7141 #(
7142 if matches!(&self.#gen_idents, #root::sql::Auto::Unset) {
7143 _generated.push(#gen_cols);
7144 }
7145 )*
7146 #upsert_write
7147 }
7148 })
7149 } else {
7150 None
7151 };
7152
7153 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
7154 let pk_column_lit = pk_column.as_str();
7155 // Optional `soft_delete_on` / `restore_on` companions when the
7156 // model has a `#[rustango(soft_delete)]` column. They land
7157 // alongside the regular `delete_on` so callers have both
7158 // options — a hard delete (audit-tracked as a real DELETE) and
7159 // a logical delete (audit-tracked as an UPDATE setting the
7160 // deleted_at column to NOW()).
7161 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
7162 let col_lit = col;
7163 let sd_field_ident = fields
7164 .soft_delete_field_ident
7165 .clone()
7166 .expect("soft_delete_column without ident");
7167 quote! {
7168 /// Soft-delete this row by setting its
7169 /// `#[rustango(soft_delete)]` column to `NOW()`.
7170 /// The row stays in the table; query helpers can
7171 /// filter it out by checking the column for `IS NOT
7172 /// NULL`.
7173 ///
7174 /// # Errors
7175 /// As [`Self::delete`].
7176 #[cfg(feature = "postgres")]
7177 pub async fn soft_delete_on #executor_generics (
7178 &self,
7179 #executor_param,
7180 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7181 #executor_where
7182 {
7183 let _now = #root::__chrono::Utc::now();
7184 let _query = #root::soft_delete::__mark_query(
7185 <Self as #root::core::Model>::SCHEMA,
7186 #col_lit,
7187 #pk_column_lit,
7188 ::core::convert::Into::<#root::core::SqlValue>::into(
7189 ::core::clone::Clone::clone(&self.#pk_ident)
7190 ),
7191 ::core::option::Option::Some(_now),
7192 );
7193 let _affected = #root::sql::__macro_internals::update_on(
7194 #executor_passes_to_data_write,
7195 &_query,
7196 ).await?;
7197 #audit_softdelete_emit
7198 ::core::result::Result::Ok(_affected)
7199 }
7200
7201 /// Inverse of [`Self::soft_delete_on`] — clears the
7202 /// soft-delete column back to NULL so the row is
7203 /// considered live again.
7204 ///
7205 /// # Errors
7206 /// As [`Self::delete`].
7207 #[cfg(feature = "postgres")]
7208 pub async fn restore_on #executor_generics (
7209 &self,
7210 #executor_param,
7211 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7212 #executor_where
7213 {
7214 let _query = #root::soft_delete::__mark_query(
7215 <Self as #root::core::Model>::SCHEMA,
7216 #col_lit,
7217 #pk_column_lit,
7218 ::core::convert::Into::<#root::core::SqlValue>::into(
7219 ::core::clone::Clone::clone(&self.#pk_ident)
7220 ),
7221 ::core::option::Option::None,
7222 );
7223 let _affected = #root::sql::__macro_internals::update_on(
7224 #executor_passes_to_data_write,
7225 &_query,
7226 ).await?;
7227 #audit_restore_emit
7228 ::core::result::Result::Ok(_affected)
7229 }
7230
7231 /// Tri-dialect counterpart of [`Self::soft_delete_on`]
7232 /// — takes [`#root::sql::Pool`] and dispatches per
7233 /// backend. Eloquent `Model::delete()` semantics on
7234 /// soft-delete-enabled models (closes #821 partial).
7235 ///
7236 /// Sets the `#[rustango(soft_delete)]` column to
7237 /// `NOW()` on every backend. Query helpers
7238 /// (`QuerySet::active()` / `only_trashed()`,
7239 /// `soft_delete::active_filter` /
7240 /// `compose_with_active`) filter trashed rows out by
7241 /// reading `IS NULL` on the same column. Keyed on the PK;
7242 /// global scopes do not apply.
7243 ///
7244 /// # Errors
7245 /// As [`#root::sql::update_pool`].
7246 pub async fn soft_delete(
7247 &self,
7248 pool: &#root::sql::Pool,
7249 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7250 let _now = #root::__chrono::Utc::now();
7251 let _query = #root::soft_delete::__mark_query(
7252 <Self as #root::core::Model>::SCHEMA,
7253 #col_lit,
7254 #pk_column_lit,
7255 ::core::convert::Into::<#root::core::SqlValue>::into(
7256 ::core::clone::Clone::clone(&self.#pk_ident)
7257 ),
7258 ::core::option::Option::Some(_now),
7259 );
7260 #pool_softdelete_run
7261 }
7262
7263 /// Tri-dialect counterpart of [`Self::restore_on`].
7264 /// Clears the `#[rustango(soft_delete)]` column back
7265 /// to `NULL`, marking the row live again. Eloquent
7266 /// `Model::restore()` parity. Keyed on the PK; global
7267 /// scopes do not apply.
7268 ///
7269 /// # Errors
7270 /// As [`#root::sql::update_pool`].
7271 pub async fn restore(
7272 &self,
7273 pool: &#root::sql::Pool,
7274 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7275 let _query = #root::soft_delete::__mark_query(
7276 <Self as #root::core::Model>::SCHEMA,
7277 #col_lit,
7278 #pk_column_lit,
7279 ::core::convert::Into::<#root::core::SqlValue>::into(
7280 ::core::clone::Clone::clone(&self.#pk_ident)
7281 ),
7282 ::core::option::Option::None,
7283 );
7284 #pool_restore_run
7285 }
7286
7287 /// Hard-delete this row, ignoring the soft-delete
7288 /// column. Eloquent `Model::forceDelete()` parity —
7289 /// the escape hatch when you need to actually purge
7290 /// data (GDPR, fixture cleanup, etc.).
7291 ///
7292 /// Equivalent to [`Self::delete_pool`] (the framework's
7293 /// non-soft delete) — exposed under the Eloquent name
7294 /// for muscle-memory + so soft-delete-enabled models
7295 /// have all three operations (soft / restore / force)
7296 /// in one place.
7297 ///
7298 /// # Errors
7299 /// As [`Self::delete_pool`].
7300 pub async fn force_delete(
7301 &self,
7302 pool: &#root::sql::Pool,
7303 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7304 Self::delete_pool(self, pool).await
7305 }
7306
7307 /// Returns `true` when this row is soft-deleted (its
7308 /// `#[rustango(soft_delete)]` column is currently
7309 /// set — Eloquent `$model->trashed()` parity).
7310 ///
7311 /// Pure in-memory predicate over `&self`; does not
7312 /// hit the database. Useful in admin/template code
7313 /// like `{% if post.trashed() %}…{% endif %}` and in
7314 /// guard clauses on restore/force-delete flows.
7315 pub fn trashed(&self) -> bool {
7316 ::core::option::Option::is_some(&self.#sd_field_ident)
7317 }
7318
7319 /// Fetch every row whose `#[rustango(soft_delete)]`
7320 /// column is `NULL` (a.k.a. the live, non-trashed
7321 /// rows). Eloquent's default `Model::all()` behavior on
7322 /// a soft-delete model (Eloquent auto-scopes trashed
7323 /// rows out; rustango doesn't have auto-scoping yet —
7324 /// see issue #820 — so this is the explicit shortcut).
7325 ///
7326 /// One-liner over `QuerySet::<Self>::default()
7327 /// .active().fetch(pool)`. Closes #821 partial.
7328 ///
7329 /// # Errors
7330 /// As [`#root::sql::FetcherPool::fetch`].
7331 pub async fn active(
7332 pool: &#root::sql::Pool,
7333 ) -> ::core::result::Result<
7334 ::std::vec::Vec<Self>,
7335 #root::sql::ExecError,
7336 > {
7337 use #root::sql::FetcherPool as _;
7338 #root::query::QuerySet::<Self>::default()
7339 .active()
7340 .fetch(pool)
7341 .await
7342 }
7343
7344 /// Fetch ONLY soft-deleted rows. Eloquent
7345 /// `Model::onlyTrashed()->get()` parity — drives the
7346 /// admin "Trash" page, restore flows, GDPR purge
7347 /// scans, etc.
7348 ///
7349 /// One-liner over `QuerySet::<Self>::default()
7350 /// .only_trashed().fetch(pool)`. Closes #821
7351 /// partial.
7352 ///
7353 /// # Errors
7354 /// As [`#root::sql::FetcherPool::fetch`].
7355 pub async fn only_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 .only_trashed()
7364 .fetch(pool)
7365 .await
7366 }
7367
7368 /// Fetch every row, both live and soft-deleted.
7369 /// Eloquent `Model::withTrashed()->get()` parity.
7370 ///
7371 /// Today every queryset already includes trashed rows
7372 /// (rustango has no global-scope tracking yet — issue
7373 /// #820), so this is functionally equivalent to
7374 /// [`Self::all`]. Exposed as a named shortcut so
7375 /// soft-delete-aware code reads `Model::with_trashed_pool`
7376 /// rather than `Model::all_pool` — keeps intent visible
7377 /// in callers and stays correct when auto-scoping lands.
7378 ///
7379 /// Closes #821 partial.
7380 ///
7381 /// # Errors
7382 /// As [`#root::sql::FetcherPool::fetch`].
7383 pub async fn with_trashed(
7384 pool: &#root::sql::Pool,
7385 ) -> ::core::result::Result<
7386 ::std::vec::Vec<Self>,
7387 #root::sql::ExecError,
7388 > {
7389 use #root::sql::FetcherPool as _;
7390 #root::query::QuerySet::<Self>::default()
7391 .with_trashed()
7392 .fetch(pool)
7393 .await
7394 }
7395
7396 }
7397 } else {
7398 quote!()
7399 };
7400 quote! {
7401 /// Delete the row identified by this instance's primary key.
7402 ///
7403 /// Returns the number of rows affected (0 or 1).
7404 ///
7405 /// **Postgres only — prefer [`Self::delete_pool`].** This takes
7406 /// a driver-specific `PgPool`, so it does not exist on a build
7407 /// without the `postgres` feature. [`Self::delete_pool`] takes
7408 /// `rustango::sql::Pool` and works on all three. The naming is
7409 /// inverted; tracked in #1293.
7410 ///
7411 /// # Errors
7412 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7413 /// driver failures.
7414 #[cfg(feature = "postgres")]
7415 pub async fn delete(
7416 &self,
7417 pool: &#root::sql::sqlx::PgPool,
7418 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7419 #pool_to_delete_on
7420 }
7421
7422 /// Like [`Self::delete`] but accepts any sqlx executor —
7423 /// for tenant-scoped deletes against an explicitly-acquired
7424 /// connection. See [`Self::save_on`] for the rationale.
7425 ///
7426 /// # Errors
7427 /// As [`Self::delete`].
7428 #[cfg(feature = "postgres")]
7429 pub async fn delete_on #executor_generics (
7430 &self,
7431 #executor_param,
7432 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7433 #executor_where
7434 {
7435 let query = #root::core::DeleteQuery::by_pk(
7436 <Self as #root::core::Model>::SCHEMA,
7437 #pk_column_lit,
7438 ::core::convert::Into::<#root::core::SqlValue>::into(
7439 ::core::clone::Clone::clone(&self.#pk_ident)
7440 ),
7441 );
7442 let _affected = #root::sql::__macro_internals::delete_on(
7443 #executor_passes_to_data_write,
7444 &query,
7445 ).await?;
7446 #audit_delete_emit
7447 ::core::result::Result::Ok(_affected)
7448 }
7449
7450 /// Per-call audit-source override for [`Self::delete_on`].
7451 /// See [`Self::save_on_with`] for shape rationale.
7452 ///
7453 /// # Errors
7454 /// As [`Self::delete_on`].
7455 #[cfg(feature = "postgres")]
7456 pub async fn delete_on_with #executor_generics (
7457 &self,
7458 #executor_param,
7459 source: #root::audit::AuditSource,
7460 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7461 #executor_where
7462 {
7463 #root::audit::with_source(source, self.delete_on(_executor)).await
7464 }
7465 #pool_delete_method
7466 #pool_insert_method
7467 #audit_entry_method
7468 #pool_save_method
7469 #refresh_replicate_methods
7470 #tx_delete_method
7471 #tx_insert_method
7472 #tx_save_method
7473 #soft_delete_methods
7474
7475 /// Returns `true` when `other` represents the same DB
7476 /// row as `self` — i.e. their primary keys compare
7477 /// equal. Eloquent `$model->is($other)` parity.
7478 ///
7479 /// Because both arguments are typed `&Self`, the
7480 /// model/table check is automatic — `Post::is` cannot
7481 /// be invoked against a `Comment` at compile time. Only
7482 /// the PK has to be compared at runtime.
7483 pub fn is(&self, other: &Self) -> bool {
7484 self.#pk_ident == other.#pk_ident
7485 }
7486
7487 /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7488 /// parity.
7489 pub fn is_not(&self, other: &Self) -> bool {
7490 self.#pk_ident != other.#pk_ident
7491 }
7492
7493 /// Returns this row's primary-key value as an
7494 /// [`#root::core::SqlValue`]. Eloquent
7495 /// `$model->getKey()` parity.
7496 ///
7497 /// Useful when you need to thread the PK through a
7498 /// generic `Into<SqlValue>`-bound API without knowing the
7499 /// concrete PK type (`i64` vs `Uuid` vs `String`).
7500 #[must_use]
7501 pub fn get_key(&self) -> #root::core::SqlValue {
7502 ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7503 }
7504
7505 }
7506 });
7507
7508 let insert_method = if fields.has_auto {
7509 let pushes = &fields.insert_pushes;
7510 let returning_cols = &fields.returning_cols;
7511 let auto_assigns = &fields.auto_assigns;
7512 quote! {
7513 /// Insert this row into its table. Skips columns whose
7514 /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7515 /// sequence fills them in, then reads each `Auto` column
7516 /// back via `RETURNING` and stores it on `self`.
7517 ///
7518 /// **Postgres only — prefer [`Self::insert_pool`].** This takes
7519 /// a driver-specific `PgPool`, so it does not exist on a build
7520 /// without the `postgres` feature. [`Self::insert_pool`] takes
7521 /// `rustango::sql::Pool` and works on all three. The naming is
7522 /// inverted; tracked in #1293.
7523 ///
7524 /// # Errors
7525 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7526 /// driver failures.
7527 #[cfg(feature = "postgres")]
7528 pub async fn insert(
7529 &mut self,
7530 pool: &#root::sql::sqlx::PgPool,
7531 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7532 #pool_to_insert_on
7533 }
7534
7535 /// Like [`Self::insert`] but accepts any sqlx executor.
7536 /// See [`Self::save_on`] for tenancy-scoped rationale.
7537 ///
7538 /// # Errors
7539 /// As [`Self::insert`].
7540 #[cfg(feature = "postgres")]
7541 pub async fn insert_on #executor_generics (
7542 &mut self,
7543 #executor_param,
7544 ) -> ::core::result::Result<(), #root::sql::ExecError>
7545 #executor_where
7546 {
7547 let mut _columns: ::std::vec::Vec<&'static str> =
7548 ::std::vec::Vec::new();
7549 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7550 ::std::vec::Vec::new();
7551 #( #pushes )*
7552 let query = #root::core::InsertQuery::new(
7553 <Self as #root::core::Model>::SCHEMA,
7554 _columns,
7555 _values,
7556 )
7557 .returning(::std::vec![ #( #returning_cols ),* ]);
7558 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7559 #executor_passes_to_data_write,
7560 &query,
7561 ).await?;
7562 let _returning_row = &_returning_row_v;
7563 #( #auto_assigns )*
7564 #audit_insert_emit
7565 ::core::result::Result::Ok(())
7566 }
7567
7568 /// Per-call audit-source override for [`Self::insert_on`].
7569 /// See [`Self::save_on_with`] for shape rationale.
7570 ///
7571 /// # Errors
7572 /// As [`Self::insert_on`].
7573 #[cfg(feature = "postgres")]
7574 pub async fn insert_on_with #executor_generics (
7575 &mut self,
7576 #executor_param,
7577 source: #root::audit::AuditSource,
7578 ) -> ::core::result::Result<(), #root::sql::ExecError>
7579 #executor_where
7580 {
7581 #root::audit::with_source(source, self.insert_on(_executor)).await
7582 }
7583 }
7584 } else {
7585 let insert_columns = &fields.insert_columns;
7586 let insert_values = &fields.insert_values;
7587 quote! {
7588 /// Insert this row into its table.
7589 ///
7590 /// # Errors
7591 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7592 /// driver failures.
7593 #[cfg(feature = "postgres")]
7594 pub async fn insert(
7595 &self,
7596 pool: &#root::sql::sqlx::PgPool,
7597 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7598 self.insert_on(pool).await
7599 }
7600
7601 /// Like [`Self::insert`] but accepts any sqlx executor.
7602 /// See [`Self::save_on`] for tenancy-scoped rationale.
7603 ///
7604 /// # Errors
7605 /// As [`Self::insert`].
7606 #[cfg(feature = "postgres")]
7607 pub async fn insert_on<'_c, _E>(
7608 &self,
7609 _executor: _E,
7610 ) -> ::core::result::Result<(), #root::sql::ExecError>
7611 where
7612 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7613 {
7614 let query = #root::core::InsertQuery::new(
7615 <Self as #root::core::Model>::SCHEMA,
7616 ::std::vec![ #( #insert_columns ),* ],
7617 ::std::vec![ #( #insert_values ),* ],
7618 );
7619 #root::sql::__macro_internals::insert_on(_executor, &query).await
7620 }
7621 }
7622 };
7623
7624 let bulk_insert_method = if fields.has_auto {
7625 let cols_no_auto = &fields.bulk_columns_no_auto;
7626 let cols_all = &fields.bulk_columns_all;
7627 let pushes_no_auto = &fields.bulk_pushes_no_auto;
7628 let pushes_all = &fields.bulk_pushes_all;
7629 let returning_cols = &fields.returning_cols;
7630 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7631 let uniformity = &fields.bulk_auto_uniformity;
7632 let fill_in_place = &fields.bulk_fill_in_place;
7633 // Nothing to read back when every Auto field is filled Rust-side (#1934).
7634 let read_back = if fields.returning_cols.is_empty() {
7635 quote!()
7636 } else {
7637 quote! {
7638 if _returned.len() != rows.len() {
7639 return ::core::result::Result::Err(
7640 #root::sql::ExecError::Sql(
7641 #root::sql::SqlError::BulkInsertReturningMismatch {
7642 expected: rows.len(),
7643 actual: _returned.len(),
7644 }
7645 )
7646 );
7647 }
7648 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7649 #auto_assigns_for_row
7650 }
7651 }
7652 };
7653 // The DB-filled `Auto` picks the path; Rust-filled ones fill per row (#1950).
7654 let first_auto_ident = fields
7655 .first_db_auto
7656 .as_ref()
7657 .map(|(ident, _)| ident)
7658 .or(fields.first_auto_ident.as_ref())
7659 .expect("has_auto implies first_auto_ident is Some");
7660 quote! {
7661 /// Bulk-insert `rows`: one statement, or one per chunk in a
7662 /// transaction past the bind cap. Every row's
7663 /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7664 /// (sequence fills them in) or uniformly `Auto::Set(_)`
7665 /// (caller-supplied values). Mixed Set/Unset is rejected
7666 /// — call `insert` per row for that case.
7667 ///
7668 /// Empty slice is a no-op. Each row's `Auto` fields are
7669 /// populated from the `RETURNING` clause in input order
7670 /// before this returns.
7671 ///
7672 /// # Errors
7673 /// Returns [`#root::sql::ExecError`] for validation,
7674 /// SQL-writing, mixed-Auto rejection, or driver failures.
7675 #[cfg(feature = "postgres")]
7676 pub async fn bulk_insert(
7677 rows: &mut [Self],
7678 pool: &#root::sql::sqlx::PgPool,
7679 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7680 #pool_to_bulk_insert_mut
7681 }
7682
7683 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7684 /// See [`Self::save_on`] for tenancy-scoped rationale.
7685 ///
7686 /// # Errors
7687 /// As [`Self::bulk_insert`].
7688 #[cfg(feature = "postgres")]
7689 pub async fn bulk_insert_on #executor_generics (
7690 rows: &mut [Self],
7691 #executor_param,
7692 ) -> ::core::result::Result<(), #root::sql::ExecError>
7693 #executor_where
7694 {
7695 if rows.is_empty() {
7696 return ::core::result::Result::Ok(());
7697 }
7698 let _first_unset = matches!(
7699 rows[0].#first_auto_ident,
7700 #root::sql::Auto::Unset
7701 );
7702 #( #uniformity )*
7703 for _row in rows.iter_mut() {
7704 #( #fill_in_place )*
7705 }
7706
7707 let mut _all_rows: ::std::vec::Vec<
7708 ::std::vec::Vec<#root::core::SqlValue>,
7709 > = ::std::vec::Vec::with_capacity(rows.len());
7710 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7711 for _row in rows.iter() {
7712 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7713 ::std::vec::Vec::new();
7714 #( #pushes_no_auto )*
7715 _all_rows.push(_row_vals);
7716 }
7717 ::std::vec![ #( #cols_no_auto ),* ]
7718 } else {
7719 for _row in rows.iter() {
7720 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7721 ::std::vec::Vec::new();
7722 #( #pushes_all )*
7723 _all_rows.push(_row_vals);
7724 }
7725 ::std::vec![ #( #cols_all ),* ]
7726 };
7727
7728 let _query = #root::core::BulkInsertQuery::new(
7729 <Self as #root::core::Model>::SCHEMA,
7730 _columns,
7731 _all_rows,
7732 )
7733 .returning(::std::vec![ #( #returning_cols ),* ]);
7734 let _returned = #root::sql::__macro_internals::bulk_insert_on(
7735 #executor_passes_to_data_write,
7736 &_query,
7737 ).await?;
7738 #read_back
7739 #audit_bulk_insert_emit
7740 ::core::result::Result::Ok(())
7741 }
7742 }
7743 } else {
7744 let cols_all = &fields.bulk_columns_all;
7745 let pushes_all = &fields.bulk_pushes_all;
7746 quote! {
7747 /// Bulk-insert `rows`: one statement, or one per chunk in a
7748 /// transaction past the bind cap. Every row's
7749 /// fields are written verbatim — there are no `Auto<T>`
7750 /// fields on this model.
7751 ///
7752 /// Empty slice is a no-op.
7753 ///
7754 /// # Errors
7755 /// Returns [`#root::sql::ExecError`] for validation,
7756 /// SQL-writing, or driver failures.
7757 #[cfg(feature = "postgres")]
7758 pub async fn bulk_insert(
7759 rows: &[Self],
7760 pool: &#root::sql::sqlx::PgPool,
7761 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7762 #pool_to_bulk_insert_on
7763 }
7764
7765 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7766 /// See [`Self::save_on`] for tenancy-scoped rationale.
7767 ///
7768 /// # Errors
7769 /// As [`Self::bulk_insert`].
7770 #[cfg(feature = "postgres")]
7771 pub async fn bulk_insert_on #executor_generics (
7772 rows: &[Self],
7773 #executor_param,
7774 ) -> ::core::result::Result<(), #root::sql::ExecError>
7775 #executor_where
7776 {
7777 if rows.is_empty() {
7778 return ::core::result::Result::Ok(());
7779 }
7780 let mut _all_rows: ::std::vec::Vec<
7781 ::std::vec::Vec<#root::core::SqlValue>,
7782 > = ::std::vec::Vec::with_capacity(rows.len());
7783 for _row in rows.iter() {
7784 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7785 ::std::vec::Vec::new();
7786 #( #pushes_all )*
7787 _all_rows.push(_row_vals);
7788 }
7789 let _query = #root::core::BulkInsertQuery::new(
7790 <Self as #root::core::Model>::SCHEMA,
7791 ::std::vec![ #( #cols_all ),* ],
7792 _all_rows,
7793 );
7794 let _ = #root::sql::__macro_internals::bulk_insert_on(
7795 #executor_passes_to_data_write,
7796 &_query,
7797 ).await?;
7798 #audit_bulk_insert_emit
7799 ::core::result::Result::Ok(())
7800 }
7801 }
7802 };
7803
7804 // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7805 // (no postgres-feature gate); routes through the existing
7806 // `bulk_insert_pool` + per-dialect conflict writer.
7807 //
7808 // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7809 // sequence picks the PK for fresh inserts; the UPDATE branch never
7810 // touches the Auto column.
7811 // Audited models audit each written row; MySQL, with no RETURNING,
7812 // is refused there (#1747, #1795).
7813 let run_conflict_bulk_insert = match (audited_fields.is_some(), primary_key.is_some()) {
7814 (false, _) => quote!(#root::sql::bulk_insert_pool(pool, &_query).await),
7815 (true, true) => quote! {
7816 #root::audit::bulk_insert_with_audit::<Self>(pool, &_query, |_r: &Self, _op| {
7817 _r.__rustango_audit_entry(_op)
7818 })
7819 .await
7820 },
7821 (true, false) => quote! {
7822 ::core::result::Result::Err(#root::sql::ExecError::MissingPrimaryKey {
7823 table: <Self as #root::core::Model>::SCHEMA.table,
7824 })
7825 },
7826 };
7827 let bulk_upsert_pool_method = {
7828 let (gen_idents, gen_cols): (Vec<_>, Vec<_>) =
7829 fields.uuid_v7_fields.iter().cloned().unzip();
7830 // Pick the "no Auto" columns when the model has Auto fields,
7831 // else every column.
7832 let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7833 (
7834 fields.bulk_columns_no_auto.clone(),
7835 fields.bulk_pushes_no_auto.clone(),
7836 )
7837 } else {
7838 (
7839 fields.bulk_columns_all.clone(),
7840 fields.bulk_pushes_all.clone(),
7841 )
7842 };
7843 quote! {
7844 /// Tri-dialect bulk insert that updates on conflict — the
7845 /// "import a batch idempotently" shape. Issue #267 / T1.5.
7846 ///
7847 /// Per-row values are extracted and lowered into a
7848 /// [`#root::core::BulkInsertQuery`] with
7849 /// `on_conflict = DoUpdate { target, update_columns }`. The
7850 /// writer dispatches per-dialect:
7851 /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7852 /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)`; refused when
7853 /// `target` is not the only unique key, since MySQL matches every UNIQUE index
7854 ///
7855 /// `target` names the column(s) whose unique constraint
7856 /// defines the conflict (typically a `unique` or
7857 /// `unique_together` natural-key column, NOT the `Auto<T>`
7858 /// PK). `update_cols` names the columns to overwrite on
7859 /// conflict — every other column is left untouched on the
7860 /// existing row.
7861 ///
7862 /// Auto-PK rows must all have `Auto::Unset` (the sequence
7863 /// picks the PK on insert; the update path never touches
7864 /// the Auto column). Auto-set rows trigger a hard error.
7865 /// Rows repeating a `target` key: the last one wins.
7866 /// Empty slice is a no-op. On an audited model each written
7867 /// row gets a `create` or `update` audit row; MySQL refuses it.
7868 ///
7869 /// # Errors
7870 /// Returns [`#root::sql::ExecError`] for validation,
7871 /// SQL-writing, or driver failures.
7872 pub async fn bulk_upsert_pool(
7873 rows: &[Self],
7874 target: &[&'static str],
7875 update_cols: &[&'static str],
7876 pool: &#root::sql::Pool,
7877 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7878 if rows.is_empty() {
7879 return ::core::result::Result::Ok(());
7880 }
7881 let mut _all_rows: ::std::vec::Vec<
7882 ::std::vec::Vec<#root::core::SqlValue>,
7883 > = ::std::vec::Vec::with_capacity(rows.len());
7884 for _row in rows.iter() {
7885 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7886 ::std::vec::Vec::new();
7887 #( #upsert_pushes )*
7888 _all_rows.push(_row_vals);
7889 }
7890 #[allow(unused_mut)]
7891 let mut _generated: ::std::vec::Vec<&'static str> = ::std::vec::Vec::new();
7892 #(
7893 if rows.iter().all(|_r| matches!(&_r.#gen_idents, #root::sql::Auto::Unset)) {
7894 _generated.push(#gen_cols);
7895 }
7896 )*
7897 let _query = #root::core::BulkInsertQuery::new(
7898 <Self as #root::core::Model>::SCHEMA,
7899 ::std::vec![ #( #upsert_cols ),* ],
7900 _all_rows,
7901 )
7902 .on_conflict_do_update(target, update_cols)
7903 .generated(_generated);
7904 #run_conflict_bulk_insert
7905 }
7906
7907 /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7908 /// skip rows that would violate a unique constraint. Issue
7909 /// #267 / T1.5. Same per-dialect dispatch as
7910 /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7911 /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7912 /// <pivot> = <pivot>` (MySQL no-op write).
7913 ///
7914 /// # Errors
7915 /// As [`Self::bulk_upsert_pool`].
7916 pub async fn bulk_insert_or_ignore_pool(
7917 rows: &[Self],
7918 pool: &#root::sql::Pool,
7919 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7920 if rows.is_empty() {
7921 return ::core::result::Result::Ok(());
7922 }
7923 let mut _all_rows: ::std::vec::Vec<
7924 ::std::vec::Vec<#root::core::SqlValue>,
7925 > = ::std::vec::Vec::with_capacity(rows.len());
7926 for _row in rows.iter() {
7927 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7928 ::std::vec::Vec::new();
7929 #( #upsert_pushes )*
7930 _all_rows.push(_row_vals);
7931 }
7932 let _query = #root::core::BulkInsertQuery::new(
7933 <Self as #root::core::Model>::SCHEMA,
7934 ::std::vec![ #( #upsert_cols ),* ],
7935 _all_rows,
7936 )
7937 .on_conflict_do_nothing();
7938 #run_conflict_bulk_insert
7939 }
7940 }
7941 };
7942
7943 // Ergonomic `Model::bulk_update(objs, fields)`.
7944 // The SQL/IR/executor stack
7945 // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7946 // `write_bulk_update_*` writers) already existed; what was missing
7947 // was the per-model constructor that maps `&[Self]` + a runtime
7948 // column list into rows of `[pk, col_vals…]` so callers don't
7949 // hand-build the IR. Emitted only when the model has a primary key
7950 // (the PK is the join key and can't itself be updated).
7951 let bulk_update_rows_run = if audited_fields.is_some() {
7952 quote!(#root::audit::bulk_update_with_audit::<Self>(pool, &_query, |_r: &Self| {
7953 _r.__rustango_audit_entry(#root::audit::AuditOp::Update)
7954 })
7955 .await)
7956 } else {
7957 quote!(#root::sql::bulk_update_pool(pool, &_query).await)
7958 };
7959 let bulk_update_method = match &fields.primary_key {
7960 None => quote! {},
7961 Some((pk_ident, pk_col)) => {
7962 // One pair of match arms per non-PK column: a validation arm
7963 // resolving the runtime name to its `&'static str` column,
7964 // and a value arm pushing that field off the row.
7965 let mut col_arms: Vec<TokenStream2> = Vec::new();
7966 let mut val_arms: Vec<TokenStream2> = Vec::new();
7967 for entry in &fields.column_entries {
7968 if &entry.column == pk_col {
7969 continue;
7970 }
7971 let col = &entry.column;
7972 let ident = &entry.ident;
7973 col_arms.push(quote! { #col => #col, });
7974 val_arms.push(quote! {
7975 #col => _row_vals.push(
7976 ::core::convert::Into::<#root::core::SqlValue>::into(
7977 ::core::clone::Clone::clone(&_o.#ident)
7978 )
7979 ),
7980 });
7981 }
7982 quote! {
7983 /// Write per-row-different values for the named `fields`
7984 /// across every object in `objs`, matched by primary key.
7985 /// Past the backend's bind cap the rows go in batches inside
7986 /// one transaction, so the update is all or none.
7987 ///
7988 /// `fields` names the **columns** to update. The primary
7989 /// key identifies each row and cannot itself be updated
7990 /// (pass it and you get
7991 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7992 /// Empty `objs` or `fields` is a no-op returning `0`.
7993 /// Objects whose PK matches no row are simply not updated.
7994 /// Returns the number of rows affected.
7995 ///
7996 /// Tri-dialect: lowers to a
7997 /// [`#root::core::BulkUpdateQuery`] and dispatches
7998 /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7999 /// a CTE + correlated subquery on SQLite, an inner
8000 /// `JOIN (VALUES …)` on MySQL.
8001 ///
8002 /// # Errors
8003 /// [`#root::core::QueryError::UnknownField`] for a name
8004 /// that isn't a column on this model,
8005 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
8006 /// `fields` names the PK, or [`#root::sql::ExecError`] for
8007 /// SQL-writing / driver failures.
8008 pub async fn bulk_update(
8009 objs: &[Self],
8010 fields: &[&str],
8011 pool: &#root::sql::Pool,
8012 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
8013 if objs.is_empty() || fields.is_empty() {
8014 return ::core::result::Result::Ok(0);
8015 }
8016 let _model_name = <Self as #root::core::Model>::SCHEMA.name;
8017 let mut _update_columns: ::std::vec::Vec<&'static str> =
8018 ::std::vec::Vec::with_capacity(fields.len());
8019 for &_f in fields {
8020 let _col: &'static str = match _f {
8021 #pk_col => {
8022 return ::core::result::Result::Err(
8023 ::core::convert::Into::into(
8024 #root::core::QueryError::BulkUpdatePrimaryKey {
8025 model: _model_name,
8026 field: ::std::string::ToString::to_string(_f),
8027 }
8028 )
8029 );
8030 }
8031 #( #col_arms )*
8032 _ => {
8033 return ::core::result::Result::Err(
8034 ::core::convert::Into::into(
8035 #root::core::QueryError::UnknownField {
8036 model: _model_name,
8037 field: ::std::string::ToString::to_string(_f),
8038 }
8039 )
8040 );
8041 }
8042 };
8043 // A model whose only field is the primary key emits no
8044 // non-PK arms, so every arm above diverges and this push
8045 // is genuinely unreachable — correct, since such a model
8046 // has nothing to bulk-update. Scoped to the statement
8047 // rather than the fn so a real unreachability elsewhere
8048 // in this body still warns. Without it every PK-only
8049 // model warns in the *user's* crate, which they cannot
8050 // act on (#1290).
8051 #[allow(unreachable_code)]
8052 _update_columns.push(_col);
8053 }
8054 let mut _rows: ::std::vec::Vec<
8055 ::std::vec::Vec<#root::core::SqlValue>,
8056 > = ::std::vec::Vec::with_capacity(objs.len());
8057 for _o in objs.iter() {
8058 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
8059 ::std::vec::Vec::with_capacity(fields.len() + 1);
8060 // PK first — the writers expect `[pk, …update cols]`.
8061 _row_vals.push(
8062 ::core::convert::Into::<#root::core::SqlValue>::into(
8063 ::core::clone::Clone::clone(&_o.#pk_ident)
8064 )
8065 );
8066 for &_f in fields {
8067 match _f {
8068 #( #val_arms )*
8069 // Unreachable: every name was validated
8070 // against the same arm set above.
8071 _ => {}
8072 }
8073 }
8074 _rows.push(_row_vals);
8075 }
8076 let _query = #root::core::BulkUpdateQuery::new(
8077 <Self as #root::core::Model>::SCHEMA,
8078 _update_columns,
8079 _rows,
8080 );
8081 #bulk_update_rows_run
8082 }
8083 }
8084 }
8085 };
8086
8087 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
8088 quote! {
8089 /// Hidden runtime accessor for the primary-key value as a
8090 /// [`SqlValue`]. Used by reverse-relation helpers
8091 /// (`<parent>::<child>_set`) emitted from sibling models'
8092 /// FK fields. Not part of the public API.
8093 #[doc(hidden)]
8094 pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
8095 ::core::convert::Into::<#root::core::SqlValue>::into(
8096 ::core::clone::Clone::clone(&self.#pk_ident)
8097 )
8098 }
8099 }
8100 });
8101
8102 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
8103 let column_arms = fields.column_entries.iter().map(|entry| {
8104 let col = &entry.column;
8105 let ident = &entry.ident;
8106 quote! {
8107 #col => ::core::option::Option::Some(
8108 ::core::convert::Into::<#root::core::SqlValue>::into(
8109 ::core::clone::Clone::clone(&self.#ident)
8110 )
8111 ),
8112 }
8113 });
8114 quote! {
8115 impl #root::sql::HasPkValue for #struct_name {
8116 fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
8117 ::core::convert::Into::<#root::core::SqlValue>::into(
8118 ::core::clone::Clone::clone(&self.#pk_ident)
8119 )
8120 }
8121 fn __rustango_column_value(
8122 &self,
8123 column: &str,
8124 ) -> ::core::option::Option<#root::core::SqlValue> {
8125 match column {
8126 #( #column_arms )*
8127 _ => ::core::option::Option::None,
8128 }
8129 }
8130 }
8131 }
8132 });
8133
8134 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
8135
8136 // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
8137 // dispatch to the right per-backend body without the macro emitting
8138 // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
8139 // so audited models with non-Auto PKs (which still go through
8140 // `insert_one_with_audit` → `apply_auto_pk`) link.
8141 let assign_auto_pk_pool_impl = {
8142 let auto_assigns = &fields.auto_assigns;
8143 // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
8144 // so the body is structurally identical to `auto_assigns` —
8145 // only the helper name swaps from `try_get_returning` to
8146 // `try_get_returning_sqlite` so the closure typechecks against
8147 // a `SqliteRow` instead of a `PgRow`.
8148 let auto_assigns_sqlite: Vec<TokenStream2> = fields
8149 .auto_field_idents
8150 .iter()
8151 .map(|(ident, column, uuid)| {
8152 // A UUID a column default wrote is TEXT, not the 16 bytes sqlx binds.
8153 if *uuid {
8154 return quote! {
8155 self.#ident = #root::sql::Auto::Set(#root::sql::try_get_flat_sqlite(
8156 _returning_row, #column
8157 )?);
8158 };
8159 }
8160 quote! {
8161 self.#ident = #root::sql::try_get_returning_sqlite(
8162 _returning_row, #column
8163 )?;
8164 }
8165 })
8166 .collect();
8167 // #1028 — decode each `generated_as` column from the same
8168 // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
8169 // own type drives the decode (no `Auto<T>` wrapper).
8170 let generated_assigns: Vec<TokenStream2> = fields
8171 .generated_field_idents
8172 .iter()
8173 .map(|(ident, column)| {
8174 quote! {
8175 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
8176 }
8177 })
8178 .collect();
8179 let generated_assigns_sqlite: Vec<TokenStream2> = fields
8180 .generated_field_idents
8181 .iter()
8182 .map(|(ident, column)| {
8183 quote! {
8184 self.#ident = #root::sql::try_get_returning_sqlite(
8185 _returning_row, #column
8186 )?;
8187 }
8188 })
8189 .collect();
8190 let mysql_body = if let Some((first, value_ty)) = fields.first_db_auto.as_ref() {
8191 // The MySQL `LAST_INSERT_ID()` is always i64. Route through
8192 // `MysqlAutoIdSet` so Auto<i32> narrows safely and
8193 // Auto<Uuid>/etc. fail to link against MySQL (intended —
8194 // those models can't use AUTO_INCREMENT). The trait is only
8195 // touched on the MySQL arm at runtime, so PG-only consumers
8196 // never see the bound failure.
8197 //
8198 // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
8199 // Auto<i64> PK + auto_now_add timestamp) errored hard at
8200 // runtime with "multi-column RETURNING". MySQL has no
8201 // multi-column RETURNING semantic and a follow-up SELECT
8202 // would need cross-trait plumbing. Pragmatic shape: succeed
8203 // with the FIRST Auto field populated from LAST_INSERT_ID();
8204 // any other Auto fields stay `Auto::Unset`. Callers that
8205 // need the DB-defaulted timestamp / UUID can re-fetch the
8206 // row by PK after `save_pool`. Fixes the cookbook chapter
8207 // 12 dialect divergence.
8208 quote! {
8209 let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
8210 ::rustango_from_mysql_auto_id(_id)?;
8211 self.#first = #root::sql::Auto::Set(_converted);
8212 ::core::result::Result::Ok(())
8213 }
8214 } else {
8215 quote! {
8216 let _ = _id;
8217 ::core::result::Result::Ok(())
8218 }
8219 };
8220 quote! {
8221 impl #root::sql::AssignAutoPkPool for #struct_name {
8222 fn __rustango_assign_from_pg_row(
8223 &mut self,
8224 _returning_row: &#root::sql::PgReturningRow,
8225 ) -> ::core::result::Result<(), #root::sql::ExecError> {
8226 #( #auto_assigns )*
8227 #( #generated_assigns )*
8228 ::core::result::Result::Ok(())
8229 }
8230 fn __rustango_assign_from_mysql_id(
8231 &mut self,
8232 _id: i64,
8233 ) -> ::core::result::Result<(), #root::sql::ExecError> {
8234 #mysql_body
8235 }
8236 fn __rustango_assign_from_sqlite_row(
8237 &mut self,
8238 _returning_row: &#root::sql::SqliteReturningRow,
8239 ) -> ::core::result::Result<(), #root::sql::ExecError> {
8240 #( #auto_assigns_sqlite )*
8241 #( #generated_assigns_sqlite )*
8242 ::core::result::Result::Ok(())
8243 }
8244 }
8245 }
8246 };
8247
8248 let from_aliased_row_inits = &fields.from_aliased_row_inits;
8249 let aliased_row_helper = quote! {
8250 /// Decode a row's aliased target columns (produced by
8251 /// `select_related`'s LEFT JOIN) into a fresh instance of
8252 /// this model. Reads each column via
8253 /// `format!("{prefix}__{col}")`, matching the alias the
8254 /// SELECT writer emitted. Slice 9.0d.
8255 #[doc(hidden)]
8256 #[cfg(feature = "postgres")]
8257 pub fn __rustango_from_aliased_row(
8258 row: &#root::sql::sqlx::postgres::PgRow,
8259 prefix: &str,
8260 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8261 ::core::result::Result::Ok(Self {
8262 #( #from_aliased_row_inits ),*
8263 })
8264 }
8265 };
8266 // v0.23.0-batch8 — MySQL counterpart, gated through the
8267 // cfg-aware macro_rules so PG-only builds expand to nothing.
8268 let from_aliased_row_inits_my = &fields.from_aliased_row_inits_my;
8269 let aliased_row_helper_my = quote! {
8270 #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
8271 #( #from_aliased_row_inits_my ),*
8272 });
8273 };
8274
8275 // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
8276 // pattern + cfg gate on the `sqlite` feature.
8277 let aliased_row_helper_sqlite = quote! {
8278 #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
8279 #( #from_aliased_row_inits ),*
8280 });
8281 };
8282
8283 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
8284 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
8285 let load_related_impl_sqlite =
8286 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
8287
8288 // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
8289 // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
8290 // default `Self::objects()`. Each accessor returns a fresh
8291 // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
8292 // methods the user defined.
8293 let extra_manager_fns: Vec<TokenStream2> = manager_fns
8294 .iter()
8295 .map(|fn_ident| {
8296 let model_name_str = struct_name.to_string();
8297 let fn_name_str = fn_ident.to_string();
8298 let doc = format!(
8299 "Custom-named QuerySet accessor for [`{model_name_str}`]. \
8300 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
8301 equivalent to `Self::objects()`. Chains with any \
8302 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
8303 extension methods."
8304 );
8305 quote! {
8306 #[doc = #doc]
8307 #[must_use]
8308 pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
8309 #root::query::QuerySet::new()
8310 }
8311 }
8312 })
8313 .collect();
8314
8315 quote! {
8316 impl #struct_name {
8317 /// Start a new `QuerySet` over this model.
8318 #[must_use]
8319 pub fn objects() -> #root::query::QuerySet<#struct_name> {
8320 #root::query::QuerySet::new()
8321 }
8322
8323 /// Eloquent-shape alias of [`Self::objects`]. Returns
8324 /// a fresh `QuerySet<Self>` ready for `.filter()` /
8325 /// `.where_()` / etc. Matches Laravel muscle-memory:
8326 ///
8327 /// ```ignore
8328 /// // Eloquent: Post::query()->where('published', true)
8329 /// // rustango: Post::query().filter("published", true)
8330 /// // or: Post::objects().filter("published", true)
8331 /// ```
8332 ///
8333 /// Both names point at the same underlying constructor;
8334 /// neither is preferred.
8335 #[must_use]
8336 pub fn query() -> #root::query::QuerySet<#struct_name> {
8337 #root::query::QuerySet::new()
8338 }
8339
8340 #( #extra_manager_fns )*
8341
8342 #insert_method
8343
8344 #bulk_insert_method
8345
8346 #bulk_upsert_pool_method
8347
8348 #bulk_update_method
8349
8350 #save_method
8351
8352 #pk_methods
8353
8354 #pk_value_helper
8355
8356 #aliased_row_helper
8357
8358 #column_consts
8359 }
8360
8361 #aliased_row_helper_my
8362
8363 #aliased_row_helper_sqlite
8364
8365 #load_related_impl
8366
8367 #load_related_impl_my
8368
8369 #load_related_impl_sqlite
8370
8371 #has_pk_value_impl
8372
8373 #fk_pk_access_impl
8374
8375 #assign_auto_pk_pool_impl
8376 }
8377}
8378
8379/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
8380/// `auto_assigns` but reading from `_returning_row` and writing to
8381/// `_row_mut` instead of `self`.
8382fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
8383 let root = rustango_root();
8384 let lines = fields.auto_field_idents.iter().map(|(ident, column, _)| {
8385 let col_lit = column.as_str();
8386 quote! {
8387 _row_mut.#ident = #root::sql::sqlx::Row::try_get(
8388 _returning_row,
8389 #col_lit,
8390 )?;
8391 }
8392 });
8393 quote! { #( #lines )* }
8394}
8395
8396/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
8397fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
8398 let lines = entries.iter().map(|e| {
8399 let ident = &e.ident;
8400 let col_ty = column_type_ident(ident);
8401 quote! {
8402 #[allow(non_upper_case_globals)]
8403 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
8404 }
8405 });
8406 quote! { #(#lines)* }
8407}
8408
8409/// Emit a hidden per-model module carrying one zero-sized type per field,
8410/// each with a `Column` impl pointing back at the model.
8411fn column_module_tokens(
8412 module_ident: &syn::Ident,
8413 struct_name: &syn::Ident,
8414 entries: &[ColumnEntry],
8415) -> TokenStream2 {
8416 let root = rustango_root();
8417 let items = entries.iter().map(|e| {
8418 let col_ty = column_type_ident(&e.ident);
8419 let value_ty = &e.value_ty;
8420 let name = &e.name;
8421 let column = &e.column;
8422 let field_type_tokens = &e.field_type_tokens;
8423 quote! {
8424 #[derive(::core::clone::Clone, ::core::marker::Copy)]
8425 pub struct #col_ty;
8426
8427 impl #root::core::Column for #col_ty {
8428 type Model = super::#struct_name;
8429 type Value = #value_ty;
8430 const NAME: &'static str = #name;
8431 const COLUMN: &'static str = #column;
8432 const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
8433 }
8434 }
8435 });
8436 quote! {
8437 #[doc(hidden)]
8438 #[allow(non_camel_case_types, non_snake_case)]
8439 pub mod #module_ident {
8440 // Re-import the parent scope so field types referencing
8441 // sibling models (e.g. `ForeignKey<Author>`) resolve
8442 // inside this submodule. Without this we'd hit
8443 // `proc_macro_derive_resolution_fallback` warnings.
8444 #[allow(unused_imports)]
8445 use super::*;
8446 #(#items)*
8447 }
8448 }
8449}
8450
8451fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
8452 let name = ident_name(field_ident);
8453 syn::Ident::new(&format!("{name}_col"), field_ident.span())
8454}
8455
8456/// Run `body` (of type `ret`) and put the `auto_now` fields back unless
8457/// `keep` holds for `_saved`, so a failed or no-row write keeps the old stamp.
8458fn restore_auto_now_unless(
8459 idents: &[syn::Ident],
8460 ret: &TokenStream2,
8461 keep: &TokenStream2,
8462 body: &TokenStream2,
8463) -> TokenStream2 {
8464 if idents.is_empty() {
8465 return body.clone();
8466 }
8467 let prev: Vec<syn::Ident> = (0..idents.len())
8468 .map(|i| quote::format_ident!("__rustango_auto_now_prev_{}", i))
8469 .collect();
8470 quote! {
8471 #( let #prev = ::core::clone::Clone::clone(&self.#idents); )*
8472 let _saved: #ret = async { #body }.await;
8473 if !(#keep) {
8474 #( self.#idents = #prev; )*
8475 }
8476 _saved
8477 }
8478}
8479
8480/// A field's name without the `r#` of a raw ident: `r#type` is `type` (#1937).
8481fn ident_name(ident: &syn::Ident) -> String {
8482 syn::ext::IdentExt::unraw(ident).to_string()
8483}
8484
8485/// The ident for a field named `name`, raw when it is a keyword (`type` → `r#type`).
8486fn ident_from_name(name: &str, span: proc_macro2::Span) -> syn::Ident {
8487 match syn::parse_str::<syn::Ident>(name) {
8488 Ok(mut ident) => {
8489 ident.set_span(span);
8490 ident
8491 }
8492 Err(_) => syn::Ident::new_raw(name, span),
8493 }
8494}
8495
8496fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8497 syn::Ident::new(
8498 &format!("__rustango_cols_{struct_name}"),
8499 struct_name.span(),
8500 )
8501}
8502
8503fn from_row_impl_tokens(
8504 struct_name: &syn::Ident,
8505 from_row_inits: &[TokenStream2],
8506 from_row_inits_my: &[TokenStream2],
8507) -> TokenStream2 {
8508 let root = rustango_root();
8509 // The Postgres impl is always emitted — every rustango build pulls in
8510 // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8511 // routed through `#root::__impl_my_from_row!`, a cfg-gated
8512 // macro_rules whose body collapses to nothing when rustango is built
8513 // without the `mysql` feature. No user-facing feature shim required.
8514 //
8515 // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8516 // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8517 // back into a comma-separated list inside square brackets. Since each
8518 // entry is already in `field: expr` shape, the existing tokens slot in.
8519 quote! {
8520 #[cfg(feature = "postgres")]
8521 impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8522 for #struct_name
8523 {
8524 fn from_row(
8525 row: &'r #root::sql::sqlx::postgres::PgRow,
8526 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8527 ::core::result::Result::Ok(Self {
8528 #( #from_row_inits ),*
8529 })
8530 }
8531 }
8532
8533 #root::__impl_my_from_row!(#struct_name, |row| {
8534 #( #from_row_inits_my ),*
8535 });
8536
8537 #root::__impl_sqlite_from_row!(#struct_name, |row| {
8538 #( #from_row_inits ),*
8539 });
8540 }
8541}
8542
8543struct ContainerAttrs {
8544 table: Option<String>,
8545 display: Option<(String, proc_macro2::Span)>,
8546 /// Explicit app label from `#[rustango(app = "blog")]`.
8547 /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8548 /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8549 /// at runtime — this attribute is the override for cases where
8550 /// the inference is wrong (e.g. a model that conceptually belongs
8551 /// to one app but is physically in another module).
8552 app: Option<String>,
8553 /// Per-model admin knobs from
8554 /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8555 /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8556 /// admin code falls back to `AdminConfig::DEFAULT`.
8557 admin: Option<AdminAttrs>,
8558 /// Per-model audit configuration from `#[rustango(audit(...))]`.
8559 /// `None` when the model isn't audited — write paths emit no
8560 /// audit entries. When present, single-row writes capture
8561 /// before/after for the listed fields and bulk writes batch
8562 /// snapshots into one INSERT into `rustango_audit_log`.
8563 audit: Option<AuditAttrs>,
8564 /// `true` when `#[rustango(permissions)]` is present. Signals that
8565 /// `auto_create_permissions` should seed the four CRUD codenames for
8566 /// this model.
8567 permissions: bool,
8568 /// Many-to-many relations declared via
8569 /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8570 /// src = "post_id", dst = "tag_id"))]`.
8571 m2m: Vec<M2MAttr>,
8572 /// Polymorphic M2M relations declared via
8573 /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8574 /// pk_column = "taggable_id", ct_column = "taggable_type",
8575 /// related_column = "tag_id"))]` (issue #818).
8576 generic_m2m: Vec<GenericM2MAttr>,
8577 /// Composite indexes declared via
8578 /// `#[rustango(index("col1, col2"))]` or
8579 /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8580 /// Single-column indexes from `#[rustango(index)]` on fields are
8581 /// accumulated here during field collection.
8582 indexes: Vec<IndexAttr>,
8583 /// Table-level CHECK constraints declared via
8584 /// `#[rustango(check(name = "…", expr = "…"))]`.
8585 checks: Vec<CheckAttr>,
8586 /// Table-level PG `EXCLUDE` constraints declared via
8587 /// `#[rustango(exclude(name = "…", using = "gist", elements =
8588 /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8589 /// migration writer renders them on Postgres and skips with a
8590 /// warning on MySQL/SQLite. Issue #319.
8591 excludes: Vec<ExcludeAttr>,
8592 /// Composite (multi-column) FKs declared via
8593 /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8594 /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8595 composite_fks: Vec<CompositeFkAttr>,
8596 /// Generic ("any model") FKs declared via
8597 /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8598 /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8599 generic_fks: Vec<GenericFkAttr>,
8600 /// Where this model lives in a tenancy deployment, declared via
8601 /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8602 /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8603 /// to partition diff output between registry-scoped and
8604 /// tenant-scoped migration files.
8605 scope: Option<String>,
8606 /// Extra QuerySet accessor names from
8607 /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8608 /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8609 /// the default `Self::objects()`. Multiple attributes allowed.
8610 manager_fns: Vec<syn::Ident>,
8611 /// Default ordering declared via `#[rustango(default_order =
8612 /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8613 /// `(column_name, desc_flag, span_for_error_reporting)` — the
8614 /// `-` prefix means descending; the `+` prefix or no prefix means
8615 /// ascending.
8616 default_order: Vec<(String, bool, proc_macro2::Span)>,
8617 /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8618 /// Routes the emitted schema's `is_view = true` so the migration
8619 /// snapshot skips this model (its underlying SQL view is operator-
8620 /// managed, not rustango-managed).
8621 is_view: bool,
8622 /// `#[rustango(managed = false)]` — who owns the table.
8623 /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8624 /// the migration snapshot skips this model so `makemigrations` /
8625 /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8626 /// TABLE` against it (operator-managed schema).
8627 managed: bool,
8628 /// Name of the model's base manager, from
8629 /// `#[rustango(base_manager_name = "...")]`. Threaded into
8630 /// `ModelSchema::base_manager_name`. Declarative-only today.
8631 base_manager_name: Option<String>,
8632 /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8633 /// field this model's instances are ordered relative to.
8634 /// Declarative-only today; threaded onto
8635 /// `ModelSchema::order_with_respect_to`.
8636 order_with_respect_to: Option<String>,
8637 /// `#[rustango(proxy)]` / `#[rustango(proxy = true)]`.
8638 /// Marks the model as a proxy that
8639 /// shares its DB table with another struct. Threaded into
8640 /// `ModelSchema::proxy` so future codegen can skip table-owning
8641 /// behavior for proxies.
8642 proxy: bool,
8643 /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8644 /// Each comma-separated capability token surfaces on
8645 /// `ModelSchema::required_db_features` so `manage check --deploy`
8646 /// can warn when the active dialect lacks one.
8647 required_db_features: Vec<String>,
8648 /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8649 /// Normalized to the dialect name `manage check --deploy`
8650 /// compares against `Settings.database.backend`. Aliases
8651 /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8652 /// stored under the canonical name.
8653 required_db_vendor: Option<String>,
8654 /// `#[rustango(default_related_name = "...")]`. Threaded into
8655 /// `ModelSchema::default_related_name`. Reverse-relation accessor
8656 /// name to use when an FK / M2M field doesn't override it.
8657 /// Today rustango doesn't auto-emit reverse managers; the
8658 /// metadata is the foundation for that work.
8659 default_related_name: Option<String>,
8660 /// `#[rustango(db_table_comment = "...")]`. Threaded into
8661 /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8662 /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8663 /// inline `COMMENT='...'`, SQLite: no-op).
8664 db_table_comment: Option<String>,
8665 /// `#[rustango(get_latest_by = "created_at")]` /
8666 /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8667 /// `(column, descending)` where `descending = true` when the
8668 /// attribute value starts with `-`. Threaded into
8669 /// `ModelSchema::get_latest_by`.
8670 get_latest_by: Option<(String, bool, proc_macro2::Span)>,
8671 /// Extra `(codename, label)` permissions
8672 /// from `#[rustango(extra_permissions = "approve:Can approve,
8673 /// archive:Can archive")]`. Comma-separated `codename:label`
8674 /// pairs. Threaded into `ModelSchema::extra_permissions`.
8675 extra_permissions: Vec<(String, String)>,
8676 /// Which CRUD codenames (`"add"` / `"change"` / `"delete"` /
8677 /// `"view"`) the framework
8678 /// auto-creates. Empty `Vec` (default) means **all four**. Set via
8679 /// `#[rustango(default_permissions = "view,change")]` to opt out.
8680 /// Validated at parse time; unknown actions fail with a span-pointing
8681 /// error.
8682 default_permissions: Vec<String>,
8683 /// `#[rustango(verbose_name = "blog post")]` — the
8684 /// human-readable singular label for the model. Threaded into
8685 /// `ModelSchema::verbose_name` so admin section headers /
8686 /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8687 /// over the Rust struct identifier.
8688 verbose_name: Option<String>,
8689 /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8690 /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8691 /// When unset, `display_label_plural()` auto-suffixes `s`.
8692 verbose_name_plural: Option<String>,
8693 /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8694 /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8695 /// a name (used by `QuerySet::without_global_scope`) with a
8696 /// `fn() -> WhereExpr` path that the macro emits into
8697 /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8698 global_scopes: Vec<GlobalScopeAttr>,
8699 /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8700 /// `#[rustango(through(name, far, far_fk_column, intermediate,
8701 /// intermediate_fk_column, intermediate_pk_column))]` — issue
8702 /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8703 /// entry emits an inherent `<name>_through(&self)` accessor that
8704 /// returns a `QuerySet<Far>` filtered via a correlated subquery
8705 /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8706 /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8707 through_relations: Vec<ThroughAttr>,
8708 /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8709 /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8710 /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8711 /// Each entry emits two associated functions on the parent —
8712 /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8713 /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8714 /// over a correlated subquery against the child table. Users
8715 /// drop the result into `QuerySet::where_raw(...)`.
8716 reverse_has_relations: Vec<ReverseHasAttr>,
8717 /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8718 /// issue #830. Each emits a `Model::generic_reverse_relations()`
8719 /// entry so the relation-existence family resolves polymorphic
8720 /// children by name.
8721 generic_has_relations: Vec<GenericHasAttr>,
8722}
8723
8724/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8725/// declaration. Each entry becomes one `core::GlobalScope` in the
8726/// emitted schema literal; `apply` resolves at macro-expand time
8727/// against the consumer's scope so the function must be in scope at
8728/// the use site. Issue #820.
8729struct GlobalScopeAttr {
8730 name: String,
8731 apply: syn::Path,
8732}
8733
8734/// Parsed `#[rustango(through(...))]` declaration. Issue
8735/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8736/// `hasManyThrough` / `hasOneThrough` parity.
8737///
8738/// `Country hasManyThrough Post via User` declares as:
8739///
8740/// ```ignore
8741/// #[rustango(through(
8742/// name = "posts",
8743/// far = "Post",
8744/// far_fk_column = "author_id",
8745/// intermediate = "User",
8746/// intermediate_fk_column = "country_id",
8747/// ))]
8748/// struct Country { ... }
8749/// ```
8750///
8751/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8752/// which returns a queryset filtered via a correlated subquery —
8753/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8754/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8755/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8756/// because the subquery lives inside a `WhereExpr::InSubquery` node
8757/// and the outer queryset's pending list stays empty.
8758///
8759/// All four required arguments use **SQL column / table names**
8760/// (not Rust field names) to sidestep the multi-hop-filter substrate
8761/// gap. Once that substrate lands, a higher-level Rust-field-name
8762/// shorthand can be added without breaking this surface.
8763struct ThroughAttr {
8764 /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8765 name: String,
8766 /// Far model type identifier. `far = "Post"` → returns
8767 /// `QuerySet<Post>`. Resolved verbatim against the scope where
8768 /// the derive expands.
8769 far: syn::Ident,
8770 /// SQL column on the far model's table that references the
8771 /// intermediate model's primary key. For `Post`'s
8772 /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8773 /// default `<field>_id` convention) or whatever the user
8774 /// declared via `#[rustango(db_column = "...")]`.
8775 far_fk_column: String,
8776 /// Intermediate model type identifier. Needed to look up its
8777 /// `SCHEMA` so the subquery's `FROM` clause points at the
8778 /// intermediate table. `intermediate = "User"`.
8779 intermediate: syn::Ident,
8780 /// SQL column on the intermediate's table that references the
8781 /// source (this) model's primary key. For `User`'s
8782 /// `country: ForeignKey<Country>` the column is `"country_id"`.
8783 intermediate_fk_column: String,
8784 /// SQL primary-key column on the intermediate's table — the
8785 /// column the subquery projects. Optional; defaults to `"id"`
8786 /// (rustango's default PK column name). Override when the
8787 /// intermediate declares a custom PK column.
8788 intermediate_pk_column: String,
8789}
8790
8791/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8792/// child_fk_column = "..."))]` declaration. Issue
8793/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8794/// `whereHas` / `whereDoesntHave` parity.
8795///
8796/// `Post hasMany Comment` declares as:
8797///
8798/// ```ignore
8799/// #[rustango(reverse_has(
8800/// name = "comments",
8801/// child = "Comment",
8802/// child_fk_column = "post_id",
8803/// ))]
8804/// struct Post { ... }
8805/// ```
8806///
8807/// The macro emits two associated functions on `Post`:
8808///
8809/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8810/// `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8811/// <outer>.<self_pk_column>)`.
8812/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8813/// but `NOT EXISTS`, the `whereDoesntHave` analog.
8814///
8815/// User code:
8816///
8817/// ```ignore
8818/// // Posts with at least one comment:
8819/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8820/// // Posts with no comments:
8821/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8822/// ```
8823///
8824/// As with #817, all column / table identifiers are **SQL names**
8825/// (not Rust field names) so the substrate is independent of the
8826/// outstanding multi-hop filter resolver gap. The emitted
8827/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8828/// SQL-emit time via the writer's scope stack.
8829struct ReverseHasAttr {
8830 /// Accessor name. `name = "comments"` → emits
8831 /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8832 name: String,
8833 /// Child model type identifier. `child = "Comment"` — needed to
8834 /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8835 /// points at the child table.
8836 child: syn::Ident,
8837 /// SQL column on the child's table that references this model's
8838 /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8839 /// column is `"post_id"`.
8840 child_fk_column: String,
8841 /// Column on **this** model's table the child FK references — the
8842 /// `OuterRef` target. Defaults to the PK column (`"id"` without one).
8843 self_pk_column: Option<String>,
8844}
8845
8846impl ReverseHasAttr {
8847 fn self_pk_column(&self) -> &str {
8848 self.self_pk_column.as_deref().unwrap_or("id")
8849 }
8850}
8851
8852/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8853/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8854/// relation-existence family (issue #830). The child is a polymorphic,
8855/// content-type-discriminated model; emits a `Model::
8856/// generic_reverse_relations()` entry the queryset resolves by name.
8857struct GenericHasAttr {
8858 /// Accessor name, e.g. `name = "tags"`.
8859 name: String,
8860 /// Child model type identifier (`child = "Tag"`) — looked up for its
8861 /// `SCHEMA` so the subquery's `FROM` points at the child table.
8862 child: syn::Ident,
8863 /// Column on the child table holding the parent's content-type id.
8864 /// Optional; defaults to `"content_type_id"`.
8865 ct_column: String,
8866 /// Column on the child table holding the parent's PK value.
8867 /// Optional; defaults to `"object_pk"`.
8868 pk_column: String,
8869 /// SQL primary-key column on **this** (parent) model's table.
8870 /// Defaults to the PK column (`"id"` without one).
8871 self_pk_column: Option<String>,
8872}
8873
8874impl GenericHasAttr {
8875 fn self_pk_column(&self) -> &str {
8876 self.self_pk_column.as_deref().unwrap_or("id")
8877 }
8878}
8879
8880/// Parsed form of one index declaration (field-level or container-level).
8881struct IndexAttr {
8882 /// The declaring attribute, for diagnostics.
8883 span: proc_macro2::Span,
8884 /// Index name; auto-derived when `None` at parse time.
8885 name: Option<String>,
8886 /// Column names in the index.
8887 columns: Vec<String>,
8888 /// `true` for `CREATE UNIQUE INDEX`.
8889 unique: bool,
8890 /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8891 /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8892 /// `"btree"` when the attribute is absent — the DDL writer omits
8893 /// the `USING` clause and the backend uses its own default
8894 /// (btree on every supported dialect).
8895 method: String,
8896 /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8897 /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8898 /// condition = "...", name = "..."))]`. `None` for plain indexes.
8899 where_clause: Option<String>,
8900 /// Covering-index columns (PG 11+ `INCLUDE (...)` clause).
8901 /// Empty `Vec` (the default) means "no covering columns".
8902 include: Vec<String>,
8903}
8904
8905/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8906struct CheckAttr {
8907 name: String,
8908 expr: String,
8909}
8910
8911/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8912/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8913/// PG-only — surfaced on every backend in the macro emit; the migration
8914/// writer skips the op on MySQL/SQLite. Issue #319.
8915struct ExcludeAttr {
8916 /// Constraint name (free-form Rust identifier).
8917 name: String,
8918 /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8919 using: String,
8920 /// Comma-separated `(column, operator)` pairs, in declaration
8921 /// order. Parsed from `"col WITH op, col WITH op"`.
8922 elements: Vec<(String, String)>,
8923 /// Optional `WHERE` predicate (raw SQL).
8924 where_clause: Option<String>,
8925}
8926
8927/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8928/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8929/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8930/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8931/// the model, not the field.
8932struct CompositeFkAttr {
8933 /// Logical relation name (free-form Rust identifier).
8934 name: String,
8935 /// SQL table name of the target.
8936 to: String,
8937 /// Source-side column names, in declaration order.
8938 from: Vec<String>,
8939 /// Target-side column names, same length / order as `from`.
8940 on: Vec<String>,
8941}
8942
8943/// Parsed form of one `#[rustango(generic_fk(name = "target",
8944/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8945/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8946/// generic ("any model") FKs live on the model, not the field.
8947struct GenericFkAttr {
8948 /// Logical relation name (free-form Rust identifier).
8949 name: String,
8950 /// Source-side column carrying the `content_type_id` value.
8951 ct_column: String,
8952 /// Source-side column carrying the target row's primary key.
8953 pk_column: String,
8954}
8955
8956/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8957struct M2MAttr {
8958 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8959 name: String,
8960 /// Target table (e.g. `"app_tags"`).
8961 to: String,
8962 /// Junction table (e.g. `"post_tags"`).
8963 through: String,
8964 /// Source FK column in the junction table (e.g. `"post_id"`).
8965 src: String,
8966 /// Destination FK column in the junction table (e.g. `"tag_id"`).
8967 dst: String,
8968 /// Whether the migration writer should auto-create the junction
8969 /// table. Default `true`. Set `auto_create = false` (#324) when
8970 /// the operator declares the through table as its own
8971 /// `#[derive(Model)]` struct with extra columns.
8972 auto_create: bool,
8973}
8974
8975/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8976/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8977/// junction carries a ContentType discriminator so unrelated models
8978/// share one pivot + related set.
8979struct GenericM2MAttr {
8980 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8981 name: String,
8982 /// Polymorphic junction table (e.g. `"taggables"`).
8983 through: String,
8984 /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8985 pk_column: String,
8986 /// Junction column holding the owning model's `content_type_id`
8987 /// discriminator (e.g. `"taggable_type"`).
8988 ct_column: String,
8989 /// Junction column holding the related model PK (e.g. `"tag_id"`).
8990 related_column: String,
8991}
8992
8993/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8994/// "user"))]`. `track` is a comma-separated list of field names whose
8995/// before/after values land in the JSONB `changes` column. `source`
8996/// is informational only — it pins a default source when the model
8997/// is written outside any `audit::with_source(...)` scope (rare).
8998#[derive(Default)]
8999struct AuditAttrs {
9000 /// Field names to capture in the `changes` JSONB. Validated
9001 /// against declared scalar fields at compile time. Empty means
9002 /// "track every scalar field".
9003 track: Option<(Vec<String>, proc_macro2::Span)>,
9004}
9005
9006/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
9007/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
9008/// comma-separated strings; we validate each ident against the model's
9009/// declared fields at compile time.
9010#[derive(Default)]
9011struct AdminAttrs {
9012 list_display: Option<(Vec<String>, proc_macro2::Span)>,
9013 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
9014 list_per_page: Option<usize>,
9015 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
9016 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
9017 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
9018 /// Bulk action names. No field-validation against model fields —
9019 /// these are action handlers, not column references.
9020 actions: Option<(Vec<String>, proc_macro2::Span)>,
9021 /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
9022 /// sections, comma-separated fields per section, optional
9023 /// `Title:` prefix. Empty title omits the `<legend>`.
9024 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
9025 /// `admin(list_display_links = "title")`. Names
9026 /// from `list_display` whose cells should link to detail/edit.
9027 /// Issue #350.
9028 list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
9029 /// `admin(search_help_text = "...")`. Short
9030 /// caption rendered beside the admin list view's search box.
9031 /// Issue #353.
9032 search_help_text: Option<String>,
9033 /// `admin(actions_on_top = false)`. Hides the
9034 /// action-bar above the table. Default `true`. Issue #354.
9035 actions_on_top: Option<bool>,
9036 /// `admin(actions_on_bottom = true)`. Renders an
9037 /// additional action-bar below the table. Default `false`.
9038 /// Issue #354.
9039 actions_on_bottom: Option<bool>,
9040 /// `admin(date_hierarchy = "created_at")`. Name of
9041 /// a date / datetime field whose values render as a clickable
9042 /// year / month / day drill-down strip above the list table.
9043 /// Empty / unset disables the strip. Issue #355.
9044 date_hierarchy: Option<String>,
9045 /// `admin(prepopulated_fields = "slug:title")`.
9046 /// Each entry is `target:source[+source2]`; multiple entries are
9047 /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
9048 /// The admin change-form emits JS that slugifies the source values
9049 /// into the target field on every keystroke. Issue #356.
9050 prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
9051 /// `admin(raw_id_fields = "parent, owner")`. Names
9052 /// of FK fields whose change-form widget renders a lookup link
9053 /// next to the input. Issue #357.
9054 raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
9055 /// `admin(autocomplete_fields = "author_id")`.
9056 /// Names of FK fields whose change-form widget renders an
9057 /// Ajax-driven typeahead populated from a `__autocomplete`
9058 /// endpoint on the target model. Issue #358.
9059 autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
9060 /// `admin(list_select_related = "all" | "none" | "author, …")`
9061 /// — tunes the admin list view's FK auto-JOIN
9062 /// policy. Default `"all"` matches rustango's join-everything
9063 /// behavior; `"none"` opts out; CSV restricts. Issue #352.
9064 list_select_related: Option<String>,
9065 /// `admin(formfield_overrides = "field:widget, field2:widget2")`.
9066 /// Each entry is `field_name:widget_name`; multiple
9067 /// entries comma-separated. Empty / unset → no overrides. The
9068 /// list of widget names supported is documented on
9069 /// `AdminConfig::formfield_overrides`. Issue #359.
9070 formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
9071}
9072
9073/// The names `#[derive(Model)]` reads its attributes under: `rgo` is a
9074/// short alias of `rustango` with the same meaning.
9075#[derive(Clone, Copy, PartialEq, Eq)]
9076enum ModelAttrName {
9077 Rustango,
9078 Rgo,
9079}
9080
9081impl ModelAttrName {
9082 fn of(attr: &syn::Attribute) -> Option<Self> {
9083 let path = attr.path();
9084 if path.is_ident("rustango") {
9085 Some(Self::Rustango)
9086 } else if path.is_ident("rgo") {
9087 Some(Self::Rgo)
9088 } else {
9089 None
9090 }
9091 }
9092
9093 fn as_str(self) -> &'static str {
9094 match self {
9095 Self::Rustango => "rustango",
9096 Self::Rgo => "rgo",
9097 }
9098 }
9099}
9100
9101/// The model attributes in `attrs`, under either name. One item or field
9102/// uses one name, so a mix reads as a mistake rather than a merge.
9103fn model_attrs(attrs: &[syn::Attribute]) -> syn::Result<Vec<&syn::Attribute>> {
9104 let mut first: Option<ModelAttrName> = None;
9105 let mut out = Vec::new();
9106 for attr in attrs {
9107 let Some(name) = ModelAttrName::of(attr) else {
9108 continue;
9109 };
9110 match first {
9111 Some(seen) if seen != name => {
9112 return Err(syn::Error::new_spanned(
9113 attr.path(),
9114 format!(
9115 "`#[{}(...)]` and `#[{}(...)]` are the same attribute; \
9116 use one name on an item or field",
9117 seen.as_str(),
9118 name.as_str(),
9119 ),
9120 ));
9121 }
9122 _ => first = Some(name),
9123 }
9124 out.push(attr);
9125 }
9126 Ok(out)
9127}
9128
9129fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
9130 let mut out = ContainerAttrs {
9131 table: None,
9132 display: None,
9133 app: None,
9134 admin: None,
9135 audit: None,
9136 // Default `permissions = true` so every `#[derive(Model)]`
9137 // gets the four CRUD codenames seeded by `auto_create_permissions`
9138 // and is visible to non-superusers in the tenant admin without
9139 // manual per-model annotation. Models that intentionally don't
9140 // want permission rows (registry-internal types, framework
9141 // tables operators shouldn't manage directly) opt out via
9142 // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
9143 // out-of-the-box admin invisibility regression (#62).
9144 permissions: true,
9145 m2m: Vec::new(),
9146 generic_m2m: Vec::new(),
9147 indexes: Vec::new(),
9148 checks: Vec::new(),
9149 excludes: Vec::new(),
9150 composite_fks: Vec::new(),
9151 generic_fks: Vec::new(),
9152 scope: None,
9153 manager_fns: Vec::new(),
9154 default_order: Vec::new(),
9155 is_view: false,
9156 managed: true,
9157 verbose_name: None,
9158 verbose_name_plural: None,
9159 base_manager_name: None,
9160 order_with_respect_to: None,
9161 proxy: false,
9162 required_db_features: Vec::new(),
9163 required_db_vendor: None,
9164 default_related_name: None,
9165 db_table_comment: None,
9166 get_latest_by: None,
9167 extra_permissions: Vec::new(),
9168 default_permissions: Vec::new(),
9169 global_scopes: Vec::new(),
9170 through_relations: Vec::new(),
9171 reverse_has_relations: Vec::new(),
9172 generic_has_relations: Vec::new(),
9173 };
9174 for attr in model_attrs(&input.attrs)? {
9175 // The name the user wrote, for hints that quote the attribute.
9176 let written = ModelAttrName::of(attr).map_or("rustango", ModelAttrName::as_str);
9177 attr.parse_nested_meta(|meta| {
9178 if meta.path.is_ident("table") {
9179 let s: LitStr = meta.value()?.parse()?;
9180 let name = s.value();
9181 // v0.27.3 (#65) — macro-time guard against table names
9182 // that compile but break SQL downstream. Hyphens are
9183 // the common footgun: PostgreSQL accepts a quoted
9184 // `"intermediate-region"` in CREATE TABLE, but the
9185 // FK / index name derivation in `migrate::ddl`
9186 // emits `intermediate-region_field_fkey` unquoted,
9187 // which then fails the SQL parser. Same shape rule
9188 // as Postgres regular identifiers so the safe path
9189 // is the only path.
9190 validate_table_name(&name, s.span())?;
9191 out.table = Some(name);
9192 return Ok(());
9193 }
9194 if meta.path.is_ident("display") {
9195 let s: LitStr = meta.value()?.parse()?;
9196 out.display = Some((s.value(), s.span()));
9197 return Ok(());
9198 }
9199 if meta.path.is_ident("app") {
9200 let s: LitStr = meta.value()?.parse()?;
9201 out.app = Some(s.value());
9202 return Ok(());
9203 }
9204 if meta.path.is_ident("scope") {
9205 let s: LitStr = meta.value()?.parse()?;
9206 let val = s.value();
9207 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
9208 return Err(meta.error(format!(
9209 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
9210 )));
9211 }
9212 out.scope = Some(val);
9213 return Ok(());
9214 }
9215 if meta.path.is_ident("admin") {
9216 let mut admin = AdminAttrs::default();
9217 meta.parse_nested_meta(|inner| {
9218 if inner.path.is_ident("list_display") {
9219 let s: LitStr = inner.value()?.parse()?;
9220 admin.list_display =
9221 Some((split_field_list(&s.value()), s.span()));
9222 return Ok(());
9223 }
9224 if inner.path.is_ident("search_fields") {
9225 let s: LitStr = inner.value()?.parse()?;
9226 admin.search_fields =
9227 Some((split_field_list(&s.value()), s.span()));
9228 return Ok(());
9229 }
9230 if inner.path.is_ident("readonly_fields") {
9231 let s: LitStr = inner.value()?.parse()?;
9232 admin.readonly_fields =
9233 Some((split_field_list(&s.value()), s.span()));
9234 return Ok(());
9235 }
9236 if inner.path.is_ident("list_per_page") {
9237 let lit: syn::LitInt = inner.value()?.parse()?;
9238 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
9239 return Ok(());
9240 }
9241 if inner.path.is_ident("ordering") {
9242 let s: LitStr = inner.value()?.parse()?;
9243 admin.ordering = Some((
9244 parse_ordering_list(&s.value()),
9245 s.span(),
9246 ));
9247 return Ok(());
9248 }
9249 if inner.path.is_ident("list_filter") {
9250 let s: LitStr = inner.value()?.parse()?;
9251 admin.list_filter =
9252 Some((split_field_list(&s.value()), s.span()));
9253 return Ok(());
9254 }
9255 if inner.path.is_ident("actions") {
9256 let s: LitStr = inner.value()?.parse()?;
9257 admin.actions =
9258 Some((split_field_list(&s.value()), s.span()));
9259 return Ok(());
9260 }
9261 if inner.path.is_ident("fieldsets") {
9262 let s: LitStr = inner.value()?.parse()?;
9263 admin.fieldsets =
9264 Some((parse_fieldset_list(&s.value()), s.span()));
9265 return Ok(());
9266 }
9267 if inner.path.is_ident("list_display_links") {
9268 let s: LitStr = inner.value()?.parse()?;
9269 admin.list_display_links =
9270 Some((split_field_list(&s.value()), s.span()));
9271 return Ok(());
9272 }
9273 if inner.path.is_ident("search_help_text") {
9274 let s: LitStr = inner.value()?.parse()?;
9275 admin.search_help_text = Some(s.value());
9276 return Ok(());
9277 }
9278 if inner.path.is_ident("actions_on_top") {
9279 let lit: syn::LitBool = inner.value()?.parse()?;
9280 admin.actions_on_top = Some(lit.value);
9281 return Ok(());
9282 }
9283 if inner.path.is_ident("actions_on_bottom") {
9284 let lit: syn::LitBool = inner.value()?.parse()?;
9285 admin.actions_on_bottom = Some(lit.value);
9286 return Ok(());
9287 }
9288 if inner.path.is_ident("date_hierarchy") {
9289 let s: LitStr = inner.value()?.parse()?;
9290 admin.date_hierarchy = Some(s.value());
9291 return Ok(());
9292 }
9293 if inner.path.is_ident("prepopulated_fields") {
9294 let s: LitStr = inner.value()?.parse()?;
9295 admin.prepopulated_fields =
9296 Some((parse_prepopulated_list(&s.value()), s.span()));
9297 return Ok(());
9298 }
9299 if inner.path.is_ident("raw_id_fields") {
9300 let s: LitStr = inner.value()?.parse()?;
9301 admin.raw_id_fields =
9302 Some((split_field_list(&s.value()), s.span()));
9303 return Ok(());
9304 }
9305 if inner.path.is_ident("autocomplete_fields") {
9306 let s: LitStr = inner.value()?.parse()?;
9307 admin.autocomplete_fields =
9308 Some((split_field_list(&s.value()), s.span()));
9309 return Ok(());
9310 }
9311 if inner.path.is_ident("list_select_related") {
9312 let s: LitStr = inner.value()?.parse()?;
9313 admin.list_select_related = Some(s.value());
9314 return Ok(());
9315 }
9316 if inner.path.is_ident("formfield_overrides") {
9317 let s: LitStr = inner.value()?.parse()?;
9318 admin.formfield_overrides =
9319 Some((parse_formfield_overrides(&s.value()), s.span()));
9320 return Ok(());
9321 }
9322 Err(inner.error(
9323 "unknown admin attribute (supported: \
9324 `list_display`, `list_display_links`, \
9325 `search_fields`, `search_help_text`, \
9326 `readonly_fields`, \
9327 `list_filter`, `list_per_page`, `ordering`, `actions`, \
9328 `actions_on_top`, `actions_on_bottom`, \
9329 `date_hierarchy`, \
9330 `prepopulated_fields`, \
9331 `raw_id_fields`, \
9332 `autocomplete_fields`, \
9333 `list_select_related`, \
9334 `formfield_overrides`, \
9335 `fieldsets`)",
9336 ))
9337 })?;
9338 out.admin = Some(admin);
9339 return Ok(());
9340 }
9341 if meta.path.is_ident("manager") {
9342 // Its empty trait could not take methods (#2132).
9343 return Err(meta.error(
9344 "`manager(ext = ...)` was removed (#2132): declare your own \
9345 `trait FooManagerExt: Sized { … }` and implement it for `QuerySet<Foo>`",
9346 ));
9347 }
9348 if meta.path.is_ident("manager_fn") {
9349 // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
9350 // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
9351 // next to the default `Self::objects()`. Multiple
9352 // attributes accumulate.
9353 let s: LitStr = meta.value()?.parse()?;
9354 let name = s.value();
9355 if name.is_empty() {
9356 return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
9357 }
9358 if name == "objects" {
9359 return Err(meta.error(
9360 "`manager_fn = \"objects\"` collides with the default \
9361 accessor — pick a different name",
9362 ));
9363 }
9364 let ident = syn::Ident::new(&name, s.span());
9365 if out.manager_fns.iter().any(|prev| *prev == ident) {
9366 return Err(meta.error(format!(
9367 "duplicate `manager_fn = \"{name}\"`"
9368 )));
9369 }
9370 out.manager_fns.push(ident);
9371 return Ok(());
9372 }
9373 if meta.path.is_ident("default_order") {
9374 // `#[rustango(default_order = "-created_at, status")]`
9375 // — issue #291 / T2.5. Comma-separated list; `-prefix`
9376 // means descending, `+prefix` or bare name means ascending.
9377 // Per-query opt-in via `QuerySet::with_default_order()`.
9378 let s: LitStr = meta.value()?.parse()?;
9379 let raw = s.value();
9380 let span = s.span();
9381 let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
9382 Vec::new();
9383 for entry in raw.split(',') {
9384 let trimmed = entry.trim();
9385 if trimmed.is_empty() {
9386 return Err(syn::Error::new(
9387 span,
9388 "`default_order = \"...\"` has an empty entry — \
9389 check for a stray comma",
9390 ));
9391 }
9392 let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
9393 (true, rest.trim().to_owned())
9394 } else if let Some(rest) = trimmed.strip_prefix('+') {
9395 (false, rest.trim().to_owned())
9396 } else {
9397 (false, trimmed.to_owned())
9398 };
9399 if name.is_empty() {
9400 return Err(syn::Error::new(
9401 span,
9402 "`default_order` entry has no column name after the prefix",
9403 ));
9404 }
9405 if parsed.iter().any(|(n, _, _)| *n == name) {
9406 return Err(syn::Error::new(
9407 span,
9408 format!("duplicate column `{name}` in `default_order`"),
9409 ));
9410 }
9411 parsed.push((name, desc, span));
9412 }
9413 if parsed.is_empty() {
9414 return Err(syn::Error::new(
9415 span,
9416 "`default_order = \"...\"` cannot be empty",
9417 ));
9418 }
9419 out.default_order = parsed;
9420 return Ok(());
9421 }
9422 if meta.path.is_ident("global_scope") {
9423 // `#[rustango(global_scope(name = "active", apply =
9424 // path::to::fn))]` — issue #820. The apply function
9425 // path resolves at macro-expand time in the consumer's
9426 // scope; the macro re-emits it verbatim into the
9427 // `ModelSchema::global_scopes` slice literal.
9428 let span = meta.path.span();
9429 let mut scope_name: Option<String> = None;
9430 let mut apply_path: Option<syn::Path> = None;
9431 meta.parse_nested_meta(|inner| {
9432 if inner.path.is_ident("name") {
9433 let s: LitStr = inner.value()?.parse()?;
9434 let raw = s.value();
9435 if raw.trim().is_empty() {
9436 return Err(syn::Error::new(
9437 s.span(),
9438 "`global_scope(name = \"...\")` must not be empty",
9439 ));
9440 }
9441 scope_name = Some(raw);
9442 return Ok(());
9443 }
9444 if inner.path.is_ident("apply") {
9445 let p: syn::Path = inner.value()?.parse()?;
9446 apply_path = Some(p);
9447 return Ok(());
9448 }
9449 Err(inner.error(
9450 "unknown `global_scope` attribute (supported: \
9451 `name`, `apply`)",
9452 ))
9453 })?;
9454 let Some(name) = scope_name else {
9455 return Err(syn::Error::new(
9456 span,
9457 "`global_scope` requires `name = \"...\"`",
9458 ));
9459 };
9460 let Some(apply) = apply_path else {
9461 return Err(syn::Error::new(
9462 span,
9463 "`global_scope` requires `apply = fn_path`",
9464 ));
9465 };
9466 if out.global_scopes.iter().any(|s| s.name == name) {
9467 return Err(syn::Error::new(
9468 span,
9469 format!(
9470 "duplicate global scope name `{name}` — \
9471 pick a unique identifier so \
9472 `QuerySet::without_global_scope(\"{name}\")` \
9473 is unambiguous"
9474 ),
9475 ));
9476 }
9477 out.global_scopes.push(GlobalScopeAttr { name, apply });
9478 return Ok(());
9479 }
9480 if meta.path.is_ident("through") {
9481 // `#[rustango(through(name = "posts", far = "Post",
9482 // far_fk_column = "author_id", intermediate = "User",
9483 // intermediate_fk_column = "country_id"
9484 // [, intermediate_pk_column = "id"]))]` — issue #817.
9485 let span = meta.path.span();
9486 let mut accessor_name: Option<String> = None;
9487 let mut far_ident: Option<syn::Ident> = None;
9488 let mut far_fk_column: Option<String> = None;
9489 let mut intermediate_ident: Option<syn::Ident> = None;
9490 let mut intermediate_fk_column: Option<String> = None;
9491 let mut intermediate_pk_column: Option<String> = None;
9492 fn parse_nonempty_string(
9493 inner: &syn::meta::ParseNestedMeta<'_>,
9494 field: &str,
9495 ) -> syn::Result<String> {
9496 let s: LitStr = inner.value()?.parse()?;
9497 let raw = s.value();
9498 let trimmed = raw.trim();
9499 if trimmed.is_empty() {
9500 return Err(syn::Error::new(
9501 s.span(),
9502 format!("`through({field} = \"...\")` must not be empty"),
9503 ));
9504 }
9505 Ok(trimmed.to_owned())
9506 }
9507 meta.parse_nested_meta(|inner| {
9508 if inner.path.is_ident("name") {
9509 accessor_name = Some(parse_nonempty_string(&inner, "name")?);
9510 return Ok(());
9511 }
9512 if inner.path.is_ident("far") {
9513 // Far model name accepted as a string literal
9514 // so the attribute fits inside the existing
9515 // `parse_nested_meta` shape; parsed back into a
9516 // `syn::Ident` so the emitted accessor can
9517 // reference the type directly.
9518 let s: LitStr = inner.value()?.parse()?;
9519 let raw = s.value();
9520 let trimmed = raw.trim();
9521 if trimmed.is_empty() {
9522 return Err(syn::Error::new(
9523 s.span(),
9524 "`through(far = \"...\")` must not be empty",
9525 ));
9526 }
9527 far_ident = Some(syn::Ident::new(trimmed, s.span()));
9528 return Ok(());
9529 }
9530 if inner.path.is_ident("far_fk_column") {
9531 far_fk_column =
9532 Some(parse_nonempty_string(&inner, "far_fk_column")?);
9533 return Ok(());
9534 }
9535 if inner.path.is_ident("intermediate") {
9536 let s: LitStr = inner.value()?.parse()?;
9537 let raw = s.value();
9538 let trimmed = raw.trim();
9539 if trimmed.is_empty() {
9540 return Err(syn::Error::new(
9541 s.span(),
9542 "`through(intermediate = \"...\")` must not be empty",
9543 ));
9544 }
9545 intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9546 return Ok(());
9547 }
9548 if inner.path.is_ident("intermediate_fk_column") {
9549 intermediate_fk_column =
9550 Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9551 return Ok(());
9552 }
9553 if inner.path.is_ident("intermediate_pk_column") {
9554 intermediate_pk_column =
9555 Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9556 return Ok(());
9557 }
9558 Err(inner.error(
9559 "unknown `through` attribute (supported: \
9560 `name`, `far`, `far_fk_column`, \
9561 `intermediate`, `intermediate_fk_column`, \
9562 `intermediate_pk_column`)",
9563 ))
9564 })?;
9565 let Some(name) = accessor_name else {
9566 return Err(syn::Error::new(
9567 span,
9568 "`through` requires `name = \"...\"`",
9569 ));
9570 };
9571 let Some(far) = far_ident else {
9572 return Err(syn::Error::new(
9573 span,
9574 "`through` requires `far = \"FarModelType\"`",
9575 ));
9576 };
9577 let Some(far_fk_column) = far_fk_column else {
9578 return Err(syn::Error::new(
9579 span,
9580 "`through` requires `far_fk_column = \"<column>\"`",
9581 ));
9582 };
9583 let Some(intermediate) = intermediate_ident else {
9584 return Err(syn::Error::new(
9585 span,
9586 "`through` requires `intermediate = \"IntermediateModelType\"`",
9587 ));
9588 };
9589 let Some(intermediate_fk_column) = intermediate_fk_column else {
9590 return Err(syn::Error::new(
9591 span,
9592 "`through` requires `intermediate_fk_column = \"<column>\"`",
9593 ));
9594 };
9595 let intermediate_pk_column =
9596 intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9597 if out.through_relations.iter().any(|t| t.name == name) {
9598 return Err(syn::Error::new(
9599 span,
9600 format!(
9601 "duplicate `through(name = \"{name}\")` — \
9602 pick a unique accessor name"
9603 ),
9604 ));
9605 }
9606 out.through_relations.push(ThroughAttr {
9607 name,
9608 far,
9609 far_fk_column,
9610 intermediate,
9611 intermediate_fk_column,
9612 intermediate_pk_column,
9613 });
9614 return Ok(());
9615 }
9616 if meta.path.is_ident("reverse_has") {
9617 // `#[rustango(reverse_has(name = "comments",
9618 // child = "Comment", child_fk_column = "post_id"
9619 // [, self_pk_column = "id"]))]` — issue #830.
9620 let span = meta.path.span();
9621 let mut accessor_name: Option<String> = None;
9622 let mut child_ident: Option<syn::Ident> = None;
9623 let mut child_fk_column: Option<String> = None;
9624 let mut self_pk_column: Option<String> = None;
9625 meta.parse_nested_meta(|inner| {
9626 if inner.path.is_ident("name") {
9627 let s: LitStr = inner.value()?.parse()?;
9628 let raw = s.value();
9629 if raw.trim().is_empty() {
9630 return Err(syn::Error::new(
9631 s.span(),
9632 "`reverse_has(name = \"...\")` must not be empty",
9633 ));
9634 }
9635 accessor_name = Some(raw);
9636 return Ok(());
9637 }
9638 if inner.path.is_ident("child") {
9639 let s: LitStr = inner.value()?.parse()?;
9640 let raw = s.value();
9641 let trimmed = raw.trim();
9642 if trimmed.is_empty() {
9643 return Err(syn::Error::new(
9644 s.span(),
9645 "`reverse_has(child = \"...\")` must not be empty",
9646 ));
9647 }
9648 child_ident = Some(syn::Ident::new(trimmed, s.span()));
9649 return Ok(());
9650 }
9651 if inner.path.is_ident("child_fk_column") {
9652 let s: LitStr = inner.value()?.parse()?;
9653 let raw = s.value();
9654 let trimmed = raw.trim();
9655 if trimmed.is_empty() {
9656 return Err(syn::Error::new(
9657 s.span(),
9658 "`reverse_has(child_fk_column = \"...\")` must not be empty",
9659 ));
9660 }
9661 child_fk_column = Some(trimmed.to_owned());
9662 return Ok(());
9663 }
9664 if inner.path.is_ident("self_pk_column") {
9665 let s: LitStr = inner.value()?.parse()?;
9666 let raw = s.value();
9667 let trimmed = raw.trim();
9668 if trimmed.is_empty() {
9669 return Err(syn::Error::new(
9670 s.span(),
9671 "`reverse_has(self_pk_column = \"...\")` must not be empty",
9672 ));
9673 }
9674 self_pk_column = Some(trimmed.to_owned());
9675 return Ok(());
9676 }
9677 Err(inner.error(
9678 "unknown `reverse_has` attribute (supported: \
9679 `name`, `child`, `child_fk_column`, \
9680 `self_pk_column`)",
9681 ))
9682 })?;
9683 let Some(name) = accessor_name else {
9684 return Err(syn::Error::new(
9685 span,
9686 "`reverse_has` requires `name = \"...\"`",
9687 ));
9688 };
9689 let Some(child) = child_ident else {
9690 return Err(syn::Error::new(
9691 span,
9692 "`reverse_has` requires `child = \"ChildModelType\"`",
9693 ));
9694 };
9695 let Some(child_fk_column) = child_fk_column else {
9696 return Err(syn::Error::new(
9697 span,
9698 "`reverse_has` requires `child_fk_column = \"<column>\"`",
9699 ));
9700 };
9701 if out.reverse_has_relations.iter().any(|r| r.name == name) {
9702 return Err(syn::Error::new(
9703 span,
9704 format!(
9705 "duplicate `reverse_has(name = \"{name}\")` — \
9706 pick a unique accessor name"
9707 ),
9708 ));
9709 }
9710 out.reverse_has_relations.push(ReverseHasAttr {
9711 name,
9712 child,
9713 child_fk_column,
9714 self_pk_column,
9715 });
9716 return Ok(());
9717 }
9718 if meta.path.is_ident("generic_has") {
9719 // `#[rustango(generic_has(name = "tags",
9720 // child = "Tag", ct_column = "content_type_id",
9721 // pk_column = "object_pk" [, self_pk_column = "id"]))]`
9722 // — issue #830, the reverse generic-FK (GFK) arm of the
9723 // relation-existence family.
9724 let span = meta.path.span();
9725 let mut accessor_name: Option<String> = None;
9726 let mut child_ident: Option<syn::Ident> = None;
9727 let mut ct_column: Option<String> = None;
9728 let mut pk_column: Option<String> = None;
9729 let mut self_pk_column: Option<String> = None;
9730 meta.parse_nested_meta(|inner| {
9731 if inner.path.is_ident("name") {
9732 let s: LitStr = inner.value()?.parse()?;
9733 let raw = s.value();
9734 if raw.trim().is_empty() {
9735 return Err(syn::Error::new(
9736 s.span(),
9737 "`generic_has(name = \"...\")` must not be empty",
9738 ));
9739 }
9740 accessor_name = Some(raw);
9741 return Ok(());
9742 }
9743 if inner.path.is_ident("child") {
9744 let s: LitStr = inner.value()?.parse()?;
9745 let trimmed = s.value().trim().to_owned();
9746 if trimmed.is_empty() {
9747 return Err(syn::Error::new(
9748 s.span(),
9749 "`generic_has(child = \"...\")` must not be empty",
9750 ));
9751 }
9752 child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9753 return Ok(());
9754 }
9755 if inner.path.is_ident("ct_column") {
9756 let s: LitStr = inner.value()?.parse()?;
9757 let trimmed = s.value().trim().to_owned();
9758 if trimmed.is_empty() {
9759 return Err(syn::Error::new(
9760 s.span(),
9761 "`generic_has(ct_column = \"...\")` must not be empty",
9762 ));
9763 }
9764 ct_column = Some(trimmed);
9765 return Ok(());
9766 }
9767 if inner.path.is_ident("pk_column") {
9768 let s: LitStr = inner.value()?.parse()?;
9769 let trimmed = s.value().trim().to_owned();
9770 if trimmed.is_empty() {
9771 return Err(syn::Error::new(
9772 s.span(),
9773 "`generic_has(pk_column = \"...\")` must not be empty",
9774 ));
9775 }
9776 pk_column = Some(trimmed);
9777 return Ok(());
9778 }
9779 if inner.path.is_ident("self_pk_column") {
9780 let s: LitStr = inner.value()?.parse()?;
9781 let trimmed = s.value().trim().to_owned();
9782 if trimmed.is_empty() {
9783 return Err(syn::Error::new(
9784 s.span(),
9785 "`generic_has(self_pk_column = \"...\")` must not be empty",
9786 ));
9787 }
9788 self_pk_column = Some(trimmed);
9789 return Ok(());
9790 }
9791 Err(inner.error(
9792 "unknown `generic_has` attribute (supported: \
9793 `name`, `child`, `ct_column`, `pk_column`, \
9794 `self_pk_column`)",
9795 ))
9796 })?;
9797 let Some(name) = accessor_name else {
9798 return Err(syn::Error::new(
9799 span,
9800 "`generic_has` requires `name = \"...\"`",
9801 ));
9802 };
9803 let Some(child) = child_ident else {
9804 return Err(syn::Error::new(
9805 span,
9806 "`generic_has` requires `child = \"ChildModelType\"`",
9807 ));
9808 };
9809 let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9810 let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9811 if out.generic_has_relations.iter().any(|r| r.name == name) {
9812 return Err(syn::Error::new(
9813 span,
9814 format!(
9815 "duplicate `generic_has(name = \"{name}\")` — \
9816 pick a unique accessor name"
9817 ),
9818 ));
9819 }
9820 out.generic_has_relations.push(GenericHasAttr {
9821 name,
9822 child,
9823 ct_column,
9824 pk_column,
9825 self_pk_column,
9826 });
9827 return Ok(());
9828 }
9829 if meta.path.is_ident("audit") {
9830 let mut audit = AuditAttrs::default();
9831 meta.parse_nested_meta(|inner| {
9832 if inner.path.is_ident("track") {
9833 let s: LitStr = inner.value()?.parse()?;
9834 audit.track =
9835 Some((split_field_list(&s.value()), s.span()));
9836 return Ok(());
9837 }
9838 Err(inner.error(
9839 "unknown audit attribute (supported: `track`)",
9840 ))
9841 })?;
9842 out.audit = Some(audit);
9843 return Ok(());
9844 }
9845 if meta.path.is_ident("permissions") {
9846 // Two forms accepted:
9847 // #[rustango(permissions)] — flag form, true
9848 // #[rustango(permissions = false)] — explicit opt-out
9849 // #[rustango(permissions = true)] — explicit opt-in
9850 if let Ok(v) = meta.value() {
9851 let lit: syn::LitBool = v.parse()?;
9852 out.permissions = lit.value;
9853 } else {
9854 out.permissions = true;
9855 }
9856 return Ok(());
9857 }
9858 if meta.path.is_ident("view") {
9859 // Issue #293 / T2.10. Two forms accepted, matching
9860 // the `permissions` flag pattern:
9861 // #[rustango(view)] — flag form, true
9862 // #[rustango(view = false)] — explicit opt-out
9863 // #[rustango(view = true)] — explicit opt-in
9864 if let Ok(v) = meta.value() {
9865 let lit: syn::LitBool = v.parse()?;
9866 out.is_view = lit.value;
9867 } else {
9868 out.is_view = true;
9869 }
9870 return Ok(());
9871 }
9872 if meta.path.is_ident("managed") {
9873 // Who owns the table. Issue #321.
9874 // #[rustango(managed = false)] — operator-managed table
9875 // #[rustango(managed = true)] — rustango-managed (the default)
9876 // Bare-flag form is intentionally not accepted: writing
9877 // `#[rustango(managed)]` reads as "yes please manage it"
9878 // which is already the default. The opt-out is the only
9879 // useful state, so it must be explicit.
9880 let v = meta.value()?;
9881 let lit: syn::LitBool = v.parse()?;
9882 out.managed = lit.value;
9883 return Ok(());
9884 }
9885 if meta.path.is_ident("verbose_name") {
9886 let s: LitStr = meta.value()?.parse()?;
9887 out.verbose_name = Some(s.value());
9888 return Ok(());
9889 }
9890 if meta.path.is_ident("verbose_name_plural") {
9891 let s: LitStr = meta.value()?.parse()?;
9892 out.verbose_name_plural = Some(s.value());
9893 return Ok(());
9894 }
9895 if meta.path.is_ident("db_table_comment") {
9896 // Free-form table-level comment attached to the DB
9897 // catalog.
9898 let s: LitStr = meta.value()?.parse()?;
9899 out.db_table_comment = Some(s.value());
9900 return Ok(());
9901 }
9902 if meta.path.is_ident("proxy") {
9903 // Declarative flag
9904 // marking the struct as a proxy of another model that
9905 // shares its DB table. Stored on `ModelSchema::proxy`
9906 // so future codegen can skip `CreateTable` emission
9907 // for proxies (parent owns the table) and route
9908 // per-instance method resolution to the proxy class.
9909 //
9910 // Accepts `proxy` (bare → true) and `proxy = true/false`.
9911 let value = if meta.input.peek(syn::Token![=]) {
9912 meta.value()?.parse::<syn::LitBool>()?.value
9913 } else {
9914 true
9915 };
9916 out.proxy = value;
9917 return Ok(());
9918 }
9919 if meta.path.is_ident("order_with_respect_to") {
9920 // `order_with_respect_to = "parent_fk"` — the model's
9921 // instances are intrinsically ordered relative to
9922 // their parent FK.
9923 //
9924 // rustango stores the FK field name on
9925 // `ModelSchema::order_with_respect_to`. Declarative-only
9926 // today: the migration writer + admin surfaces still
9927 // treat every model identically. Future codegen will
9928 // key off the metadata to auto-emit the `_order`
9929 // column and reorder helpers.
9930 //
9931 // Validated as a Rust-shape identifier so the macro
9932 // can reject typos at derive time.
9933 let s: LitStr = meta.value()?.parse()?;
9934 let raw = s.value();
9935 if raw.is_empty() {
9936 return Err(syn::Error::new(
9937 s.span(),
9938 "`order_with_respect_to` must be a non-empty FK field name",
9939 ));
9940 }
9941 let valid = raw
9942 .chars()
9943 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9944 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9945 if !valid {
9946 return Err(syn::Error::new(
9947 s.span(),
9948 format!(
9949 "`order_with_respect_to` must be a valid Rust \
9950 identifier (letters / digits / underscores, \
9951 not starting with a digit); got `{raw}`"
9952 ),
9953 ));
9954 }
9955 out.order_with_respect_to = Some(raw);
9956 return Ok(());
9957 }
9958 if meta.path.is_ident("required_db_features") {
9959 // Capability tokens the model needs (e.g.
9960 // `"json_extract"`, `"window_functions"`,
9961 // `"row_security"`). Comma-separated.
9962 // `manage check --deploy` walks every model and warns
9963 // when the active backend doesn't advertise the
9964 // capability.
9965 //
9966 // rustango ships a small registry of capability tokens
9967 // each dialect supports — see `Dialect::supports`.
9968 // Unknown tokens still parse (they end up on the
9969 // schema and show up in the warning) so projects can
9970 // declare aspirational capabilities and the check
9971 // verb will keep nagging until the dialect implements
9972 // them.
9973 let s: LitStr = meta.value()?.parse()?;
9974 let raw = s.value();
9975 let features: Vec<String> = raw
9976 .split(',')
9977 .map(str::trim)
9978 .filter(|s| !s.is_empty())
9979 .map(str::to_owned)
9980 .collect();
9981 if features.is_empty() {
9982 return Err(syn::Error::new(
9983 s.span(),
9984 "`required_db_features` must list at least one \
9985 comma-separated capability token",
9986 ));
9987 }
9988 out.required_db_features = features;
9989 return Ok(());
9990 }
9991 if meta.path.is_ident("required_db_vendor") {
9992 // The model is only meant to run against the named DB
9993 // backend. `manage check --deploy` flags a mismatch so
9994 // ops catches "I forgot to switch DATABASE_URL" at
9995 // deploy time rather than runtime.
9996 //
9997 // Restricted to the three backends rustango ships
9998 // dialects for so the check verb can compare reliably.
9999 let s: LitStr = meta.value()?.parse()?;
10000 let raw = s.value().to_ascii_lowercase();
10001 match raw.as_str() {
10002 "postgresql" | "postgres" | "pg" => {
10003 out.required_db_vendor = Some("postgres".to_owned());
10004 }
10005 "mysql" | "mariadb" => {
10006 out.required_db_vendor = Some("mysql".to_owned());
10007 }
10008 "sqlite" | "sqlite3" => {
10009 out.required_db_vendor = Some("sqlite".to_owned());
10010 }
10011 _ => {
10012 return Err(syn::Error::new(
10013 s.span(),
10014 format!(
10015 "unknown required_db_vendor `{raw}` — \
10016 expected `postgres` (aliases: `postgresql`, `pg`), \
10017 `mysql` (alias: `mariadb`), or `sqlite` \
10018 (alias: `sqlite3`)"
10019 ),
10020 ));
10021 }
10022 }
10023 return Ok(());
10024 }
10025 if meta.path.is_ident("base_manager_name") {
10026 // Name of the manager that `<instance>.<relation>_set`
10027 // uses when resolving reverse-relation managers.
10028 // Distinct from `default_manager_name` (what
10029 // `Self::objects()` returns). Stored on
10030 // `ModelSchema::base_manager_name`.
10031 //
10032 // Validated as a Rust identifier so it stays safe to
10033 // re-emit as code in future reverse-manager codegen.
10034 let s: LitStr = meta.value()?.parse()?;
10035 let raw = s.value();
10036 if raw.is_empty() {
10037 return Err(syn::Error::new(
10038 s.span(),
10039 "`base_manager_name` must be a non-empty string",
10040 ));
10041 }
10042 let valid = raw
10043 .chars()
10044 .all(|c| c == '_' || c.is_ascii_alphanumeric())
10045 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
10046 if !valid {
10047 return Err(syn::Error::new(
10048 s.span(),
10049 format!(
10050 "`base_manager_name` must be a valid Rust \
10051 identifier (letters / digits / underscores, \
10052 not starting with a digit); got `{raw}`"
10053 ),
10054 ));
10055 }
10056 out.base_manager_name = Some(raw);
10057 return Ok(());
10058 }
10059 if meta.path.is_ident("default_related_name") {
10060 // The name reverse-relation accessors use when callers
10061 // don't override `related_name = "..."` on the FK / M2M
10062 // field. Stored on `ModelSchema::default_related_name`
10063 // so future reverse-manager codegen / schema emit /
10064 // admin templates can pick the right accessor name
10065 // (today rustango doesn't auto-emit reverse managers;
10066 // the metadata is the foundation for that work).
10067 //
10068 // Enforced non-empty + ASCII identifier-shape so the
10069 // string is safe to use as a Rust ident later.
10070 let s: LitStr = meta.value()?.parse()?;
10071 let raw = s.value();
10072 if raw.is_empty() {
10073 return Err(syn::Error::new(
10074 s.span(),
10075 "`default_related_name` must be a non-empty string",
10076 ));
10077 }
10078 let valid = raw
10079 .chars()
10080 .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
10081 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
10082 if !valid {
10083 return Err(syn::Error::new(
10084 s.span(),
10085 format!(
10086 "`default_related_name` must be snake_case ASCII \
10087 (lowercase letters / digits / underscores, not \
10088 starting with a digit); got `{raw}`"
10089 ),
10090 ));
10091 }
10092 out.default_related_name = Some(raw);
10093 return Ok(());
10094 }
10095 if meta.path.is_ident("extra_permissions") {
10096 // Comma-separated `codename:label` pairs.
10097 let s: LitStr = meta.value()?.parse()?;
10098 let raw = s.value();
10099 let mut pairs = Vec::new();
10100 for entry in raw.split(',') {
10101 let entry = entry.trim();
10102 if entry.is_empty() {
10103 continue;
10104 }
10105 let (codename, label) = match entry.split_once(':') {
10106 Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
10107 None => (entry.to_owned(), entry.to_owned()),
10108 };
10109 if codename.is_empty() {
10110 return Err(meta.error(
10111 "`extra_permissions` entries must be `codename:label` pairs",
10112 ));
10113 }
10114 pairs.push((codename, label));
10115 }
10116 if pairs.is_empty() {
10117 return Err(meta
10118 .error("`extra_permissions = \"…\"` must list at least one pair"));
10119 }
10120 out.extra_permissions = pairs;
10121 return Ok(());
10122 }
10123 if meta.path.is_ident("default_permissions") {
10124 // Comma-separated subset of the CRUD action set.
10125 // Empty means all four (the framework default).
10126 let s: LitStr = meta.value()?.parse()?;
10127 let raw = s.value();
10128 let mut actions: Vec<String> = Vec::new();
10129 for entry in raw.split(',') {
10130 let action = entry.trim().to_ascii_lowercase();
10131 if action.is_empty() {
10132 continue;
10133 }
10134 match action.as_str() {
10135 "add" | "change" | "delete" | "view" => {}
10136 other => {
10137 return Err(syn::Error::new(
10138 s.span(),
10139 format!(
10140 "unknown default_permissions action `{other}` — \
10141 expected one of `add`, `change`, `delete`, `view`"
10142 ),
10143 ));
10144 }
10145 }
10146 if !actions.contains(&action) {
10147 actions.push(action);
10148 }
10149 }
10150 if actions.is_empty() {
10151 return Err(syn::Error::new(
10152 s.span(),
10153 "`default_permissions = \"…\"` must list at least one action; \
10154 use `permissions = false` on the container if you want NO \
10155 permissions seeded for this model.",
10156 ));
10157 }
10158 out.default_permissions = actions;
10159 return Ok(());
10160 }
10161 if meta.path.is_ident("get_latest_by") {
10162 // The `-` prefix selects descending order.
10163 let s: LitStr = meta.value()?.parse()?;
10164 let raw = s.value();
10165 let trimmed = raw.trim();
10166 if trimmed.is_empty() {
10167 return Err(meta.error("`get_latest_by` must name a column"));
10168 }
10169 let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
10170 (stripped.to_owned(), true)
10171 } else if let Some(stripped) = trimmed.strip_prefix('+') {
10172 (stripped.to_owned(), false)
10173 } else {
10174 (trimmed.to_owned(), false)
10175 };
10176 if col.is_empty() {
10177 return Err(meta.error("`get_latest_by` must name a column"));
10178 }
10179 out.get_latest_by = Some((col, desc, s.span()));
10180 return Ok(());
10181 }
10182 if meta.path.is_ident("unique_together") {
10183 // Composite UNIQUE index. Two syntaxes:
10184 //
10185 // #[rustango(unique_together = "org_id, user_id")] — auto-derived name
10186 // #[rustango(unique_together(columns = "org_id, user_id", name = "x"))] — explicit name
10187 //
10188 // Both produce `CREATE UNIQUE INDEX <name> ON <table>
10189 // (col1, col2)`, where <name> defaults to
10190 // `<table>_<col1>_<col2>_uq` when not supplied.
10191 let (columns, name) = parse_together_attr(&meta, "unique_together", written)?;
10192 out.indexes.push(IndexAttr {
10193 span: meta.path.span(),
10194 name,
10195 columns,
10196 unique: true,
10197 method: "btree".to_owned(),
10198 where_clause: None,
10199 include: Vec::new(),
10200 });
10201 return Ok(());
10202 }
10203 if meta.path.is_ident("index_together") {
10204 // Composite (non-unique) index. Two syntaxes
10205 // mirroring `unique_together`.
10206 //
10207 // #[rustango(index_together = "created_at, status")]
10208 // #[rustango(index_together(columns = "created_at, status", name = "x"))]
10209 let (columns, name) = parse_together_attr(&meta, "index_together", written)?;
10210 out.indexes.push(IndexAttr {
10211 span: meta.path.span(),
10212 name,
10213 columns,
10214 unique: false,
10215 method: "btree".to_owned(),
10216 where_clause: None,
10217 include: Vec::new(),
10218 });
10219 return Ok(());
10220 }
10221 if meta.path.is_ident("unique_when") {
10222 // Partial unique index. Issue #265 / T1.3.
10223 //
10224 // #[rustango(unique_when(
10225 // columns = "email",
10226 // condition = "deleted_at IS NULL",
10227 // name = "unique_active_email"
10228 // ))]
10229 //
10230 // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
10231 // on PG / SQLite (both ship partial indexes natively).
10232 // MySQL falls back to a plain UNIQUE index — the
10233 // condition is lost; document the limitation in the
10234 // generated migration.
10235 let mut columns: Option<Vec<String>> = None;
10236 let mut condition: Option<String> = None;
10237 let mut name: Option<String> = None;
10238 let mut include: Vec<String> = Vec::new();
10239 meta.parse_nested_meta(|inner| {
10240 if inner.path.is_ident("columns") {
10241 let s: LitStr = inner.value()?.parse()?;
10242 columns = Some(split_field_list(&s.value()));
10243 return Ok(());
10244 }
10245 if inner.path.is_ident("condition") {
10246 let s: LitStr = inner.value()?.parse()?;
10247 condition = Some(s.value());
10248 return Ok(());
10249 }
10250 if inner.path.is_ident("name") {
10251 let s: LitStr = inner.value()?.parse()?;
10252 name = Some(s.value());
10253 return Ok(());
10254 }
10255 if inner.path.is_ident("include") {
10256 // PG 11+ covering-index columns. Non-key columns
10257 // travel with the index leaf for index-only
10258 // scans. Dropped on MySQL/SQLite by the writer.
10259 let s: LitStr = inner.value()?.parse()?;
10260 include = split_field_list(&s.value());
10261 return Ok(());
10262 }
10263 Err(inner.error(
10264 "unknown unique_when attribute (supported: \
10265 `columns = \"...\"`, `condition = \"...\"`, \
10266 `name = \"...\"`, `include = \"...\"`)",
10267 ))
10268 })?;
10269 let columns = columns.ok_or_else(|| {
10270 meta.error("`unique_when(...)` requires `columns = \"...\"`")
10271 })?;
10272 let condition = condition.ok_or_else(|| {
10273 meta.error("`unique_when(...)` requires `condition = \"...\"`")
10274 })?;
10275 if columns.is_empty() {
10276 return Err(meta.error("`unique_when(columns = \"\")` is empty"));
10277 }
10278 out.indexes.push(IndexAttr {
10279 span: meta.path.span(),
10280 name,
10281 columns,
10282 unique: true,
10283 method: "btree".to_owned(),
10284 where_clause: Some(condition),
10285 include,
10286 });
10287 return Ok(());
10288 }
10289 if meta.path.is_ident("index_when") {
10290 // Non-unique partial index. Sibling of `unique_when`
10291 // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
10292 //
10293 // #[rustango(index_when(
10294 // columns = "status, created_at",
10295 // condition = "deleted_at IS NULL",
10296 // name = "active_status_created_idx"
10297 // ))]
10298 //
10299 // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
10300 // on PG / SQLite (both ship partial indexes natively).
10301 // MySQL has no native partial-index support — the writer
10302 // emits a plain CREATE INDEX and the condition is lost;
10303 // operators wanting that selectivity on MySQL should
10304 // declare a covering index plus an application-level
10305 // filter.
10306 let mut columns: Option<Vec<String>> = None;
10307 let mut condition: Option<String> = None;
10308 let mut name: Option<String> = None;
10309 let mut method: String = "btree".to_owned();
10310 let mut include: Vec<String> = Vec::new();
10311 meta.parse_nested_meta(|inner| {
10312 if inner.path.is_ident("columns") {
10313 let s: LitStr = inner.value()?.parse()?;
10314 columns = Some(split_field_list(&s.value()));
10315 return Ok(());
10316 }
10317 if inner.path.is_ident("condition") {
10318 let s: LitStr = inner.value()?.parse()?;
10319 condition = Some(s.value());
10320 return Ok(());
10321 }
10322 if inner.path.is_ident("name") {
10323 let s: LitStr = inner.value()?.parse()?;
10324 name = Some(s.value());
10325 return Ok(());
10326 }
10327 if inner.path.is_ident("method") {
10328 let s: LitStr = inner.value()?.parse()?;
10329 method = s.value();
10330 return Ok(());
10331 }
10332 if inner.path.is_ident("include") {
10333 // PG 11+ covering-index columns; non-key
10334 // columns travel with the index leaf. Dropped
10335 // on MySQL/SQLite.
10336 let s: LitStr = inner.value()?.parse()?;
10337 include = split_field_list(&s.value());
10338 return Ok(());
10339 }
10340 Err(inner.error(
10341 "unknown index_when attribute (supported: \
10342 `columns = \"...\"`, `condition = \"...\"`, \
10343 `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
10344 `include = \"...\"`)",
10345 ))
10346 })?;
10347 let columns = columns
10348 .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
10349 let condition = condition.ok_or_else(|| {
10350 meta.error("`index_when(...)` requires `condition = \"...\"`")
10351 })?;
10352 if columns.is_empty() {
10353 return Err(meta.error("`index_when(columns = \"\")` is empty"));
10354 }
10355 out.indexes.push(IndexAttr {
10356 span: meta.path.span(),
10357 name,
10358 columns,
10359 unique: false,
10360 method,
10361 where_clause: Some(condition),
10362 include,
10363 });
10364 return Ok(());
10365 }
10366 if meta.path.is_ident("index") {
10367 // Container-level composite index. Two syntaxes:
10368 // #[rustango(index = "col1, col2")] — bare, non-unique btree
10369 // #[rustango(index("col1, col2"))] — call form (same result)
10370 // #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
10371 // The call form takes the column list as a leading string
10372 // literal, then optional `unique` / `name = "..."` /
10373 // `method = "..."` flags (a leading literal can't compose
10374 // under `parse_nested_meta`, so the paren body is parsed by
10375 // hand). `unique_together` / `index_together` remain
10376 // aliases for the same feature.
10377 let cols_lit: LitStr;
10378 let mut unique = false;
10379 let mut name: Option<String> = None;
10380 let mut method = "btree".to_owned();
10381 if meta.input.peek(syn::token::Paren) {
10382 let content;
10383 syn::parenthesized!(content in meta.input);
10384 cols_lit = content.parse()?;
10385 while content.peek(syn::Token![,]) {
10386 content.parse::<syn::Token![,]>()?;
10387 if content.is_empty() {
10388 break;
10389 }
10390 let flag: syn::Ident = content.parse()?;
10391 if flag == "unique" {
10392 unique = true;
10393 } else if flag == "name" {
10394 content.parse::<syn::Token![=]>()?;
10395 let s: LitStr = content.parse()?;
10396 name = Some(s.value());
10397 } else if flag == "method" {
10398 content.parse::<syn::Token![=]>()?;
10399 let s: LitStr = content.parse()?;
10400 let v = s.value();
10401 match v.as_str() {
10402 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
10403 | "bloom" => method = v,
10404 other => {
10405 return Err(syn::Error::new(
10406 s.span(),
10407 format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
10408 ));
10409 }
10410 }
10411 } else {
10412 return Err(syn::Error::new(
10413 flag.span(),
10414 "unknown index flag (supported: `unique`, `name`, `method`)",
10415 ));
10416 }
10417 }
10418 } else {
10419 cols_lit = meta.value()?.parse()?;
10420 }
10421 let columns = split_field_list(&cols_lit.value());
10422 out.indexes.push(IndexAttr {
10423 span: meta.path.span(),
10424 name,
10425 columns,
10426 unique,
10427 method,
10428 where_clause: None,
10429 include: Vec::new(),
10430 });
10431 return Ok(());
10432 }
10433 if meta.path.is_ident("check") {
10434 // #[rustango(check(name = "…", expr = "…"))]
10435 let mut name: Option<String> = None;
10436 let mut expr: Option<String> = None;
10437 meta.parse_nested_meta(|inner| {
10438 if inner.path.is_ident("name") {
10439 let s: LitStr = inner.value()?.parse()?;
10440 name = Some(s.value());
10441 return Ok(());
10442 }
10443 if inner.path.is_ident("expr") {
10444 let s: LitStr = inner.value()?.parse()?;
10445 expr = Some(s.value());
10446 return Ok(());
10447 }
10448 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
10449 })?;
10450 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
10451 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
10452 out.checks.push(CheckAttr { name, expr });
10453 return Ok(());
10454 }
10455 if meta.path.is_ident("exclude") {
10456 // #[rustango(exclude(name = "…", using = "gist",
10457 // elements = "col WITH op, col WITH op",
10458 // where = "…"))]
10459 let mut name: Option<String> = None;
10460 let mut using: Option<String> = None;
10461 let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
10462 let mut where_clause: Option<String> = None;
10463 meta.parse_nested_meta(|inner| {
10464 if inner.path.is_ident("name") {
10465 let s: LitStr = inner.value()?.parse()?;
10466 name = Some(s.value());
10467 return Ok(());
10468 }
10469 if inner.path.is_ident("using") {
10470 let s: LitStr = inner.value()?.parse()?;
10471 using = Some(s.value());
10472 return Ok(());
10473 }
10474 if inner.path.is_ident("elements") {
10475 let s: LitStr = inner.value()?.parse()?;
10476 elements_raw = Some((s.value(), s.span()));
10477 return Ok(());
10478 }
10479 if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
10480 let s: LitStr = inner.value()?.parse()?;
10481 where_clause = Some(s.value());
10482 return Ok(());
10483 }
10484 Err(inner.error(
10485 "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
10486 ))
10487 })?;
10488 let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
10489 let using = using.unwrap_or_else(|| "gist".to_owned());
10490 let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
10491 meta.error(
10492 "exclude requires `elements = \"col WITH op, col WITH op\"`",
10493 )
10494 })?;
10495 // Parse `col WITH op` pairs separated by commas.
10496 let mut elements: Vec<(String, String)> = Vec::new();
10497 for pair in elements_str.split(',') {
10498 let pair = pair.trim();
10499 if pair.is_empty() {
10500 continue;
10501 }
10502 let mut split = pair.splitn(2, |c: char| c.is_whitespace());
10503 let col = split.next().unwrap_or("").trim();
10504 let rest = split.next().unwrap_or("").trim();
10505 // `WITH op` — case-insensitive on `WITH`, then op.
10506 let rest_lc = rest.to_ascii_lowercase();
10507 let op = rest_lc
10508 .strip_prefix("with")
10509 .map(|r| r.trim_start())
10510 .filter(|r| !r.is_empty())
10511 .map(|_| {
10512 // Pull the original-case op from `rest` after the
10513 // `WITH ` token (5 chars).
10514 rest[4..].trim_start().to_owned()
10515 });
10516 let Some(op) = op else {
10517 return Err(syn::Error::new(
10518 elements_span,
10519 format!(
10520 "exclude elements: `{pair}` must be `<col> WITH <op>` \
10521 (e.g. `room_id WITH =` or `during WITH &&`)"
10522 ),
10523 ));
10524 };
10525 if col.is_empty() || op.is_empty() {
10526 return Err(syn::Error::new(
10527 elements_span,
10528 format!(
10529 "exclude elements: `{pair}` must be `<col> WITH <op>` \
10530 (both sides non-empty)"
10531 ),
10532 ));
10533 }
10534 elements.push((col.to_owned(), op));
10535 }
10536 if elements.is_empty() {
10537 return Err(syn::Error::new(
10538 elements_span,
10539 "exclude requires at least one `col WITH op` element",
10540 ));
10541 }
10542 out.excludes.push(ExcludeAttr {
10543 name,
10544 using,
10545 elements,
10546 where_clause,
10547 });
10548 return Ok(());
10549 }
10550 if meta.path.is_ident("generic_fk") {
10551 let mut gfk = GenericFkAttr {
10552 name: String::new(),
10553 ct_column: String::new(),
10554 pk_column: String::new(),
10555 };
10556 meta.parse_nested_meta(|inner| {
10557 if inner.path.is_ident("name") {
10558 let s: LitStr = inner.value()?.parse()?;
10559 gfk.name = s.value();
10560 return Ok(());
10561 }
10562 if inner.path.is_ident("ct_column") {
10563 let s: LitStr = inner.value()?.parse()?;
10564 gfk.ct_column = s.value();
10565 return Ok(());
10566 }
10567 if inner.path.is_ident("pk_column") {
10568 let s: LitStr = inner.value()?.parse()?;
10569 gfk.pk_column = s.value();
10570 return Ok(());
10571 }
10572 Err(inner.error(
10573 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10574 ))
10575 })?;
10576 if gfk.name.is_empty() {
10577 return Err(meta.error("generic_fk requires `name = \"...\"`"));
10578 }
10579 if gfk.ct_column.is_empty() {
10580 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10581 }
10582 if gfk.pk_column.is_empty() {
10583 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10584 }
10585 out.generic_fks.push(gfk);
10586 return Ok(());
10587 }
10588 if meta.path.is_ident("fk_composite") {
10589 let mut fk = CompositeFkAttr {
10590 name: String::new(),
10591 to: String::new(),
10592 from: Vec::new(),
10593 on: Vec::new(),
10594 };
10595 meta.parse_nested_meta(|inner| {
10596 if inner.path.is_ident("name") {
10597 let s: LitStr = inner.value()?.parse()?;
10598 fk.name = s.value();
10599 return Ok(());
10600 }
10601 if inner.path.is_ident("to") {
10602 let s: LitStr = inner.value()?.parse()?;
10603 fk.to = s.value();
10604 return Ok(());
10605 }
10606 // `on = ("col1", "col2", ...)` — parse a parenthesised
10607 // comma-list of string literals.
10608 if inner.path.is_ident("on") || inner.path.is_ident("from") {
10609 let value = inner.value()?;
10610 let content;
10611 syn::parenthesized!(content in value);
10612 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10613 content.parse_terminated(
10614 |p| p.parse::<syn::LitStr>(),
10615 syn::Token![,],
10616 )?;
10617 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10618 if inner.path.is_ident("on") {
10619 fk.on = cols;
10620 } else {
10621 fk.from = cols;
10622 }
10623 return Ok(());
10624 }
10625 Err(inner.error(
10626 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10627 ))
10628 })?;
10629 if fk.name.is_empty() {
10630 return Err(meta.error("fk_composite requires `name = \"...\"`"));
10631 }
10632 if fk.to.is_empty() {
10633 return Err(meta.error("fk_composite requires `to = \"...\"`"));
10634 }
10635 if fk.from.is_empty() || fk.on.is_empty() {
10636 return Err(meta.error(
10637 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10638 ));
10639 }
10640 if fk.from.len() != fk.on.len() {
10641 return Err(meta.error(format!(
10642 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10643 fk.from.len(),
10644 fk.on.len(),
10645 )));
10646 }
10647 out.composite_fks.push(fk);
10648 return Ok(());
10649 }
10650 if meta.path.is_ident("m2m") {
10651 let mut m2m = M2MAttr {
10652 name: String::new(),
10653 to: String::new(),
10654 through: String::new(),
10655 src: String::new(),
10656 dst: String::new(),
10657 auto_create: true,
10658 };
10659 meta.parse_nested_meta(|inner| {
10660 if inner.path.is_ident("name") {
10661 let s: LitStr = inner.value()?.parse()?;
10662 m2m.name = s.value();
10663 return Ok(());
10664 }
10665 if inner.path.is_ident("to") {
10666 let s: LitStr = inner.value()?.parse()?;
10667 m2m.to = s.value();
10668 return Ok(());
10669 }
10670 if inner.path.is_ident("through") {
10671 let s: LitStr = inner.value()?.parse()?;
10672 m2m.through = s.value();
10673 return Ok(());
10674 }
10675 if inner.path.is_ident("src") {
10676 let s: LitStr = inner.value()?.parse()?;
10677 m2m.src = s.value();
10678 return Ok(());
10679 }
10680 if inner.path.is_ident("dst") {
10681 let s: LitStr = inner.value()?.parse()?;
10682 m2m.dst = s.value();
10683 return Ok(());
10684 }
10685 if inner.path.is_ident("auto_create") {
10686 let lit: syn::LitBool = inner.value()?.parse()?;
10687 m2m.auto_create = lit.value;
10688 return Ok(());
10689 }
10690 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10691 })?;
10692 if m2m.name.is_empty() {
10693 return Err(meta.error("m2m requires `name = \"...\"`"));
10694 }
10695 if m2m.to.is_empty() {
10696 return Err(meta.error("m2m requires `to = \"...\"`"));
10697 }
10698 if m2m.through.is_empty() {
10699 return Err(meta.error("m2m requires `through = \"...\"`"));
10700 }
10701 if m2m.src.is_empty() {
10702 return Err(meta.error("m2m requires `src = \"...\"`"));
10703 }
10704 if m2m.dst.is_empty() {
10705 return Err(meta.error("m2m requires `dst = \"...\"`"));
10706 }
10707 out.m2m.push(m2m);
10708 return Ok(());
10709 }
10710 if meta.path.is_ident("generic_m2m") {
10711 let mut gm = GenericM2MAttr {
10712 name: String::new(),
10713 through: String::new(),
10714 pk_column: String::new(),
10715 ct_column: String::new(),
10716 related_column: String::new(),
10717 };
10718 meta.parse_nested_meta(|inner| {
10719 let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10720 let s: LitStr = inner.value()?.parse()?;
10721 Ok(s.value())
10722 };
10723 if inner.path.is_ident("name") {
10724 gm.name = field(&inner)?;
10725 return Ok(());
10726 }
10727 if inner.path.is_ident("through") {
10728 gm.through = field(&inner)?;
10729 return Ok(());
10730 }
10731 if inner.path.is_ident("pk_column") {
10732 gm.pk_column = field(&inner)?;
10733 return Ok(());
10734 }
10735 if inner.path.is_ident("ct_column") {
10736 gm.ct_column = field(&inner)?;
10737 return Ok(());
10738 }
10739 if inner.path.is_ident("related_column") {
10740 gm.related_column = field(&inner)?;
10741 return Ok(());
10742 }
10743 Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10744 })?;
10745 for (val, label) in [
10746 (&gm.name, "name"),
10747 (&gm.through, "through"),
10748 (&gm.pk_column, "pk_column"),
10749 (&gm.ct_column, "ct_column"),
10750 (&gm.related_column, "related_column"),
10751 ] {
10752 if val.is_empty() {
10753 return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10754 }
10755 }
10756 out.generic_m2m.push(gm);
10757 return Ok(());
10758 }
10759 Err(unknown_key_error(&meta, "container", CONTAINER_ATTR_KEYS))
10760 })?;
10761 }
10762 Ok(out)
10763}
10764
10765/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10766/// owned field names, trimming whitespace and skipping empty entries.
10767/// Field-name validation against the model is done by the caller.
10768fn split_field_list(raw: &str) -> Vec<String> {
10769 raw.split(',')
10770 .map(str::trim)
10771 .filter(|s| !s.is_empty())
10772 .map(str::to_owned)
10773 .collect()
10774}
10775
10776/// Shared parser for `unique_together` and `index_together` container
10777/// attrs. Accepts both shapes:
10778///
10779/// * `attr = "col1, col2"` — auto-derived index name.
10780/// * `attr(columns = "col1, col2", name = "...")` — explicit name.
10781///
10782/// Returns `(columns, name)`.
10783fn parse_together_attr(
10784 meta: &syn::meta::ParseNestedMeta<'_>,
10785 attr: &str,
10786 written: &str,
10787) -> syn::Result<(Vec<String>, Option<String>)> {
10788 // Disambiguate by whether the next token is `=` (key-value) or
10789 // `(` (parenthesized).
10790 if meta.input.peek(syn::Token![=]) {
10791 let cols_lit: LitStr = meta.value()?.parse()?;
10792 let columns = split_field_list(&cols_lit.value());
10793 check_together_columns(meta, attr, written, &columns)?;
10794 return Ok((columns, None));
10795 }
10796 let mut columns: Option<Vec<String>> = None;
10797 let mut name: Option<String> = None;
10798 meta.parse_nested_meta(|inner| {
10799 if inner.path.is_ident("columns") {
10800 let s: LitStr = inner.value()?.parse()?;
10801 columns = Some(split_field_list(&s.value()));
10802 return Ok(());
10803 }
10804 if inner.path.is_ident("name") {
10805 let s: LitStr = inner.value()?.parse()?;
10806 name = Some(s.value());
10807 return Ok(());
10808 }
10809 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10810 })?;
10811 let columns = columns.ok_or_else(|| {
10812 meta.error(format!(
10813 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10814 ))
10815 })?;
10816 check_together_columns(meta, attr, written, &columns)?;
10817 Ok((columns, name))
10818}
10819
10820fn check_together_columns(
10821 meta: &syn::meta::ParseNestedMeta<'_>,
10822 attr: &str,
10823 written: &str,
10824 columns: &[String],
10825) -> syn::Result<()> {
10826 if columns.len() < 2 {
10827 let flag = if attr == "unique_together" {
10828 "unique"
10829 } else {
10830 "index"
10831 };
10832 let single = format!("#[{written}({flag})] on the field");
10833 return Err(meta.error(format!(
10834 "{attr} expects two or more columns; for a single-column equivalent use {single}",
10835 )));
10836 }
10837 Ok(())
10838}
10839
10840/// Parse the fieldsets DSL: pipe-separated sections, optional
10841/// `"Title:"` prefix on each, comma-separated field names after.
10842/// Examples:
10843/// * `"name, office"` → one untitled section with two fields
10844/// * `"Identity: name, office | Metadata: created_at"` → two titled
10845/// sections
10846///
10847/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10848fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10849 raw.split('|')
10850 .map(str::trim)
10851 .filter(|s| !s.is_empty())
10852 .map(|section| {
10853 // Split off an optional `Title:` prefix (first colon).
10854 let (title, rest) = match section.split_once(':') {
10855 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10856 _ => (String::new(), section),
10857 };
10858 let fields = split_field_list(rest);
10859 (title, fields)
10860 })
10861 .collect()
10862}
10863
10864/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10865/// comma-separated entry maps a target field to one or more source
10866/// fields joined with `+`. Whitespace around tokens is trimmed.
10867/// Entries missing `:` or with empty target/source lists are dropped.
10868fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10869 raw.split(',')
10870 .map(str::trim)
10871 .filter(|s| !s.is_empty())
10872 .filter_map(|entry| {
10873 let (target, sources_raw) = entry.split_once(':')?;
10874 let target = target.trim().to_owned();
10875 if target.is_empty() {
10876 return None;
10877 }
10878 let sources: Vec<String> = sources_raw
10879 .split('+')
10880 .map(|s| s.trim().to_owned())
10881 .filter(|s| !s.is_empty())
10882 .collect();
10883 if sources.is_empty() {
10884 return None;
10885 }
10886 Some((target, sources))
10887 })
10888 .collect()
10889}
10890
10891/// Parse `formfield_overrides` — `"field:widget,field2:widget2"`
10892/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10893/// and empty halves drop silently — the macro layer only enforces shape,
10894/// not field-name vs. widget-name validity (those checks happen at
10895/// `AdminConfig` consumption time). Issue #359.
10896fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10897 raw.split(',')
10898 .map(str::trim)
10899 .filter(|s| !s.is_empty())
10900 .filter_map(|entry| {
10901 let (field, widget) = entry.split_once(':')?;
10902 let field = field.trim().to_owned();
10903 let widget = widget.trim().to_owned();
10904 if field.is_empty() || widget.is_empty() {
10905 return None;
10906 }
10907 Some((field, widget))
10908 })
10909 .collect()
10910}
10911
10912/// Parse an ordering list — `"name"` is ASC, `"-name"` is DESC.
10913/// Returns `(field_name, desc)` pairs in the same order as the input.
10914fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10915 raw.split(',')
10916 .map(str::trim)
10917 .filter(|s| !s.is_empty())
10918 .map(|spec| {
10919 spec.strip_prefix('-')
10920 .map_or((spec.to_owned(), false), |rest| {
10921 (rest.trim().to_owned(), true)
10922 })
10923 })
10924 .collect()
10925}
10926
10927struct FieldAttrs {
10928 column: Option<String>,
10929 primary_key: bool,
10930 fk: Option<String>,
10931 o2o: Option<String>,
10932 on: Option<String>,
10933 /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10934 /// "set_default" | "no_action")]` — what happens to this row when
10935 /// the row it points at is deleted.
10936 /// Only meaningful on a `ForeignKey<T>` field or with `fk` / `o2o`
10937 /// set; the macro errors at compile time if applied to a
10938 /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10939 /// DDL writer renders `ON DELETE <action>` after the constraint
10940 /// clause when this is `Some`; `None` falls back to the database
10941 /// default (NO ACTION on every backend rustango supports).
10942 on_delete: Option<String>,
10943 /// `#[rustango(related_name = "...")]` — per-FK
10944 /// reverse-accessor override. When set, the derive emits
10945 /// `Parent::<related_name>[_pool]` instead of the container-level
10946 /// `default_related_name` or the `<child_snake>_set[_pool]`
10947 /// fallback. Only meaningful when `fk` / `o2o` is also set;
10948 /// silently ignored on non-FK fields. Follow-up to #816.
10949 /// `"+"` emits none, for a parent defined in another crate.
10950 related_name: Option<String>,
10951 max_length: Option<u32>,
10952 /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10953 /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10954 /// unconstrained `vector` column.
10955 vector_dims: Option<u32>,
10956 /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10957 /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10958 /// unconstrained `geometry(Point)` column (SRID 0).
10959 geometry_srid: Option<u32>,
10960 min: Option<i64>,
10961 max: Option<i64>,
10962 default: Option<String>,
10963 /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10964 /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10965 /// "gen_random_uuid()"`. The Rust field type must be
10966 /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10967 /// INSERTs so the DB DEFAULT fires.
10968 auto_uuid: bool,
10969 /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10970 /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10971 /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10972 /// the field is `Auto::Unset`, then bound as a normal parameter
10973 /// rather than relying on a per-dialect DB function. Issue #823
10974 /// (Eloquent `HasUuids`).
10975 ///
10976 /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10977 /// Composes with every backend (PG / MySQL / SQLite) — no
10978 /// `gen_random_uuid()` requirement on the database.
10979 default_uuid_v7: bool,
10980 /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10981 /// Server-set on insert, immutable from app code afterwards.
10982 /// Implies `auto + default = "now()"`. Field type must be
10983 /// `DateTime<Utc>`.
10984 auto_now_add: bool,
10985 /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10986 /// every insert AND every update. Implies `auto + default =
10987 /// "now()"`; the macro additionally rewrites `update_on` /
10988 /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10989 /// field value.
10990 auto_now: bool,
10991 /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10992 /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10993 /// `soft_delete_on(executor)` and `restore_on(executor)`
10994 /// methods on the model.
10995 soft_delete: bool,
10996 /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10997 /// the column in the generated DDL.
10998 unique: bool,
10999 /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
11000 /// generates a `CREATE INDEX` for this column. `unique` here means
11001 /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
11002 index: bool,
11003 index_unique: bool,
11004 index_name: Option<String>,
11005 /// Index access method (`"btree"` / `"gin"` / …). Defaults to
11006 /// `"btree"`. Issue #34.
11007 index_method: String,
11008 /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
11009 /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
11010 /// the macro skips this column from every INSERT and UPDATE
11011 /// path, so the database always recomputes the value from
11012 /// `EXPR`. Backlog item #35.
11013 generated_as: Option<String>,
11014 /// `#[rustango(help_text = "…")]` — help text
11015 /// rendered below the admin form's input. Threaded into
11016 /// `FieldSchema::help_text` so admin / serializer / OpenAPI
11017 /// layers can read it.
11018 help_text: Option<String>,
11019 /// `#[rustango(choices = "value:Label, value:Label")]` — the
11020 /// enumerated allowed values. Threaded into `FieldSchema::choices`
11021 /// as a `&'static [(&'static str, &'static str)]` slice. When
11022 /// present, the admin form renders a `<select>` instead of `<input>`
11023 /// and the validator rejects values not in the list. Only meaningful
11024 /// for `FieldType::String`; the macro errors at compile time if
11025 /// applied to a non-string field.
11026 choices: Option<Vec<(String, String)>>,
11027 /// `#[rustango(db_comment = "…")]` — DB-side column
11028 /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
11029 /// the comment in CREATE TABLE; Postgres emits a separate
11030 /// `COMMENT ON COLUMN` statement after the table is created;
11031 /// SQLite silently drops the value (no native column comments).
11032 db_comment: Option<String>,
11033 /// `#[rustango(verbose_name = "…")]` — human-readable
11034 /// label for the field. Threaded into `FieldSchema::verbose_name`
11035 /// so admin column headers, form labels, and other display
11036 /// surfaces can prefer the friendly caption over the Rust
11037 /// identifier. `None` means renderers fall back to the field name.
11038 verbose_name: Option<String>,
11039 /// `#[rustango(editable = false)]` — opt out of
11040 /// auto-generated form rendering. Defaults to `true` so existing
11041 /// fields keep their current admin / form behavior; setting
11042 /// `false` removes the field from the admin change-form entirely
11043 /// (the value is still visible on detail / list views, just not
11044 /// editable).
11045 editable: bool,
11046 /// `#[rustango(blank)]` / `#[rustango(blank = true)]` —
11047 /// "form may submit empty even when DB is NOT NULL". Threaded into
11048 /// `FieldSchema::blank`. Defaults to `false`.
11049 blank: bool,
11050 /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
11051 /// case-insensitive text column. Threaded into
11052 /// `FieldSchema::case_insensitive`. Only meaningful for `String`
11053 /// fields; the macro errors at derive time if applied elsewhere.
11054 case_insensitive: bool,
11055 /// `#[rustango(validators = "email,url")]` — the
11056 /// model-level validator chain. Comma-separated names that
11057 /// dispatch to the `validators::*` family in `validate_value`.
11058 /// Empty by default; fires on every typed INSERT/UPDATE. Each is
11059 /// spanned on the attribute string, so an unknown name errors there.
11060 validators: Vec<syn::Ident>,
11061}
11062
11063/// Keys `parse_field_attrs` accepts, listed in its unknown-key error.
11064const FIELD_ATTR_KEYS: &[&str] = &[
11065 "column",
11066 "primary_key",
11067 "fk",
11068 "o2o",
11069 "on",
11070 "on_delete",
11071 "related_name",
11072 "max_length",
11073 "vector",
11074 "geometry",
11075 "min",
11076 "max",
11077 "default",
11078 "generated_as",
11079 "help_text",
11080 "choices",
11081 "db_comment",
11082 "verbose_name",
11083 "editable",
11084 "blank",
11085 "citext",
11086 "validators",
11087 "auto_uuid",
11088 "default_uuid_v7",
11089 "auto_now_add",
11090 "auto_now",
11091 "soft_delete",
11092 "unique",
11093 "index",
11094];
11095
11096/// Keys `parse_container_attrs` accepts, listed in its unknown-key error.
11097const CONTAINER_ATTR_KEYS: &[&str] = &[
11098 "table",
11099 "display",
11100 "app",
11101 "scope",
11102 "admin",
11103 "manager",
11104 "manager_fn",
11105 "default_order",
11106 "global_scope",
11107 "through",
11108 "reverse_has",
11109 "generic_has",
11110 "audit",
11111 "permissions",
11112 "view",
11113 "managed",
11114 "verbose_name",
11115 "verbose_name_plural",
11116 "db_table_comment",
11117 "proxy",
11118 "order_with_respect_to",
11119 "required_db_features",
11120 "required_db_vendor",
11121 "base_manager_name",
11122 "default_related_name",
11123 "extra_permissions",
11124 "default_permissions",
11125 "get_latest_by",
11126 "unique_together",
11127 "index_together",
11128 "unique_when",
11129 "index_when",
11130 "index",
11131 "check",
11132 "exclude",
11133 "generic_fk",
11134 "fk_composite",
11135 "m2m",
11136 "generic_m2m",
11137];
11138
11139/// A flag attribute: `#[rustango(key)]` or `#[rustango(key = true)]`.
11140/// `= false` is refused: off is the default, so the attribute is a no-op.
11141fn parse_flag(meta: &syn::meta::ParseNestedMeta<'_>, key: &str) -> syn::Result<()> {
11142 if meta.input.peek(syn::Token![=]) {
11143 let lit: syn::LitBool = meta.value()?.parse()?;
11144 if !lit.value {
11145 return Err(syn::Error::new(
11146 lit.span(),
11147 format!("`{key} = false` is the default; remove the attribute"),
11148 ));
11149 }
11150 }
11151 Ok(())
11152}
11153
11154/// "unknown rustango <what> attribute `key`", with the nearest key and the full list.
11155fn unknown_key_error(
11156 meta: &syn::meta::ParseNestedMeta<'_>,
11157 what: &str,
11158 keys: &[&str],
11159) -> syn::Error {
11160 let path = &meta.path;
11161 let got = path
11162 .get_ident()
11163 .map_or_else(|| quote!(#path).to_string(), ToString::to_string);
11164 let mut msg = format!("unknown rustango {what} attribute `{got}`");
11165 if let Some(near) = keys
11166 .iter()
11167 .map(|k| (edit_distance(&got, k), *k))
11168 .filter(|(d, k)| *d <= 2.max(k.len() / 4))
11169 .min_by_key(|(d, _)| *d)
11170 .map(|(_, k)| k)
11171 {
11172 msg.push_str(&format!("; did you mean `{near}`?"));
11173 }
11174 msg.push_str(&format!(" Supported: {}", keys.join(", ")));
11175 meta.error(msg)
11176}
11177
11178/// Levenshtein distance, for "did you mean" hints.
11179fn edit_distance(a: &str, b: &str) -> usize {
11180 let b: Vec<char> = b.chars().collect();
11181 let mut prev: Vec<usize> = (0..=b.len()).collect();
11182 for (i, ca) in a.chars().enumerate() {
11183 let mut cur = vec![i + 1];
11184 for (j, cb) in b.iter().enumerate() {
11185 let sub = prev[j] + usize::from(ca != *cb);
11186 cur.push(sub.min(prev[j + 1] + 1).min(cur[j] + 1));
11187 }
11188 prev = cur;
11189 }
11190 prev[b.len()]
11191}
11192
11193fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
11194 let mut out = FieldAttrs {
11195 column: None,
11196 primary_key: false,
11197 fk: None,
11198 o2o: None,
11199 on: None,
11200 on_delete: None,
11201 related_name: None,
11202 max_length: None,
11203 vector_dims: None,
11204 geometry_srid: None,
11205 min: None,
11206 max: None,
11207 default: None,
11208 auto_uuid: false,
11209 default_uuid_v7: false,
11210 auto_now_add: false,
11211 auto_now: false,
11212 soft_delete: false,
11213 unique: false,
11214 index: false,
11215 index_unique: false,
11216 index_name: None,
11217 index_method: "btree".to_owned(),
11218 generated_as: None,
11219 help_text: None,
11220 choices: None,
11221 db_comment: None,
11222 verbose_name: None,
11223 editable: true,
11224 blank: false,
11225 case_insensitive: false,
11226 validators: Vec::new(),
11227 };
11228 for attr in model_attrs(&field.attrs)? {
11229 attr.parse_nested_meta(|meta| {
11230 if meta.path.is_ident("column") {
11231 let s: LitStr = meta.value()?.parse()?;
11232 let name = s.value();
11233 validate_sql_identifier(&name, "column", s.span())?;
11234 out.column = Some(name);
11235 return Ok(());
11236 }
11237 if meta.path.is_ident("primary_key") {
11238 parse_flag(&meta, "primary_key")?;
11239 out.primary_key = true;
11240 return Ok(());
11241 }
11242 if meta.path.is_ident("fk") {
11243 let s: LitStr = meta.value()?.parse()?;
11244 out.fk = Some(s.value());
11245 return Ok(());
11246 }
11247 if meta.path.is_ident("o2o") {
11248 let s: LitStr = meta.value()?.parse()?;
11249 out.o2o = Some(s.value());
11250 return Ok(());
11251 }
11252 if meta.path.is_ident("on") {
11253 let s: LitStr = meta.value()?.parse()?;
11254 out.on = Some(s.value());
11255 return Ok(());
11256 }
11257 if meta.path.is_ident("on_delete") {
11258 let s: LitStr = meta.value()?.parse()?;
11259 let raw = s.value();
11260 let normalized = raw.trim().to_ascii_lowercase();
11261 // Validate at parse time so the user gets a clear span
11262 // rather than a downstream compile error in the emit.
11263 match normalized.as_str() {
11264 "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
11265 _ => {
11266 return Err(syn::Error::new(
11267 s.span(),
11268 format!(
11269 "unknown on_delete action `{raw}`; expected one of \
11270 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
11271 ),
11272 ));
11273 }
11274 }
11275 out.on_delete = Some(normalized);
11276 return Ok(());
11277 }
11278 if meta.path.is_ident("related_name") {
11279 let s: LitStr = meta.value()?.parse()?;
11280 let raw = s.value();
11281 if raw.trim().is_empty() {
11282 return Err(syn::Error::new(
11283 s.span(),
11284 "`related_name` must be a non-empty identifier",
11285 ));
11286 }
11287 // Validate as a Rust-ident shape — the value becomes
11288 // a method name on the parent type. Same rule as
11289 // `default_related_name` so the two surfaces match.
11290 // `"+"` emits no accessor at all.
11291 let snake = raw
11292 .chars()
11293 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
11294 && !raw.starts_with(char::is_numeric);
11295 if !snake && raw != NO_REVERSE_ACCESSOR {
11296 return Err(syn::Error::new(
11297 s.span(),
11298 "`related_name` must be snake_case ASCII (lowercase letters, \
11299 digits, underscores; no leading digit), or \"+\" for none",
11300 ));
11301 }
11302 out.related_name = Some(raw);
11303 return Ok(());
11304 }
11305 if meta.path.is_ident("max_length") {
11306 let lit: syn::LitInt = meta.value()?.parse()?;
11307 out.max_length = Some(lit.base10_parse::<u32>()?);
11308 return Ok(());
11309 }
11310 // `#[rustango(vector(dims = N))]` — pgvector column
11311 // dimension (#824). Nested-meta form so it reads like the
11312 // other typed-column attrs.
11313 if meta.path.is_ident("vector") {
11314 meta.parse_nested_meta(|inner| {
11315 if inner.path.is_ident("dims") {
11316 let lit: syn::LitInt = inner.value()?.parse()?;
11317 out.vector_dims = Some(lit.base10_parse::<u32>()?);
11318 return Ok(());
11319 }
11320 Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
11321 })?;
11322 return Ok(());
11323 }
11324 // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
11325 // (#443). Nested-meta form, mirroring `vector(dims = N)`.
11326 if meta.path.is_ident("geometry") {
11327 meta.parse_nested_meta(|inner| {
11328 if inner.path.is_ident("srid") {
11329 let lit: syn::LitInt = inner.value()?.parse()?;
11330 out.geometry_srid = Some(lit.base10_parse::<u32>()?);
11331 return Ok(());
11332 }
11333 Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
11334 })?;
11335 return Ok(());
11336 }
11337 if meta.path.is_ident("min") {
11338 out.min = Some(parse_signed_i64(&meta)?);
11339 return Ok(());
11340 }
11341 if meta.path.is_ident("max") {
11342 out.max = Some(parse_signed_i64(&meta)?);
11343 return Ok(());
11344 }
11345 if meta.path.is_ident("default") {
11346 let s: LitStr = meta.value()?.parse()?;
11347 out.default = Some(s.value());
11348 return Ok(());
11349 }
11350 if meta.path.is_ident("generated_as") {
11351 let s: LitStr = meta.value()?.parse()?;
11352 out.generated_as = Some(s.value());
11353 return Ok(());
11354 }
11355 if meta.path.is_ident("help_text") {
11356 let s: LitStr = meta.value()?.parse()?;
11357 out.help_text = Some(s.value());
11358 return Ok(());
11359 }
11360 if meta.path.is_ident("choices") {
11361 let s: LitStr = meta.value()?.parse()?;
11362 let raw = s.value();
11363 let mut pairs: Vec<(String, String)> = Vec::new();
11364 for chunk in raw.split(',') {
11365 let chunk = chunk.trim();
11366 if chunk.is_empty() {
11367 continue;
11368 }
11369 let (value, label) = match chunk.split_once(':') {
11370 Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
11371 None => (chunk.to_owned(), chunk.to_owned()),
11372 };
11373 if value.is_empty() {
11374 return Err(syn::Error::new(
11375 s.span(),
11376 "`choices` entry has empty value before `:`",
11377 ));
11378 }
11379 pairs.push((value, label));
11380 }
11381 if pairs.is_empty() {
11382 return Err(syn::Error::new(
11383 s.span(),
11384 "`choices = \"…\"` must contain at least one value",
11385 ));
11386 }
11387 out.choices = Some(pairs);
11388 return Ok(());
11389 }
11390 if meta.path.is_ident("db_comment") {
11391 let s: LitStr = meta.value()?.parse()?;
11392 out.db_comment = Some(s.value());
11393 return Ok(());
11394 }
11395 if meta.path.is_ident("verbose_name") {
11396 let s: LitStr = meta.value()?.parse()?;
11397 out.verbose_name = Some(s.value());
11398 return Ok(());
11399 }
11400 if meta.path.is_ident("editable") {
11401 // Two forms accepted:
11402 // #[rustango(editable = false)] / true — explicit
11403 // #[rustango(editable)] — flag form (= true, the
11404 // default, so harmless; included for symmetry)
11405 if let Ok(v) = meta.value() {
11406 let lit: syn::LitBool = v.parse()?;
11407 out.editable = lit.value;
11408 } else {
11409 out.editable = true;
11410 }
11411 return Ok(());
11412 }
11413 if meta.path.is_ident("blank") {
11414 // Two forms accepted:
11415 // #[rustango(blank)] — flag form, true
11416 // #[rustango(blank = false)] / true — explicit
11417 if let Ok(v) = meta.value() {
11418 let lit: syn::LitBool = v.parse()?;
11419 out.blank = lit.value;
11420 } else {
11421 out.blank = true;
11422 }
11423 return Ok(());
11424 }
11425 if meta.path.is_ident("citext") {
11426 // Case-insensitive text column (#344). Two forms:
11427 // #[rustango(citext)] — flag form, true
11428 // #[rustango(citext = true)] — explicit
11429 // #[rustango(citext = false)] — explicit opt-out
11430 // String-only validation lives in the field-type
11431 // emitter (the FieldType discriminant is computed in
11432 // `detect_type`); the macro records the flag and the
11433 // DDL writer emits dialect-specific COLLATE / CITEXT.
11434 if let Ok(v) = meta.value() {
11435 let lit: syn::LitBool = v.parse()?;
11436 out.case_insensitive = lit.value;
11437 } else {
11438 out.case_insensitive = true;
11439 }
11440 return Ok(());
11441 }
11442 if meta.path.is_ident("validators") {
11443 let s: LitStr = meta.value()?.parse()?;
11444 let raw = s.value();
11445 out.validators = raw
11446 .split(',')
11447 .map(str::trim)
11448 .filter(|s| !s.is_empty())
11449 .map(|name| {
11450 let mut ident = syn::parse_str::<syn::Ident>(name).map_err(|_| {
11451 syn::Error::new(s.span(), format!("unknown validator `{name}`"))
11452 })?;
11453 ident.set_span(s.span());
11454 Ok(ident)
11455 })
11456 .collect::<syn::Result<_>>()?;
11457 if out.validators.is_empty() {
11458 return Err(syn::Error::new(
11459 s.span(),
11460 "`validators = \"…\"` must list at least one name",
11461 ));
11462 }
11463 return Ok(());
11464 }
11465 if meta.path.is_ident("auto_uuid") {
11466 parse_flag(&meta, "auto_uuid")?;
11467 out.auto_uuid = true;
11468 // Implied: PK + auto + DEFAULT gen_random_uuid().
11469 // Each is also explicitly settable; the explicit
11470 // value wins if conflicting.
11471 out.primary_key = true;
11472 if out.default.is_none() {
11473 out.default = Some("gen_random_uuid()".into());
11474 }
11475 return Ok(());
11476 }
11477 if meta.path.is_ident("default_uuid_v7") {
11478 parse_flag(&meta, "default_uuid_v7")?;
11479 // Backend-neutral counterpart of `auto_uuid` — issue #823.
11480 // No SQL DEFAULT (the macro fills the value Rust-side
11481 // before binding); just mark the field as PK + Auto
11482 // so the insert path is the `Auto::Unset → generate`
11483 // branch.
11484 out.default_uuid_v7 = true;
11485 out.primary_key = true;
11486 return Ok(());
11487 }
11488 if meta.path.is_ident("auto_now_add") {
11489 parse_flag(&meta, "auto_now_add")?;
11490 out.auto_now_add = true;
11491 if out.default.is_none() {
11492 out.default = Some("now()".into());
11493 }
11494 return Ok(());
11495 }
11496 if meta.path.is_ident("auto_now") {
11497 parse_flag(&meta, "auto_now")?;
11498 out.auto_now = true;
11499 if out.default.is_none() {
11500 out.default = Some("now()".into());
11501 }
11502 return Ok(());
11503 }
11504 if meta.path.is_ident("soft_delete") {
11505 parse_flag(&meta, "soft_delete")?;
11506 out.soft_delete = true;
11507 return Ok(());
11508 }
11509 if meta.path.is_ident("unique") {
11510 parse_flag(&meta, "unique")?;
11511 out.unique = true;
11512 return Ok(());
11513 }
11514 if meta.path.is_ident("index") {
11515 parse_flag(&meta, "index")?;
11516 out.index = true;
11517 // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
11518 if meta.input.peek(syn::token::Paren) {
11519 meta.parse_nested_meta(|inner| {
11520 if inner.path.is_ident("unique") {
11521 out.index_unique = true;
11522 return Ok(());
11523 }
11524 if inner.path.is_ident("name") {
11525 let s: LitStr = inner.value()?.parse()?;
11526 out.index_name = Some(s.value());
11527 return Ok(());
11528 }
11529 if inner.path.is_ident("method") {
11530 let s: LitStr = inner.value()?.parse()?;
11531 let v = s.value();
11532 match v.as_str() {
11533 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
11534 out.index_method = v;
11535 }
11536 other => {
11537 return Err(inner.error(format!(
11538 "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
11539 )));
11540 }
11541 }
11542 return Ok(());
11543 }
11544 Err(inner.error(
11545 "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
11546 ))
11547 })?;
11548 }
11549 return Ok(());
11550 }
11551 Err(unknown_key_error(&meta, "field", FIELD_ATTR_KEYS))
11552 })?;
11553 }
11554 Ok(out)
11555}
11556
11557/// Parse a signed integer literal, accepting optional leading `-`.
11558fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
11559 let expr: syn::Expr = meta.value()?.parse()?;
11560 match expr {
11561 syn::Expr::Lit(syn::ExprLit {
11562 lit: syn::Lit::Int(lit),
11563 ..
11564 }) => lit.base10_parse::<i64>(),
11565 syn::Expr::Unary(syn::ExprUnary {
11566 op: syn::UnOp::Neg(_),
11567 expr,
11568 ..
11569 }) => {
11570 if let syn::Expr::Lit(syn::ExprLit {
11571 lit: syn::Lit::Int(lit),
11572 ..
11573 }) = *expr
11574 {
11575 let v: i64 = lit.base10_parse()?;
11576 Ok(-v)
11577 } else {
11578 Err(syn::Error::new_spanned(expr, "expected integer literal"))
11579 }
11580 }
11581 other => Err(syn::Error::new_spanned(
11582 other,
11583 "expected integer literal (signed)",
11584 )),
11585 }
11586}
11587
11588struct FieldInfo<'a> {
11589 ident: &'a syn::Ident,
11590 column: String,
11591 primary_key: bool,
11592 /// `true` when the Rust type was `Auto<T>` — the INSERT path will
11593 /// skip this column when `Auto::Unset` and emit it under
11594 /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
11595 auto: bool,
11596 /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
11597 /// the `Column::Value` associated type for typed-column tokens.
11598 value_ty: &'a Type,
11599 /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
11600 field_type_tokens: TokenStream2,
11601 schema: TokenStream2,
11602 from_row_init: TokenStream2,
11603 /// Variant of [`Self::from_row_init`] that reads the column via
11604 /// `format!("{prefix}__{col}")` so a model can be decoded out of
11605 /// the aliased columns of a JOINed row. Drives slice 9.0d's
11606 /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
11607 /// helper that `select_related` calls when stitching loaded FKs.
11608 from_aliased_row_init: TokenStream2,
11609 /// MySQL twins of the two inits above. They differ only for a UUID
11610 /// column, which MySQL keeps as `CHAR(36)` text (#1733).
11611 from_row_init_my: TokenStream2,
11612 from_aliased_row_init_my: TokenStream2,
11613 /// A bare or optional `Uuid` field.
11614 uuid_column: bool,
11615 /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
11616 /// relation helper emit (`Author::<child>_set`) needs to know `T`
11617 /// to point the generated method at the right child model.
11618 fk_inner: Option<Type>,
11619 /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
11620 /// `kind` (since ForeignKey detection sets `kind` to K's
11621 /// underlying type) but stored separately for clarity at the
11622 /// `FkRelation` construction site, which only sees the FK's
11623 /// surface fields.
11624 fk_pk_kind: DetectedKind,
11625 /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
11626 /// the bare `ForeignKey<T, K>`. Routes the load_related and
11627 /// fk_pk_access emitters to wrap assignments / accessors in
11628 /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
11629 /// FK column compiles end-to-end. The DDL writer reads this off
11630 /// the field schema (`nullable` flag); the macro just needs to
11631 /// keep the Rust-side codegen consistent.
11632 nullable: bool,
11633 /// `true` when this column was marked `#[rustango(auto_now)]` —
11634 /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
11635 /// column instead of the user-supplied value, so `updated_at`
11636 /// always reflects the latest write without the caller having
11637 /// to remember to set it.
11638 auto_now: bool,
11639 /// `true` when this column was marked `#[rustango(auto_now_add)]`
11640 /// — the column is server-set on INSERT (DB DEFAULT) and
11641 /// **immutable** afterwards. `update_on` / `save_on` skip the
11642 /// column entirely so a stale `created_at` value in memory never
11643 /// rewrites the persisted timestamp.
11644 auto_now_add: bool,
11645 /// `true` when this column was marked `#[rustango(soft_delete)]`.
11646 /// Triggers emission of `soft_delete_on(executor)` and
11647 /// `restore_on(executor)` on the model's inherent impl. There is
11648 /// at most one such column per model — emission asserts this.
11649 soft_delete: bool,
11650 /// `Some` when this column was marked
11651 /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
11652 /// every INSERT and UPDATE path; the database recomputes the
11653 /// value from `EXPR`. Backlog item #35.
11654 generated_as: Option<String>,
11655 /// `true` when this column was marked
11656 /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
11657 /// emit an `insert_push` that auto-fills an `Auto::Unset` value
11658 /// with `Uuid::now_v7()` before binding, so the PK is generated
11659 /// Rust-side and the column is always present in the INSERT
11660 /// statement (no DB DEFAULT requirement). Issue #823.
11661 default_uuid_v7: bool,
11662 /// `Some` when this FK field carried `#[rustango(related_name =
11663 /// "...")]`. Threaded into [`FkRelation::related_name`] and
11664 /// consumed by [`reverse_helper_tokens`] to override the default
11665 /// `<child_snake>_set` accessor name. Follow-up to #816.
11666 related_name: Option<String>,
11667}
11668
11669/// Reject table names that won't survive SQL identifier
11670/// derivation downstream. Postgres' regular-identifier rule
11671/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
11672/// through the framework's unquoted FK / index / constraint name
11673/// emission without surprises. We also disallow leading-digit and
11674/// the empty string for clarity.
11675///
11676/// Reserved-word collisions (`select`, `from`, …) aren't flagged
11677/// here — those produce a runtime error from the SQL parser,
11678/// which is loud enough; statically enumerating reserved words
11679/// across the three supported dialects is more friction than help.
11680///
11681/// Backlog item #65.
11682fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11683 validate_sql_identifier(name, "table", span)
11684}
11685
11686/// Reject SQL identifiers that compile but break downstream SQL
11687/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11688/// `kind` is "table" / "column" — used for the error message so users
11689/// see which attribute caused the failure.
11690fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11691 if name.is_empty() {
11692 return Err(syn::Error::new(
11693 span,
11694 format!("`{kind} = \"\"` is not a valid SQL identifier"),
11695 ));
11696 }
11697 let mut chars = name.chars();
11698 let first = chars.next().unwrap();
11699 if !(first.is_ascii_alphabetic() || first == '_') {
11700 return Err(syn::Error::new(
11701 span,
11702 format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11703 ));
11704 }
11705 for c in chars {
11706 if !(c.is_ascii_alphanumeric() || c == '_') {
11707 return Err(syn::Error::new(
11708 span,
11709 format!(
11710 "{kind} name `{name}` contains invalid character {c:?} — \
11711 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11712 Hyphens in particular break FK / index name derivation \
11713 downstream; use underscores instead (e.g. `{}`)",
11714 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11715 ),
11716 ));
11717 }
11718 }
11719 Ok(())
11720}
11721
11722fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11723 let root = rustango_root();
11724 let attrs = parse_field_attrs(field)?;
11725 let ident = field
11726 .ident
11727 .as_ref()
11728 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11729 let name = ident_name(ident);
11730 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11731 let primary_key = attrs.primary_key;
11732 let DetectedType {
11733 kind,
11734 nullable,
11735 auto: detected_auto,
11736 fk_inner,
11737 } = detect_type(&field.ty)?;
11738 check_bound_compatibility(field, &attrs, kind)?;
11739 let auto = detected_auto;
11740 // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11741 // INSERT path: the user must wrap the field in `Auto<T>`, which
11742 // marks the column as DB-default-supplied. The mixin attrs then
11743 // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11744 // for `auto_now`, force the value on UPDATE too.
11745 if attrs.auto_uuid {
11746 if kind != DetectedKind::Uuid {
11747 return Err(syn::Error::new_spanned(
11748 field,
11749 "`#[rustango(auto_uuid)]` requires the field type to be \
11750 `Auto<uuid::Uuid>`",
11751 ));
11752 }
11753 if !detected_auto {
11754 return Err(syn::Error::new_spanned(
11755 field,
11756 "`#[rustango(auto_uuid)]` requires the field type to be \
11757 wrapped in `Auto<...>` so the macro skips the column on \
11758 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11759 ));
11760 }
11761 }
11762 if attrs.default_uuid_v7 {
11763 if kind != DetectedKind::Uuid {
11764 return Err(syn::Error::new_spanned(
11765 field,
11766 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11767 `Auto<uuid::Uuid>`",
11768 ));
11769 }
11770 if !detected_auto {
11771 return Err(syn::Error::new_spanned(
11772 field,
11773 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11774 wrapped in `Auto<...>` so the macro can detect the \
11775 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11776 ));
11777 }
11778 if attrs.auto_uuid {
11779 return Err(syn::Error::new_spanned(
11780 field,
11781 "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11782 `#[rustango(auto_uuid)]` — the former generates the UUID \
11783 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11784 Pick one.",
11785 ));
11786 }
11787 }
11788 if attrs.auto_now_add || attrs.auto_now {
11789 if kind != DetectedKind::DateTime {
11790 return Err(syn::Error::new_spanned(
11791 field,
11792 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11793 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11794 ));
11795 }
11796 if !detected_auto {
11797 return Err(syn::Error::new_spanned(
11798 field,
11799 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11800 the field type to be wrapped in `Auto<...>` so the macro skips \
11801 the column on INSERT and the DB DEFAULT (`now()`) fires",
11802 ));
11803 }
11804 }
11805 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11806 return Err(syn::Error::new_spanned(
11807 field,
11808 "`#[rustango(soft_delete)]` requires the field type to be \
11809 `Option<chrono::DateTime<chrono::Utc>>`",
11810 ));
11811 }
11812 let is_mixin_auto =
11813 attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11814 if detected_auto && !primary_key && !is_mixin_auto {
11815 return Err(syn::Error::new_spanned(
11816 field,
11817 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11818 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11819 `auto_now`",
11820 ));
11821 }
11822 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11823 return Err(syn::Error::new_spanned(
11824 field,
11825 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11826 SERIAL / BIGSERIAL already supplies a default sequence.",
11827 ));
11828 }
11829 if fk_inner.is_some() && primary_key {
11830 return Err(syn::Error::new_spanned(
11831 field,
11832 "`ForeignKey<T>` is not allowed on a primary-key field — \
11833 a row's PK is its own identity, not a reference to a parent.",
11834 ));
11835 }
11836 if attrs.generated_as.is_some() {
11837 if primary_key {
11838 return Err(syn::Error::new_spanned(
11839 field,
11840 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11841 primary-key field — a PK must be writable so the row \
11842 has an identity at INSERT time.",
11843 ));
11844 }
11845 if attrs.default.is_some() {
11846 return Err(syn::Error::new_spanned(
11847 field,
11848 "`#[rustango(generated_as = \"…\")]` cannot combine with \
11849 `default = \"…\"` — Postgres rejects DEFAULT on \
11850 generated columns. The expression IS the default.",
11851 ));
11852 }
11853 if detected_auto {
11854 return Err(syn::Error::new_spanned(
11855 field,
11856 "`#[rustango(generated_as = \"…\")]` is not allowed on \
11857 an `Auto<T>` field — generated columns are computed \
11858 by the DB, not server-assigned via a sequence. Use a \
11859 plain Rust type (e.g. `f64`).",
11860 ));
11861 }
11862 if fk_inner.is_some() {
11863 return Err(syn::Error::new_spanned(
11864 field,
11865 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11866 ForeignKey field.",
11867 ));
11868 }
11869 }
11870 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11871 let column_lit = column.as_str();
11872 // pgvector (#824): the `vector(dims = N)` attribute supplies the
11873 // dimension that the bare `Vector` Rust type can't carry, so emit
11874 // `FieldType::Vector(N)` here rather than the `variant_tokens`
11875 // fallback of `Vector(0)`.
11876 let field_type_tokens = if kind == DetectedKind::Vector {
11877 let root = rustango_root();
11878 let dims = attrs.vector_dims.unwrap_or(0);
11879 quote!(#root::core::FieldType::Vector(#dims))
11880 } else if kind == DetectedKind::Geometry {
11881 // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11882 // SRID that the bare `Point` Rust type can't carry, so emit
11883 // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11884 let root = rustango_root();
11885 let srid = attrs.geometry_srid.unwrap_or(0);
11886 quote!(#root::core::FieldType::Geometry(#srid))
11887 } else {
11888 kind.variant_tokens()
11889 };
11890 let max_length = optional_u32(attrs.max_length);
11891 let min = optional_i64(attrs.min);
11892 let max = optional_i64(attrs.max);
11893 let default = optional_str(attrs.default.as_deref());
11894
11895 let unique = attrs.unique;
11896 let generated_as = optional_str(attrs.generated_as.as_deref());
11897 let help_text = optional_str(attrs.help_text.as_deref());
11898 let choices = optional_choices(attrs.choices.as_deref());
11899 let db_comment = optional_str(attrs.db_comment.as_deref());
11900 let verbose_name = optional_str(attrs.verbose_name.as_deref());
11901 let editable = attrs.editable;
11902 let blank = attrs.blank;
11903 let case_insensitive = attrs.case_insensitive;
11904 // A path per name: one the runtime does not know fails to resolve.
11905 let validators = &attrs.validators;
11906 if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() && fk_inner.is_none()
11907 {
11908 return Err(syn::Error::new_spanned(
11909 field,
11910 "`#[rustango(on_delete = \"…\")]` requires a `ForeignKey<T>` field \
11911 or `fk = \"<table>\"` / `o2o = \"<table>\"` on the same field — it \
11912 has no meaning on a non-FK column.",
11913 ));
11914 }
11915 if attrs.on_delete.as_deref() == Some("set_null") && !nullable {
11916 return Err(syn::Error::new_spanned(
11917 field,
11918 "`on_delete = \"set_null\"` needs a nullable column: use \
11919 `Option<ForeignKey<T>>` or `Option<…>` (spelled out; a type \
11920 alias for `Option` is not seen).",
11921 ));
11922 }
11923 let fk_on_delete = match attrs.on_delete.as_deref() {
11924 None => quote!(::core::option::Option::None),
11925 Some(action) => {
11926 let variant = match action {
11927 "cascade" => quote!(Cascade),
11928 "restrict" => quote!(Restrict),
11929 "set_null" => quote!(SetNull),
11930 "set_default" => quote!(SetDefault),
11931 "no_action" => quote!(NoAction),
11932 // parse_field_attrs already validated this — guard against future drift.
11933 other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11934 };
11935 quote!(::core::option::Option::Some(
11936 #root::core::OnDeleteAction::#variant
11937 ))
11938 }
11939 };
11940 let auto_now_flag = attrs.auto_now;
11941 let cast = match cast_type(&field.ty) {
11942 Some(ty) => quote!(::core::option::Option::Some(<#ty>::FIELD_CAST)),
11943 None => quote!(::core::option::Option::None),
11944 };
11945 let schema = quote! {{
11946 #root::core::FieldSchema::from_parts(#root::core::FieldSchemaParts {
11947 name: #name,
11948 column: #column_lit,
11949 ty: #field_type_tokens,
11950 nullable: #nullable,
11951 primary_key: #primary_key,
11952 relation: #relation,
11953 max_length: #max_length,
11954 min: #min,
11955 max: #max,
11956 default: #default,
11957 auto: #auto,
11958 auto_now: #auto_now_flag,
11959 unique: #unique,
11960 generated_as: #generated_as,
11961 help_text: #help_text,
11962 choices: #choices,
11963 db_comment: #db_comment,
11964 verbose_name: #verbose_name,
11965 editable: #editable,
11966 blank: #blank,
11967 case_insensitive: #case_insensitive,
11968 fk_on_delete: #fk_on_delete,
11969 validators: &[ #( #root::core::__validator_names::#validators ),* ],
11970 cast: #cast,
11971 })
11972 }};
11973
11974 // sqlx-sqlite has no `Decimal` decode: it goes through `FlatScalar`'s cell.
11975 let decimal_column = kind == DetectedKind::Decimal && !detected_auto && fk_inner.is_none();
11976 let (from_row_init, from_aliased_row_init) = if decimal_column {
11977 (
11978 quote! { #ident: #root::sql::try_get_flat(row, #column_lit)? },
11979 quote! {
11980 #ident: #root::sql::try_get_flat(
11981 row,
11982 #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11983 )?
11984 },
11985 )
11986 } else {
11987 (
11988 quote! { #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)? },
11989 quote! {
11990 #ident: #root::sql::sqlx::Row::try_get(
11991 row,
11992 #root::core::joins::joined_label(prefix, #column_lit).as_str(),
11993 )?
11994 },
11995 )
11996 };
11997 // `Auto<Uuid>` and `ForeignKey<_, Uuid>` decode through their own
11998 // MySQL impls; a bare or optional `Uuid` needs `FlatScalar`'s cell.
11999 let uuid_column = kind == DetectedKind::Uuid && !detected_auto && fk_inner.is_none();
12000 let (from_row_init_my, from_aliased_row_init_my) = if uuid_column {
12001 (
12002 quote! { #ident: #root::sql::try_get_flat_my(row, #column_lit)? },
12003 quote! {
12004 #ident: #root::sql::try_get_flat_my(
12005 row,
12006 #root::core::joins::joined_label(prefix, #column_lit).as_str(),
12007 )?
12008 },
12009 )
12010 } else {
12011 (from_row_init.clone(), from_aliased_row_init.clone())
12012 };
12013
12014 Ok(FieldInfo {
12015 ident,
12016 column,
12017 primary_key,
12018 auto,
12019 value_ty: &field.ty,
12020 field_type_tokens,
12021 schema,
12022 from_row_init,
12023 from_aliased_row_init,
12024 from_row_init_my,
12025 from_aliased_row_init_my,
12026 uuid_column,
12027 fk_inner: fk_inner.cloned(),
12028 fk_pk_kind: kind,
12029 nullable,
12030 auto_now: attrs.auto_now,
12031 auto_now_add: attrs.auto_now_add,
12032 soft_delete: attrs.soft_delete,
12033 generated_as: attrs.generated_as.clone(),
12034 default_uuid_v7: attrs.default_uuid_v7,
12035 related_name: attrs.related_name.clone(),
12036 })
12037}
12038
12039fn check_bound_compatibility(
12040 field: &syn::Field,
12041 attrs: &FieldAttrs,
12042 kind: DetectedKind,
12043) -> syn::Result<()> {
12044 if attrs.max_length.is_some() && kind != DetectedKind::String {
12045 return Err(syn::Error::new_spanned(
12046 field,
12047 "`max_length` is only valid on `String` fields (or `Option<String>`)",
12048 ));
12049 }
12050 // These were silently dropped on the wrong type (#1937).
12051 if attrs.case_insensitive && kind != DetectedKind::String {
12052 return Err(syn::Error::new_spanned(
12053 field,
12054 "`citext` is only valid on `String` fields (or `Option<String>`)",
12055 ));
12056 }
12057 if attrs.vector_dims.is_some() && kind != DetectedKind::Vector {
12058 return Err(syn::Error::new_spanned(
12059 field,
12060 "`vector(dims = …)` is only valid on a pgvector `Vector` field",
12061 ));
12062 }
12063 if attrs.geometry_srid.is_some() && kind != DetectedKind::Geometry {
12064 return Err(syn::Error::new_spanned(
12065 field,
12066 "`geometry(srid = …)` is only valid on a PostGIS `Point` field",
12067 ));
12068 }
12069 if attrs.choices.is_some() && kind != DetectedKind::String {
12070 return Err(syn::Error::new_spanned(
12071 field,
12072 "`choices` is only valid on `String` fields (or `Option<String>`) — \
12073 integer-valued enumerations should be modeled with a Rust enum and \
12074 custom (de)serializer for now",
12075 ));
12076 }
12077 // They run only on string values, so elsewhere they would never fire.
12078 if !attrs.validators.is_empty() && kind != DetectedKind::String {
12079 return Err(syn::Error::new_spanned(
12080 field,
12081 "`validators` is only valid on `String` fields (or `Option<String>`)",
12082 ));
12083 }
12084 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
12085 return Err(syn::Error::new_spanned(
12086 field,
12087 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
12088 ));
12089 }
12090 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
12091 if min > max {
12092 return Err(syn::Error::new_spanned(
12093 field,
12094 format!("`min` ({min}) is greater than `max` ({max})"),
12095 ));
12096 }
12097 }
12098 if let Some(default) = &attrs.default {
12099 check_default_expr(default, kind).map_err(|msg| syn::Error::new_spanned(field, msg))?;
12100 }
12101 Ok(())
12102}
12103
12104/// SQL words a `default` may be on its own; any other bare word is a
12105/// column reference, which Postgres and MySQL refuse in a DEFAULT.
12106const DEFAULT_KEYWORDS: &[&str] = &[
12107 "NULL",
12108 "TRUE",
12109 "FALSE",
12110 "CURRENT_TIMESTAMP",
12111 "CURRENT_DATE",
12112 "CURRENT_TIME",
12113 "LOCALTIMESTAMP",
12114 "LOCALTIME",
12115];
12116
12117/// `default` is raw SQL. Refuse the shapes only SQLite accepts: a bare
12118/// word (read as a column on PG/MySQL) and a non-numeric literal on a
12119/// numeric field.
12120fn check_default_expr(default: &str, kind: DetectedKind) -> Result<(), String> {
12121 let expr = default.trim();
12122 let is_word = expr.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
12123 && expr
12124 .chars()
12125 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == ' ');
12126 let upper = expr.to_ascii_uppercase();
12127 if is_word && ["CURRENT_USER", "SESSION_USER", "USER"].contains(&upper.as_str()) {
12128 return Err(format!(
12129 "`default = \"{expr}\"` works on Postgres only: MySQL refuses it as a default \
12130 and SQLite stores the text `{expr}`; set the value in code instead"
12131 ));
12132 }
12133 if is_word && !DEFAULT_KEYWORDS.contains(&upper.as_str()) {
12134 return Err(format!(
12135 "`default = \"{expr}\"` is raw SQL, so `{expr}` is read as a column name; \
12136 for the text '{expr}' write `default = \"'{expr}'\"`"
12137 ));
12138 }
12139 let numeric = kind.is_integer()
12140 || matches!(
12141 kind,
12142 DetectedKind::F32 | DetectedKind::F64 | DetectedKind::Decimal
12143 );
12144 if numeric {
12145 let unquoted = expr
12146 .strip_prefix('\'')
12147 .and_then(|e| e.strip_suffix('\''))
12148 .unwrap_or(expr);
12149 let is_number = if kind.is_integer() {
12150 unquoted.parse::<i64>().is_ok()
12151 } else {
12152 unquoted.parse::<f64>().is_ok_and(f64::is_finite)
12153 };
12154 // Keep expressions (`(1 + 2)`, `abs(-1)`) and NULL; refuse a
12155 // literal that is no number. `""` renders as `''`.
12156 let is_literal = is_word || expr.is_empty() || expr.starts_with('\'');
12157 if is_literal && !is_number && upper != "NULL" {
12158 return Err(format!(
12159 "`default = \"{expr}\"` is not a number, but the field is numeric"
12160 ));
12161 }
12162 }
12163 Ok(())
12164}
12165
12166/// A `&'static [ty]` of `const fn` results. The slice needs its own
12167/// `const`: a `&[..]` assigned in a statement is not extended to `'static`.
12168fn const_slice(ty: &TokenStream2, items: impl Iterator<Item = TokenStream2>) -> TokenStream2 {
12169 quote!({
12170 const ITEMS: &[#ty] = &[ #(#items),* ];
12171 ITEMS
12172 })
12173}
12174
12175fn optional_u32(value: Option<u32>) -> TokenStream2 {
12176 if let Some(v) = value {
12177 quote!(::core::option::Option::Some(#v))
12178 } else {
12179 quote!(::core::option::Option::None)
12180 }
12181}
12182
12183fn optional_i64(value: Option<i64>) -> TokenStream2 {
12184 if let Some(v) = value {
12185 quote!(::core::option::Option::Some(#v))
12186 } else {
12187 quote!(::core::option::Option::None)
12188 }
12189}
12190
12191fn optional_str(value: Option<&str>) -> TokenStream2 {
12192 if let Some(v) = value {
12193 quote!(::core::option::Option::Some(#v))
12194 } else {
12195 quote!(::core::option::Option::None)
12196 }
12197}
12198
12199fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
12200 let Some(pairs) = pairs else {
12201 return quote!(::core::option::Option::None);
12202 };
12203 let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
12204 quote!(::core::option::Option::Some(&[#(#entries),*]))
12205}
12206
12207fn relation_tokens(
12208 field: &syn::Field,
12209 attrs: &FieldAttrs,
12210 fk_inner: Option<&syn::Type>,
12211 table: &str,
12212) -> syn::Result<TokenStream2> {
12213 let root = rustango_root();
12214 if let Some(inner) = fk_inner {
12215 if attrs.fk.is_some() || attrs.o2o.is_some() {
12216 return Err(syn::Error::new_spanned(
12217 field,
12218 "`ForeignKey<T>` already declares the FK target via the type parameter — \
12219 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
12220 ));
12221 }
12222 // No `on`: the target's PK column, which may be renamed.
12223 let on = match attrs.on.as_deref() {
12224 Some(on) => quote!(#on),
12225 None => quote! {
12226 match <#inner as #root::core::Model>::SCHEMA.pk_column() {
12227 ::core::option::Option::Some(c) => c,
12228 ::core::option::Option::None => "id",
12229 }
12230 },
12231 };
12232 return Ok(quote! {
12233 ::core::option::Option::Some(#root::core::Relation::fk(
12234 <#inner as #root::core::Model>::SCHEMA.table,
12235 #on,
12236 ))
12237 });
12238 }
12239 match (&attrs.fk, &attrs.o2o) {
12240 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
12241 field,
12242 "`fk` and `o2o` are mutually exclusive",
12243 )),
12244 (Some(to), None) => {
12245 let on = attrs.on.as_deref().unwrap_or("id");
12246 // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
12247 // the model's own table. Threaded as a literal string at
12248 // macro-expansion time to sidestep the const-eval cycle
12249 // that `Self::SCHEMA.table` would create when referenced
12250 // inside Self::SCHEMA's own initializer.
12251 let resolved = if to == "self" { table } else { to };
12252 Ok(quote! {
12253 ::core::option::Option::Some(#root::core::Relation::fk(#resolved, #on))
12254 })
12255 }
12256 (None, Some(to)) => {
12257 let on = attrs.on.as_deref().unwrap_or("id");
12258 let resolved = if to == "self" { table } else { to };
12259 Ok(quote! {
12260 ::core::option::Option::Some(#root::core::Relation::o2o(#resolved, #on))
12261 })
12262 }
12263 (None, None) => {
12264 if attrs.on.is_some() {
12265 return Err(syn::Error::new_spanned(
12266 field,
12267 "`on` requires `fk` or `o2o`",
12268 ));
12269 }
12270 Ok(quote!(::core::option::Option::None))
12271 }
12272 }
12273}
12274
12275/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
12276/// about kinds without depending on `rustango-core` (which would require a
12277/// proc-macro/normal split it doesn't have today).
12278#[derive(Clone, Copy, PartialEq, Eq)]
12279enum DetectedKind {
12280 I16,
12281 I32,
12282 I64,
12283 F32,
12284 F64,
12285 Bool,
12286 String,
12287 DateTime,
12288 Date,
12289 Time,
12290 Uuid,
12291 Json,
12292 Decimal,
12293 Binary,
12294 /// `Array<String>` → PG `text[]` (#341).
12295 ArrayText,
12296 /// `Array<i32>` → PG `integer[]` (#341).
12297 ArrayInt,
12298 /// `Array<i64>` → PG `bigint[]` (#341).
12299 ArrayBigInt,
12300 /// `Range<i32>` → PG `int4range` (#343).
12301 RangeInt,
12302 /// `Range<i64>` → PG `int8range` (#343).
12303 RangeBigInt,
12304 /// `Range<Decimal>` → PG `numrange` (#343).
12305 RangeNumeric,
12306 /// `Range<NaiveDate>` → PG `daterange` (#343).
12307 RangeDate,
12308 /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
12309 RangeDateTime,
12310 /// `HStore` → PG `hstore` (#342).
12311 HStore,
12312 /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
12313 /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
12314 /// in at the `FieldType` emission site (not carried on this enum).
12315 Vector,
12316 /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
12317 /// from the `#[rustango(geometry(srid = N))]` field attribute,
12318 /// threaded in at the `FieldType` emission site.
12319 Geometry,
12320}
12321
12322impl DetectedKind {
12323 fn variant_tokens(self) -> TokenStream2 {
12324 let root = rustango_root();
12325 match self {
12326 Self::I16 => quote!(#root::core::FieldType::I16),
12327 Self::I32 => quote!(#root::core::FieldType::I32),
12328 Self::I64 => quote!(#root::core::FieldType::I64),
12329 Self::F32 => quote!(#root::core::FieldType::F32),
12330 Self::F64 => quote!(#root::core::FieldType::F64),
12331 Self::Bool => quote!(#root::core::FieldType::Bool),
12332 Self::String => quote!(#root::core::FieldType::String),
12333 Self::DateTime => quote!(#root::core::FieldType::DateTime),
12334 Self::Date => quote!(#root::core::FieldType::Date),
12335 Self::Time => quote!(#root::core::FieldType::Time),
12336 Self::Uuid => quote!(#root::core::FieldType::Uuid),
12337 Self::Json => quote!(#root::core::FieldType::Json),
12338 Self::Decimal => quote!(#root::core::FieldType::Decimal),
12339 Self::Binary => quote!(#root::core::FieldType::Binary),
12340 Self::ArrayText => {
12341 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
12342 }
12343 Self::ArrayInt => {
12344 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
12345 }
12346 Self::ArrayBigInt => {
12347 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
12348 }
12349 Self::RangeInt => {
12350 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
12351 }
12352 Self::RangeBigInt => {
12353 quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
12354 }
12355 Self::RangeNumeric => {
12356 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
12357 }
12358 Self::RangeDate => {
12359 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
12360 }
12361 Self::RangeDateTime => {
12362 quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
12363 }
12364 Self::HStore => quote!(#root::core::FieldType::HStore),
12365 // Dimension comes from the `vector(dims = N)` attribute,
12366 // applied at the emission site; `0` here is just a fallback.
12367 Self::Vector => quote!(#root::core::FieldType::Vector(0)),
12368 // SRID comes from the `geometry(srid = N)` attribute, applied
12369 // at the emission site; `0` here is just a fallback.
12370 Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
12371 }
12372 }
12373
12374 fn is_integer(self) -> bool {
12375 matches!(self, Self::I16 | Self::I32 | Self::I64)
12376 }
12377}
12378
12379/// Result of walking a field's Rust type. `kind` is the underlying
12380/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
12381/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
12382/// `Some(<T>)` when the field was `ForeignKey<T>` (or
12383/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
12384#[derive(Clone, Copy)]
12385struct DetectedType<'a> {
12386 kind: DetectedKind,
12387 nullable: bool,
12388 auto: bool,
12389 fk_inner: Option<&'a syn::Type>,
12390}
12391
12392/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
12393/// non-`Auto` types — the caller should already have routed Auto-only
12394/// codegen through this helper, so a `None` indicates a macro-internal
12395/// invariant break.
12396fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
12397 let Type::Path(TypePath { path, qself: None }) = ty else {
12398 return None;
12399 };
12400 let last = path.segments.last()?;
12401 if last.ident != "Auto" {
12402 return None;
12403 }
12404 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
12405 return None;
12406 };
12407 args.args.iter().find_map(|a| match a {
12408 syn::GenericArgument::Type(t) => Some(t),
12409 _ => None,
12410 })
12411}
12412
12413/// The `Cast<C>` inside `ty` (bare or in an `Option`), for its schema record.
12414fn cast_type(ty: &syn::Type) -> Option<&syn::Type> {
12415 let Type::Path(TypePath { path, qself: None }) = ty else {
12416 return None;
12417 };
12418 let last = path.segments.last()?;
12419 if last.ident == "Cast" {
12420 return Some(ty);
12421 }
12422 if last.ident != "Option" {
12423 return None;
12424 }
12425 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
12426 return None;
12427 };
12428 match args.args.first()? {
12429 syn::GenericArgument::Type(inner) => cast_type(inner).filter(|_| args.args.len() == 1),
12430 _ => None,
12431 }
12432}
12433
12434fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
12435 let Type::Path(TypePath { path, qself: None }) = ty else {
12436 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
12437 };
12438 let last = path
12439 .segments
12440 .last()
12441 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
12442
12443 if last.ident == "Option" {
12444 let inner = generic_inner(ty, &last.arguments, "Option")?;
12445 let inner_det = detect_type(inner)?;
12446 if inner_det.nullable {
12447 return Err(syn::Error::new_spanned(
12448 ty,
12449 "nested Option is not supported",
12450 ));
12451 }
12452 if inner_det.auto {
12453 return Err(syn::Error::new_spanned(
12454 ty,
12455 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
12456 ));
12457 }
12458 return Ok(DetectedType {
12459 nullable: true,
12460 ..inner_det
12461 });
12462 }
12463
12464 if last.ident == "Auto" {
12465 let inner = generic_inner(ty, &last.arguments, "Auto")?;
12466 let inner_det = detect_type(inner)?;
12467 if inner_det.auto {
12468 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
12469 }
12470 if inner_det.nullable {
12471 return Err(syn::Error::new_spanned(
12472 ty,
12473 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
12474 ));
12475 }
12476 if inner_det.fk_inner.is_some() {
12477 return Err(syn::Error::new_spanned(
12478 ty,
12479 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
12480 ));
12481 }
12482 if !matches!(
12483 inner_det.kind,
12484 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
12485 ) {
12486 return Err(syn::Error::new_spanned(
12487 ty,
12488 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
12489 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
12490 (DEFAULT now())",
12491 ));
12492 }
12493 return Ok(DetectedType {
12494 auto: true,
12495 ..inner_det
12496 });
12497 }
12498
12499 if last.ident == "ForeignKey" {
12500 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
12501 // Resolve the FK column's underlying SQL type from `K`. When the
12502 // user wrote `ForeignKey<T>` without a key parameter, the type
12503 // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
12504 // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
12505 // recurse into K so the column DDL emits the right SQL type
12506 // (VARCHAR for String, UUID for Uuid, …) and the load_related
12507 // emitter knows which `SqlValue` variant to match.
12508 let kind = match key_ty {
12509 Some(k) => detect_type(k)?.kind,
12510 None => DetectedKind::I64,
12511 };
12512 return Ok(DetectedType {
12513 kind,
12514 nullable: false,
12515 auto: false,
12516 fk_inner: Some(inner),
12517 });
12518 }
12519
12520 let kind = match last.ident.to_string().as_str() {
12521 "i16" => DetectedKind::I16,
12522 "i32" => DetectedKind::I32,
12523 "i64" => DetectedKind::I64,
12524 "f32" => DetectedKind::F32,
12525 "f64" => DetectedKind::F64,
12526 "bool" => DetectedKind::Bool,
12527 "String" => DetectedKind::String,
12528 "DateTime" => DetectedKind::DateTime,
12529 "NaiveDate" => DetectedKind::Date,
12530 "NaiveTime" => DetectedKind::Time,
12531 "Uuid" => DetectedKind::Uuid,
12532 "Value" => DetectedKind::Json,
12533 "Decimal" => DetectedKind::Decimal,
12534 // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
12535 // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
12536 // — that would emit Binary DDL and decode-fail at runtime.
12537 "Vec" => {
12538 let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
12539 if let Type::Path(TypePath { path, qself: None }) = inner {
12540 if let Some(seg) = path.segments.last() {
12541 if seg.ident == "u8" && seg.arguments.is_empty() {
12542 return Ok(DetectedType {
12543 kind: DetectedKind::Binary,
12544 nullable: false,
12545 auto: false,
12546 fk_inner: None,
12547 });
12548 }
12549 }
12550 }
12551 return Err(syn::Error::new_spanned(
12552 ty,
12553 "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
12554 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
12555 ));
12556 }
12557 // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
12558 // `integer[]` / `bigint[]` (#341).
12559 "Array" => {
12560 let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
12561 let elem = match inner {
12562 Type::Path(TypePath { path, qself: None }) => {
12563 path.segments.last().map(|s| s.ident.to_string())
12564 }
12565 _ => None,
12566 };
12567 let kind = match elem.as_deref() {
12568 Some("String") => DetectedKind::ArrayText,
12569 Some("i32") => DetectedKind::ArrayInt,
12570 Some("i64") => DetectedKind::ArrayBigInt,
12571 _ => {
12572 return Err(syn::Error::new_spanned(
12573 ty,
12574 "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
12575 `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
12576 ));
12577 }
12578 };
12579 return Ok(DetectedType {
12580 kind,
12581 nullable: false,
12582 auto: false,
12583 fk_inner: None,
12584 });
12585 }
12586 // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
12587 // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
12588 // `int8range` / `numrange` / `daterange` / `tstzrange` (#343).
12589 "Range" => {
12590 let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
12591 let elem = match inner {
12592 Type::Path(TypePath { path, qself: None }) => {
12593 path.segments.last().map(|s| s.ident.to_string())
12594 }
12595 _ => None,
12596 };
12597 let kind = match elem.as_deref() {
12598 Some("i32") => DetectedKind::RangeInt,
12599 Some("i64") => DetectedKind::RangeBigInt,
12600 Some("Decimal") => DetectedKind::RangeNumeric,
12601 Some("NaiveDate") => DetectedKind::RangeDate,
12602 Some("DateTime") => DetectedKind::RangeDateTime,
12603 _ => {
12604 return Err(syn::Error::new_spanned(
12605 ty,
12606 "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
12607 `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
12608 `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
12609 (→ tstzrange) are supported (#343)",
12610 ));
12611 }
12612 };
12613 return Ok(DetectedType {
12614 kind,
12615 nullable: false,
12616 auto: false,
12617 fk_inner: None,
12618 });
12619 }
12620 // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
12621 // (the `CastValue` impl bridges logical↔stored); the field's own
12622 // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
12623 // schema just needs `FieldType::String`.
12624 "Cast" => {
12625 return Ok(DetectedType {
12626 kind: DetectedKind::String,
12627 nullable: false,
12628 auto: false,
12629 fk_inner: None,
12630 });
12631 }
12632 // `HStore` → PG `hstore` (#342). No generic
12633 // parameter — always a string→string map.
12634 "HStore" => {
12635 return Ok(DetectedType {
12636 kind: DetectedKind::HStore,
12637 nullable: false,
12638 auto: false,
12639 fk_inner: None,
12640 });
12641 }
12642 // `Vector` → pgvector `vector(N)` (#824). The dimension is
12643 // supplied by `#[rustango(vector(dims = N))]`, not the type.
12644 "Vector" => {
12645 return Ok(DetectedType {
12646 kind: DetectedKind::Vector,
12647 nullable: false,
12648 auto: false,
12649 fk_inner: None,
12650 });
12651 }
12652 // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
12653 // supplied by `#[rustango(geometry(srid = N))]`, not the type.
12654 "Point" => {
12655 return Ok(DetectedType {
12656 kind: DetectedKind::Geometry,
12657 nullable: false,
12658 auto: false,
12659 fk_inner: None,
12660 });
12661 }
12662 other => {
12663 return Err(syn::Error::new_spanned(
12664 ty,
12665 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)"),
12666 ));
12667 }
12668 };
12669 Ok(DetectedType {
12670 kind,
12671 nullable: false,
12672 auto: false,
12673 fk_inner: None,
12674 })
12675}
12676
12677fn generic_inner<'a>(
12678 ty: &'a Type,
12679 arguments: &'a PathArguments,
12680 wrapper: &str,
12681) -> syn::Result<&'a Type> {
12682 let PathArguments::AngleBracketed(args) = arguments else {
12683 return Err(syn::Error::new_spanned(
12684 ty,
12685 format!("{wrapper} requires a generic argument"),
12686 ));
12687 };
12688 args.args
12689 .iter()
12690 .find_map(|a| match a {
12691 GenericArgument::Type(t) => Some(t),
12692 _ => None,
12693 })
12694 .ok_or_else(|| {
12695 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12696 })
12697}
12698
12699/// Like [`generic_inner`] but pulls *two* type args — the first is
12700/// required, the second is optional. Used by the `ForeignKey<T, K>`
12701/// detection where K defaults to `i64` when omitted.
12702fn generic_pair<'a>(
12703 ty: &'a Type,
12704 arguments: &'a PathArguments,
12705 wrapper: &str,
12706) -> syn::Result<(&'a Type, Option<&'a Type>)> {
12707 let PathArguments::AngleBracketed(args) = arguments else {
12708 return Err(syn::Error::new_spanned(
12709 ty,
12710 format!("{wrapper} requires a generic argument"),
12711 ));
12712 };
12713 let mut types = args.args.iter().filter_map(|a| match a {
12714 GenericArgument::Type(t) => Some(t),
12715 _ => None,
12716 });
12717 let first = types.next().ok_or_else(|| {
12718 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
12719 })?;
12720 let second = types.next();
12721 Ok((first, second))
12722}
12723
12724fn to_snake_case(s: &str) -> String {
12725 let mut out = String::with_capacity(s.len() + 4);
12726 for (i, ch) in s.chars().enumerate() {
12727 if ch.is_ascii_uppercase() {
12728 if i > 0 {
12729 out.push('_');
12730 }
12731 out.push(ch.to_ascii_lowercase());
12732 } else {
12733 out.push(ch);
12734 }
12735 }
12736 out
12737}
12738
12739// ============================================================
12740// #[derive(Form)] — slice 8.4B
12741// ============================================================
12742
12743/// Per-field `#[form(...)]` attributes recognised by the derive.
12744#[derive(Default)]
12745struct FormFieldAttrs {
12746 min: Option<i64>,
12747 max: Option<i64>,
12748 min_length: Option<u32>,
12749 max_length: Option<u32>,
12750 /// `#[form(clean = "fn_name")]` — per-field clean hook.
12751 /// The named static method on the form struct is called after the
12752 /// field's typed parse + length/range checks; it gets the parsed
12753 /// value by reference and returns `Result<<FieldType>, String>`.
12754 /// On Ok, the returned value replaces the parsed one; on Err, the
12755 /// message is attached to the field error list. Issue #372.
12756 clean: Option<syn::Ident>,
12757}
12758
12759/// Container-level `#[form(...)]` attributes. Currently only the
12760/// cross-field `validate` hook (issue #373).
12761#[derive(Default)]
12762struct FormContainerAttrs {
12763 /// `#[form(validate = "fn_name")]` — whole-form clean hook.
12764 /// After every per-field parse succeeds, the named method on the
12765 /// form struct is called with `&self` and may return
12766 /// `Result<(), FormErrors>`. Errors merge into the field error
12767 /// list. Issue #373.
12768 validate: Option<syn::Ident>,
12769}
12770
12771/// Detected shape of a form field's Rust type.
12772#[derive(Clone, Copy)]
12773enum FormFieldKind {
12774 String,
12775 I16,
12776 I32,
12777 I64,
12778 F32,
12779 F64,
12780 Bool,
12781}
12782
12783impl FormFieldKind {
12784 fn parse_method(self) -> &'static str {
12785 match self {
12786 Self::I16 => "i16",
12787 Self::I32 => "i32",
12788 Self::I64 => "i64",
12789 Self::F32 => "f32",
12790 Self::F64 => "f64",
12791 // String + Bool don't go through `str::parse`; the codegen
12792 // handles them inline.
12793 Self::String | Self::Bool => "",
12794 }
12795 }
12796}
12797
12798/// A generated `derive(Form)` local; mixed-site so a same-named field can't shadow it.
12799fn form_local(name: &str) -> syn::Ident {
12800 syn::Ident::new(name, proc_macro2::Span::mixed_site())
12801}
12802
12803fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
12804 let root = rustango_root();
12805 let (errs, cleaned, data_param) = (
12806 form_local("__errors"),
12807 form_local("__cleaned"),
12808 form_local("data"),
12809 );
12810 let struct_name = &input.ident;
12811
12812 let Data::Struct(data) = &input.data else {
12813 return Err(syn::Error::new_spanned(
12814 struct_name,
12815 "Form can only be derived on structs",
12816 ));
12817 };
12818 let Fields::Named(named) = &data.fields else {
12819 return Err(syn::Error::new_spanned(
12820 struct_name,
12821 "Form requires a struct with named fields",
12822 ));
12823 };
12824
12825 // #373 — container-level `#[form(validate = "fn")]` hook.
12826 let container = parse_form_container_attrs(input)?;
12827 let post_field_clean: Vec<TokenStream2> = Vec::new();
12828 let _ = post_field_clean;
12829
12830 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12831 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12832
12833 for field in &named.named {
12834 let ident = field
12835 .ident
12836 .as_ref()
12837 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12838 let attrs = parse_form_field_attrs(field)?;
12839 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12840 let is_string = matches!(kind, FormFieldKind::String);
12841 let is_number = !is_string && !matches!(kind, FormFieldKind::Bool);
12842 if !is_string && (attrs.min_length.is_some() || attrs.max_length.is_some()) {
12843 return Err(syn::Error::new(
12844 field.span(),
12845 "`min_length` / `max_length` need a String field",
12846 ));
12847 }
12848 if !is_number && (attrs.min.is_some() || attrs.max.is_some()) {
12849 return Err(syn::Error::new(
12850 field.span(),
12851 "`min` / `max` need a numeric field",
12852 ));
12853 }
12854
12855 let name_lit = ident_name(ident);
12856 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12857 // #372 — append the per-field `clean_<field>` call right after
12858 // the parse block when the attribute is set. The clean fn
12859 // takes &T and returns Result<T, String>; on Err we attach
12860 // the message to the field error list without aborting, so
12861 // the caller sees every field error at once.
12862 let clean_block = if let Some(clean_fn) = &attrs.clean {
12863 quote! {
12864 if #errs.fields().get(#name_lit).is_none() {
12865 match Self::#clean_fn(&#ident) {
12866 ::core::result::Result::Ok(#cleaned) => { #ident = #cleaned; }
12867 ::core::result::Result::Err(__msg) => {
12868 #errs.add(#name_lit, __msg);
12869 }
12870 }
12871 }
12872 }
12873 } else {
12874 quote! {}
12875 };
12876 field_blocks.push(quote! {
12877 #parse_block
12878 #clean_block
12879 });
12880 field_idents.push(ident);
12881 }
12882
12883 // #373 — after every per-field parse + clean succeeds, call the
12884 // cross-field validator if declared. Errors merge into the
12885 // outgoing FormErrors via the existing `FormErrors::merge` helper
12886 // (same primitive the serializer cross-field hook uses).
12887 let cross_field_call = if let Some(validate_fn) = &container.validate {
12888 quote! {
12889 if #errs.is_empty() {
12890 let __candidate = Self { #( #field_idents ),* };
12891 if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12892 #errs.merge(__other);
12893 }
12894 if !#errs.is_empty() {
12895 return ::core::result::Result::Err(#errs);
12896 }
12897 return ::core::result::Result::Ok(__candidate);
12898 }
12899 }
12900 } else {
12901 quote! {}
12902 };
12903
12904 Ok(quote! {
12905 impl #root::forms::Form for #struct_name {
12906 fn parse(
12907 #data_param: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12908 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12909 let mut #errs = #root::forms::FormErrors::default();
12910 #( #field_blocks )*
12911 #cross_field_call
12912 if !#errs.is_empty() {
12913 return ::core::result::Result::Err(#errs);
12914 }
12915 ::core::result::Result::Ok(Self {
12916 #( #field_idents ),*
12917 })
12918 }
12919 }
12920 })
12921}
12922
12923fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12924 let mut out = FormContainerAttrs::default();
12925 for attr in &input.attrs {
12926 if !attr.path().is_ident("form") {
12927 continue;
12928 }
12929 attr.parse_nested_meta(|meta| {
12930 if meta.path.is_ident("validate") {
12931 let s: LitStr = meta.value()?.parse()?;
12932 out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12933 return Ok(());
12934 }
12935 Err(meta.error("unknown form container attribute (supported: `validate`)"))
12936 })?;
12937 }
12938 Ok(out)
12939}
12940
12941fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12942 let mut out = FormFieldAttrs::default();
12943 for attr in &field.attrs {
12944 if !attr.path().is_ident("form") {
12945 continue;
12946 }
12947 attr.parse_nested_meta(|meta| {
12948 if meta.path.is_ident("min") {
12949 let lit: syn::LitInt = meta.value()?.parse()?;
12950 out.min = Some(lit.base10_parse::<i64>()?);
12951 return Ok(());
12952 }
12953 if meta.path.is_ident("max") {
12954 let lit: syn::LitInt = meta.value()?.parse()?;
12955 out.max = Some(lit.base10_parse::<i64>()?);
12956 return Ok(());
12957 }
12958 if meta.path.is_ident("min_length") {
12959 let lit: syn::LitInt = meta.value()?.parse()?;
12960 out.min_length = Some(lit.base10_parse::<u32>()?);
12961 return Ok(());
12962 }
12963 if meta.path.is_ident("max_length") {
12964 let lit: syn::LitInt = meta.value()?.parse()?;
12965 out.max_length = Some(lit.base10_parse::<u32>()?);
12966 return Ok(());
12967 }
12968 if meta.path.is_ident("clean") {
12969 let s: LitStr = meta.value()?.parse()?;
12970 out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12971 return Ok(());
12972 }
12973 Err(meta.error(
12974 "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12975 ))
12976 })?;
12977 }
12978 Ok(out)
12979}
12980
12981fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12982 let Type::Path(TypePath { path, qself: None }) = ty else {
12983 return Err(syn::Error::new(
12984 span,
12985 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12986 ));
12987 };
12988 let last = path
12989 .segments
12990 .last()
12991 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12992
12993 if last.ident == "Option" {
12994 let inner = generic_inner(ty, &last.arguments, "Option")?;
12995 let (kind, nested) = detect_form_field(inner, span)?;
12996 if nested {
12997 return Err(syn::Error::new(
12998 span,
12999 "nested Option in Form fields is not supported",
13000 ));
13001 }
13002 return Ok((kind, true));
13003 }
13004
13005 let kind = match last.ident.to_string().as_str() {
13006 "String" => FormFieldKind::String,
13007 "i16" => FormFieldKind::I16,
13008 "i32" => FormFieldKind::I32,
13009 "i64" => FormFieldKind::I64,
13010 "f32" => FormFieldKind::F32,
13011 "f64" => FormFieldKind::F64,
13012 "bool" => FormFieldKind::Bool,
13013 other => {
13014 return Err(syn::Error::new(
13015 span,
13016 format!(
13017 "Form field type `{other}` is not supported in v0.8 — use String / \
13018 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
13019 ),
13020 ));
13021 }
13022 };
13023 Ok((kind, false))
13024}
13025
13026#[allow(clippy::too_many_lines)]
13027fn render_form_field_parse(
13028 ident: &syn::Ident,
13029 name_lit: &str,
13030 kind: FormFieldKind,
13031 nullable: bool,
13032 attrs: &FormFieldAttrs,
13033) -> TokenStream2 {
13034 let (errs, data_param) = (form_local("__errors"), form_local("data"));
13035 // Pull the raw &str from the payload `Form::parse(data: &HashMap<…>)`.
13036 let lookup = quote! {
13037 let __raw: ::core::option::Option<&::std::string::String> = #data_param.get(#name_lit);
13038 };
13039
13040 let root = rustango_root();
13041 // Strict, as ModelForm: an unknown spelling is an error, not `true`.
13042 let checkbox = quote! {
13043 match #root::forms::checkbox(__s) {
13044 ::core::option::Option::Some(__b) => __b,
13045 ::core::option::Option::None => {
13046 #errs.add(#name_lit, "Enter true or false.");
13047 false
13048 }
13049 }
13050 };
13051 let parsed_value = match kind {
13052 // `Option<bool>`: absent or blank is `None`, as for the other types.
13053 FormFieldKind::Bool if nullable => quote! {
13054 let __v: ::core::option::Option<bool> = match __raw {
13055 ::core::option::Option::None => ::core::option::Option::None,
13056 ::core::option::Option::Some(__s) if __s.is_empty() => {
13057 ::core::option::Option::None
13058 }
13059 ::core::option::Option::Some(__s) => ::core::option::Option::Some(#checkbox),
13060 };
13061 },
13062 FormFieldKind::Bool => quote! {
13063 let __v: bool = match __raw {
13064 ::core::option::Option::None => false,
13065 ::core::option::Option::Some(__s) => #checkbox,
13066 };
13067 },
13068 FormFieldKind::String => {
13069 if nullable {
13070 quote! {
13071 let __v: ::core::option::Option<::std::string::String> = match __raw {
13072 ::core::option::Option::None => ::core::option::Option::None,
13073 ::core::option::Option::Some(__s) if __s.is_empty() => {
13074 ::core::option::Option::None
13075 }
13076 ::core::option::Option::Some(__s) => {
13077 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
13078 }
13079 };
13080 }
13081 } else {
13082 quote! {
13083 let __v: ::std::string::String = match __raw {
13084 ::core::option::Option::Some(__s) if !__s.is_empty() => {
13085 ::core::clone::Clone::clone(__s)
13086 }
13087 _ => {
13088 #errs.add(#name_lit, "This field is required.");
13089 ::std::string::String::new()
13090 }
13091 };
13092 }
13093 }
13094 }
13095 FormFieldKind::I16
13096 | FormFieldKind::I32
13097 | FormFieldKind::I64
13098 | FormFieldKind::F32
13099 | FormFieldKind::F64 => {
13100 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
13101 let ty_lit = kind.parse_method();
13102 let default_val = match kind {
13103 FormFieldKind::I16 => quote! { 0i16 },
13104 FormFieldKind::I32 => quote! { 0i32 },
13105 FormFieldKind::I64 => quote! { 0i64 },
13106 FormFieldKind::F32 => quote! { 0f32 },
13107 FormFieldKind::F64 => quote! { 0f64 },
13108 _ => quote! { Default::default() },
13109 };
13110 // NaN and inf parse, then pass every min/max compare.
13111 let finite = if matches!(kind, FormFieldKind::F32 | FormFieldKind::F64) {
13112 quote! {
13113 .and_then(|__n| if __n.is_finite() {
13114 ::core::result::Result::Ok(__n)
13115 } else {
13116 ::core::result::Result::Err(::std::string::String::from("not a finite number"))
13117 })
13118 }
13119 } else {
13120 quote! {}
13121 };
13122 if nullable {
13123 quote! {
13124 let __v: ::core::option::Option<#parse_ty> = match __raw {
13125 ::core::option::Option::None => ::core::option::Option::None,
13126 ::core::option::Option::Some(__s) if __s.is_empty() => {
13127 ::core::option::Option::None
13128 }
13129 ::core::option::Option::Some(__s) => {
13130 match __s.parse::<#parse_ty>().map_err(|__e| ::std::string::ToString::to_string(&__e)) #finite {
13131 ::core::result::Result::Ok(__n) => {
13132 ::core::option::Option::Some(__n)
13133 }
13134 ::core::result::Result::Err(__e) => {
13135 #errs.add(
13136 #name_lit,
13137 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
13138 );
13139 ::core::option::Option::None
13140 }
13141 }
13142 }
13143 };
13144 }
13145 } else {
13146 quote! {
13147 let __v: #parse_ty = match __raw {
13148 ::core::option::Option::Some(__s) if !__s.is_empty() => {
13149 match __s.parse::<#parse_ty>().map_err(|__e| ::std::string::ToString::to_string(&__e)) #finite {
13150 ::core::result::Result::Ok(__n) => __n,
13151 ::core::result::Result::Err(__e) => {
13152 #errs.add(
13153 #name_lit,
13154 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
13155 );
13156 #default_val
13157 }
13158 }
13159 }
13160 _ => {
13161 #errs.add(#name_lit, "This field is required.");
13162 #default_val
13163 }
13164 };
13165 }
13166 }
13167 }
13168 };
13169
13170 let validators = render_form_validators(name_lit, kind, nullable, attrs);
13171
13172 quote! {
13173 // `mut` so the per-field `clean` hook (#372) can rewrite the
13174 // parsed value in-place when it returns Ok with a normalized
13175 // form (e.g. trim / lowercase).
13176 let mut #ident = {
13177 #lookup
13178 #parsed_value
13179 #validators
13180 __v
13181 };
13182 }
13183}
13184
13185fn render_form_validators(
13186 name_lit: &str,
13187 kind: FormFieldKind,
13188 nullable: bool,
13189 attrs: &FormFieldAttrs,
13190) -> TokenStream2 {
13191 let errs = form_local("__errors");
13192 let mut checks: Vec<TokenStream2> = Vec::new();
13193
13194 let val_ref = if nullable {
13195 quote! { __v.as_ref() }
13196 } else {
13197 quote! { ::core::option::Option::Some(&__v) }
13198 };
13199
13200 let is_string = matches!(kind, FormFieldKind::String);
13201 let is_numeric = matches!(
13202 kind,
13203 FormFieldKind::I16
13204 | FormFieldKind::I32
13205 | FormFieldKind::I64
13206 | FormFieldKind::F32
13207 | FormFieldKind::F64
13208 );
13209
13210 if is_string {
13211 if let Some(min_len) = attrs.min_length {
13212 let min_len_usize = min_len as usize;
13213 checks.push(quote! {
13214 if let ::core::option::Option::Some(__s) = #val_ref {
13215 if __s.chars().count() < #min_len_usize {
13216 #errs.add(
13217 #name_lit,
13218 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
13219 );
13220 }
13221 }
13222 });
13223 }
13224 if let Some(max_len) = attrs.max_length {
13225 let max_len_usize = max_len as usize;
13226 checks.push(quote! {
13227 if let ::core::option::Option::Some(__s) = #val_ref {
13228 if __s.chars().count() > #max_len_usize {
13229 #errs.add(
13230 #name_lit,
13231 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
13232 );
13233 }
13234 }
13235 });
13236 }
13237 }
13238
13239 if is_numeric {
13240 if let Some(min) = attrs.min {
13241 checks.push(quote! {
13242 if let ::core::option::Option::Some(__n) = #val_ref {
13243 if (*__n as f64) < (#min as f64) {
13244 #errs.add(
13245 #name_lit,
13246 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
13247 );
13248 }
13249 }
13250 });
13251 }
13252 if let Some(max) = attrs.max {
13253 checks.push(quote! {
13254 if let ::core::option::Option::Some(__n) = #val_ref {
13255 if (*__n as f64) > (#max as f64) {
13256 #errs.add(
13257 #name_lit,
13258 ::std::format!("Ensure this value is less than or equal to {}.", #max),
13259 );
13260 }
13261 }
13262 });
13263 }
13264 }
13265
13266 quote! { #( #checks )* }
13267}
13268
13269// ============================================================
13270// #[derive(ViewSet)]
13271// ============================================================
13272
13273struct ViewSetAttrs {
13274 model: syn::Path,
13275 fields: Vec<String>,
13276 filter_fields: Vec<String>,
13277 search_fields: Vec<String>,
13278 /// (field_name, desc)
13279 ordering: Vec<(String, bool)>,
13280 page_size: Option<usize>,
13281 read_only: bool,
13282 allow_anonymous: bool,
13283 perms: ViewSetPermsAttrs,
13284 /// `#[viewset(serializer = SomeSerializer)]` — render list /
13285 /// retrieve / create responses through this `#[derive(Serializer)]`
13286 /// type instead of the default field-level projection (requires the
13287 /// `serializer` feature). Tri-dialect.
13288 serializer: Option<syn::Path>,
13289}
13290
13291#[derive(Default)]
13292struct ViewSetPermsAttrs {
13293 list: Vec<String>,
13294 retrieve: Vec<String>,
13295 create: Vec<String>,
13296 update: Vec<String>,
13297 destroy: Vec<String>,
13298}
13299
13300fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
13301 let root = rustango_root();
13302 let struct_name = &input.ident;
13303
13304 // Must be a unit struct or an empty named struct.
13305 match &input.data {
13306 Data::Struct(s) => match &s.fields {
13307 Fields::Unit | Fields::Named(_) => {}
13308 Fields::Unnamed(_) => {
13309 return Err(syn::Error::new_spanned(
13310 struct_name,
13311 "ViewSet can only be derived on a unit struct or an empty named struct",
13312 ));
13313 }
13314 },
13315 _ => {
13316 return Err(syn::Error::new_spanned(
13317 struct_name,
13318 "ViewSet can only be derived on a struct",
13319 ));
13320 }
13321 }
13322
13323 let attrs = parse_viewset_attrs(input)?;
13324 let model_path = &attrs.model;
13325
13326 let field_lits = attrs.fields.iter().map(|f| f.as_str());
13327
13328 let filter_fields_call = if attrs.filter_fields.is_empty() {
13329 quote!()
13330 } else {
13331 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
13332 quote!(.filter_fields(&[ #(#lits),* ]))
13333 };
13334
13335 let search_fields_call = if attrs.search_fields.is_empty() {
13336 quote!()
13337 } else {
13338 let lits = attrs.search_fields.iter().map(|f| f.as_str());
13339 quote!(.search_fields(&[ #(#lits),* ]))
13340 };
13341
13342 let ordering_call = if attrs.ordering.is_empty() {
13343 quote!()
13344 } else {
13345 let pairs = attrs.ordering.iter().map(|(f, desc)| {
13346 let f = f.as_str();
13347 quote!((#f, #desc))
13348 });
13349 quote!(.ordering(&[ #(#pairs),* ]))
13350 };
13351
13352 let page_size_call = if let Some(n) = attrs.page_size {
13353 quote!(.page_size(#n))
13354 } else {
13355 quote!()
13356 };
13357
13358 let read_only_call = if attrs.read_only {
13359 quote!(.read_only())
13360 } else {
13361 quote!()
13362 };
13363 let allow_anonymous_call = if attrs.allow_anonymous {
13364 quote!(.allow_anonymous())
13365 } else {
13366 quote!()
13367 };
13368
13369 // `.serializer::<S>()` — reshape responses through a derived
13370 // serializer. Requires the downstream crate to enable the
13371 // `serializer` feature (the method is gated on it).
13372 let serializer_call = if let Some(ref ser) = attrs.serializer {
13373 quote!(.serializer::<#ser>())
13374 } else {
13375 quote!()
13376 };
13377
13378 let perms = &attrs.perms;
13379 let perms_call = if perms.list.is_empty()
13380 && perms.retrieve.is_empty()
13381 && perms.create.is_empty()
13382 && perms.update.is_empty()
13383 && perms.destroy.is_empty()
13384 {
13385 quote!()
13386 } else {
13387 let list_lits = perms.list.iter().map(|s| s.as_str());
13388 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
13389 let create_lits = perms.create.iter().map(|s| s.as_str());
13390 let update_lits = perms.update.iter().map(|s| s.as_str());
13391 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
13392 quote! {
13393 .permissions(#root::viewset::ViewSetPerms {
13394 list: ::std::vec![ #(#list_lits.to_owned()),* ],
13395 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
13396 create: ::std::vec![ #(#create_lits.to_owned()),* ],
13397 update: ::std::vec![ #(#update_lits.to_owned()),* ],
13398 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
13399 })
13400 }
13401 };
13402
13403 Ok(quote! {
13404 impl #struct_name {
13405 /// Build an `axum::Router` with the six standard REST endpoints
13406 /// for this ViewSet, mounted at `prefix`.
13407 ///
13408 /// Accepts any backend's pool — a `sqlx::PgPool`,
13409 /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
13410 /// enum — because they all `Into<Pool>` (#1273). This is why
13411 /// the derive no longer names `PgPool`, which would not
13412 /// exist on a project built without the `postgres` feature.
13413 pub fn router(
13414 prefix: &str,
13415 pool: impl ::core::convert::Into<#root::sql::Pool>,
13416 ) -> #root::__axum::Router {
13417 #root::viewset::ViewSet::for_model(
13418 <#model_path as #root::core::Model>::SCHEMA,
13419 &[ #(#field_lits),* ],
13420 )
13421 #filter_fields_call
13422 #search_fields_call
13423 #ordering_call
13424 #page_size_call
13425 #perms_call
13426 #read_only_call
13427 #allow_anonymous_call
13428 #serializer_call
13429 .router_pool(prefix, pool.into())
13430 }
13431 }
13432 })
13433}
13434
13435fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
13436 let mut model: Option<syn::Path> = None;
13437 let mut fields: Option<Vec<String>> = None;
13438 let mut filter_fields: Vec<String> = Vec::new();
13439 let mut search_fields: Vec<String> = Vec::new();
13440 let mut ordering: Vec<(String, bool)> = Vec::new();
13441 let mut page_size: Option<usize> = None;
13442 let mut read_only = false;
13443 let mut allow_anonymous = false;
13444 let mut perms = ViewSetPermsAttrs::default();
13445 let mut serializer: Option<syn::Path> = None;
13446
13447 for attr in &input.attrs {
13448 if !attr.path().is_ident("viewset") {
13449 continue;
13450 }
13451 attr.parse_nested_meta(|meta| {
13452 if meta.path.is_ident("model") {
13453 let path: syn::Path = meta.value()?.parse()?;
13454 model = Some(path);
13455 return Ok(());
13456 }
13457 if meta.path.is_ident("serializer") {
13458 let path: syn::Path = meta.value()?.parse()?;
13459 serializer = Some(path);
13460 return Ok(());
13461 }
13462 if meta.path.is_ident("fields") {
13463 let s: LitStr = meta.value()?.parse()?;
13464 fields = Some(split_field_list(&s.value()));
13465 return Ok(());
13466 }
13467 if meta.path.is_ident("filter_fields") {
13468 let s: LitStr = meta.value()?.parse()?;
13469 filter_fields = split_field_list(&s.value());
13470 return Ok(());
13471 }
13472 if meta.path.is_ident("search_fields") {
13473 let s: LitStr = meta.value()?.parse()?;
13474 search_fields = split_field_list(&s.value());
13475 return Ok(());
13476 }
13477 if meta.path.is_ident("ordering") {
13478 let s: LitStr = meta.value()?.parse()?;
13479 ordering = parse_ordering_list(&s.value());
13480 return Ok(());
13481 }
13482 if meta.path.is_ident("page_size") {
13483 let lit: syn::LitInt = meta.value()?.parse()?;
13484 page_size = Some(lit.base10_parse::<usize>()?);
13485 return Ok(());
13486 }
13487 if meta.path.is_ident("read_only") {
13488 read_only = true;
13489 return Ok(());
13490 }
13491 if meta.path.is_ident("allow_anonymous") {
13492 allow_anonymous = true;
13493 return Ok(());
13494 }
13495 if meta.path.is_ident("permissions") {
13496 meta.parse_nested_meta(|inner| {
13497 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
13498 let s: LitStr = inner.value()?.parse()?;
13499 Ok(split_field_list(&s.value()))
13500 };
13501 if inner.path.is_ident("list") {
13502 perms.list = parse_codenames(&inner)?;
13503 } else if inner.path.is_ident("retrieve") {
13504 perms.retrieve = parse_codenames(&inner)?;
13505 } else if inner.path.is_ident("create") {
13506 perms.create = parse_codenames(&inner)?;
13507 } else if inner.path.is_ident("update") {
13508 perms.update = parse_codenames(&inner)?;
13509 } else if inner.path.is_ident("destroy") {
13510 perms.destroy = parse_codenames(&inner)?;
13511 } else {
13512 return Err(inner.error(
13513 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
13514 ));
13515 }
13516 Ok(())
13517 })?;
13518 return Ok(());
13519 }
13520 Err(meta.error(
13521 "unknown viewset attribute (supported: model, fields, filter_fields, \
13522 search_fields, ordering, page_size, read_only, allow_anonymous, serializer, \
13523 permissions(...))",
13524 ))
13525 })?;
13526 }
13527
13528 let model = model.ok_or_else(|| {
13529 syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
13530 })?;
13531 // The field list is the write allow-list, so it is never implied.
13532 let fields = fields.ok_or_else(|| {
13533 syn::Error::new_spanned(
13534 &input.ident,
13535 "`#[viewset(fields = \"id, title\")]` is required: the fields the API renders and writes",
13536 )
13537 })?;
13538
13539 Ok(ViewSetAttrs {
13540 model,
13541 fields,
13542 filter_fields,
13543 search_fields,
13544 ordering,
13545 page_size,
13546 read_only,
13547 allow_anonymous,
13548 perms,
13549 serializer,
13550 })
13551}
13552
13553// ============================================================ #[derive(Serializer)]
13554
13555struct SerializerContainerAttrs {
13556 model: syn::Path,
13557 /// `#[serializer(validate = "fn_name")]` on the struct — the
13558 /// cross-field validation hook (#436). The named inherent method
13559 /// must take `&self` and return
13560 /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
13561 /// `validate()` runs every per-field validator first; then calls
13562 /// the cross-field method if declared; aggregates all errors into
13563 /// one `FormErrors`.
13564 cross_validate: Option<syn::Ident>,
13565}
13566
13567#[derive(Default)]
13568struct SerializerFieldAttrs {
13569 read_only: bool,
13570 write_only: bool,
13571 source: Option<String>,
13572 skip: bool,
13573 /// `#[serializer(method = "fn_name")]` — computed field. The macro
13574 /// emits a `from_model` initializer that calls
13575 /// `Self::fn_name(&model)` and stores the return value.
13576 method: Option<String>,
13577 /// `#[serializer(validate = "fn_name")]` — per-field validator
13578 /// callable run by `Self::validate(&self)`. Must return
13579 /// `Result<(), String>`. Errors land in `FormErrors` keyed by
13580 /// the field name.
13581 validate: Option<String>,
13582 /// `#[serializer(nested)]` on a field whose type is another
13583 /// `Serializer` — the macro emits `from_model` initializer that
13584 /// reads the parent via `model.<source>.value()` then calls the
13585 /// child serializer's `from_model(parent)`. When the FK is
13586 /// unloaded the field falls back to `Default::default()` (does
13587 /// NOT panic) so a missing prefetch in prod degrades gracefully.
13588 /// Source field on the model defaults to the field name; override
13589 /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
13590 /// panic-on-unloaded behavior for tests.
13591 nested: bool,
13592 /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
13593 /// strict behavior: panic when the FK isn't loaded. Useful in
13594 /// test code where forgetting select_related must trip a hard
13595 /// failure rather than render a blank nested object.
13596 nested_strict: bool,
13597 /// `#[serializer(many = TagSerializer)]` — declare the field as
13598 /// a list of nested serializers. Field type must be `Vec<S>`
13599 /// where `S` is the inner serializer. The macro initializes the
13600 /// field to `Vec::new()` in `from_model` and emits a typed
13601 /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
13602 /// maps each model row through `S::from_model`. Auto-load isn't
13603 /// possible (the M2M / one-to-many accessor is async); callers
13604 /// fetch the children + call the setter post-from_model.
13605 many: Option<syn::Type>,
13606 /// `#[serializer(slug = "name")]` — render a relation as one of
13607 /// its fields.
13608 /// Source field on the model must be a `ForeignKey<T>`; the
13609 /// macro emits `from_model` glue that walks
13610 /// `model.<source>.value()?.<slug>` and clones it. Field type on
13611 /// the serializer is typically `String` (whatever type the slug
13612 /// column has). When the FK is unloaded the field falls back to
13613 /// `Default::default()`, same graceful-degrade contract as
13614 /// `nested`. Source defaults to the field name; override with
13615 /// `source = "..."`. v0.44.
13616 slug: Option<String>,
13617 /// `#[serializer(max_length = N)]` — caps
13618 /// the character count of a string field on write. Overrides the
13619 /// model's `max_length`; when absent the model value is inherited.
13620 max_length: Option<u64>,
13621 /// `#[serializer(min_length = N)]` — minimum character count.
13622 /// Serializer-only (the model has no `min_length` column).
13623 min_length: Option<u64>,
13624 /// `#[serializer(min = N)]` — inclusive
13625 /// integer lower bound; overrides the model's `min` when given.
13626 min: Option<i64>,
13627 /// `#[serializer(max = N)]` — inclusive
13628 /// integer upper bound; overrides the model's `max` when given.
13629 max: Option<i64>,
13630}
13631
13632fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
13633 let mut model: Option<syn::Path> = None;
13634 let mut cross_validate: Option<syn::Ident> = None;
13635 for attr in &input.attrs {
13636 if !attr.path().is_ident("serializer") {
13637 continue;
13638 }
13639 attr.parse_nested_meta(|meta| {
13640 if meta.path.is_ident("model") {
13641 let _eq: syn::Token![=] = meta.input.parse()?;
13642 model = Some(meta.input.parse()?);
13643 return Ok(());
13644 }
13645 if meta.path.is_ident("validate") {
13646 // #436 — container-level `validate = "fn_name"` for
13647 // cross-field validation. Field-level
13648 // `#[serializer(validate = "...")]` on a field is
13649 // parsed separately in `parse_serializer_field_attrs`.
13650 let s: LitStr = meta.value()?.parse()?;
13651 cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
13652 return Ok(());
13653 }
13654 Err(meta.error(
13655 "unknown serializer container attribute \
13656 (supported: `model`, `validate`)",
13657 ))
13658 })?;
13659 }
13660 let model = model.ok_or_else(|| {
13661 syn::Error::new_spanned(
13662 &input.ident,
13663 "`#[serializer(model = SomeModel)]` is required",
13664 )
13665 })?;
13666 Ok(SerializerContainerAttrs {
13667 model,
13668 cross_validate,
13669 })
13670}
13671
13672/// The `#[serde(...)]` keys the Serializer derive honours. Its own
13673/// `Serialize` impl would otherwise drop them silently.
13674#[derive(Default)]
13675struct SerdeWireAttrs {
13676 rename_ser: Option<String>,
13677 rename_de: Option<String>,
13678 skip_ser: bool,
13679 skip_de: bool,
13680 skip_ser_if: Option<syn::ExprPath>,
13681}
13682
13683/// A serde `rename_all` rule, applied to a `snake_case` field name.
13684#[derive(Clone, Copy)]
13685enum SerdeRenameRule {
13686 Lower,
13687 Upper,
13688 Pascal,
13689 Camel,
13690 ScreamingSnake,
13691 Kebab,
13692 ScreamingKebab,
13693}
13694
13695impl SerdeRenameRule {
13696 fn parse(s: &LitStr) -> syn::Result<Self> {
13697 Ok(match s.value().as_str() {
13698 "lowercase" | "snake_case" => Self::Lower,
13699 "UPPERCASE" => Self::Upper,
13700 "PascalCase" => Self::Pascal,
13701 "camelCase" => Self::Camel,
13702 "SCREAMING_SNAKE_CASE" => Self::ScreamingSnake,
13703 "kebab-case" => Self::Kebab,
13704 "SCREAMING-KEBAB-CASE" => Self::ScreamingKebab,
13705 _ => {
13706 return Err(syn::Error::new_spanned(
13707 s,
13708 "unknown serde `rename_all` rule",
13709 ))
13710 }
13711 })
13712 }
13713
13714 /// Same output as serde's own field renaming.
13715 fn apply(self, field: &str) -> String {
13716 match self {
13717 Self::Lower => field.to_owned(),
13718 Self::Upper | Self::ScreamingSnake => field.to_ascii_uppercase(),
13719 Self::Pascal | Self::Camel => {
13720 let mut out = String::new();
13721 let mut cap = matches!(self, Self::Pascal);
13722 for ch in field.chars() {
13723 if ch == '_' {
13724 cap = !out.is_empty() || matches!(self, Self::Pascal);
13725 } else if cap {
13726 out.push(ch.to_ascii_uppercase());
13727 cap = false;
13728 } else {
13729 out.push(ch);
13730 }
13731 }
13732 out
13733 }
13734 Self::Kebab => field.replace('_', "-"),
13735 Self::ScreamingKebab => field.to_ascii_uppercase().replace('_', "-"),
13736 }
13737 }
13738}
13739
13740/// Consume the value of a `serde` key the derive does not act on.
13741fn skip_serde_meta_value(meta: &syn::meta::ParseNestedMeta) -> syn::Result<()> {
13742 if meta.input.peek(syn::Token![=]) {
13743 let _: syn::Expr = meta.value()?.parse()?;
13744 } else if meta.input.peek(syn::token::Paren) {
13745 let _: proc_macro2::Group = meta.input.parse()?;
13746 }
13747 Ok(())
13748}
13749
13750/// `rename = "x"` or `rename(serialize = "a", deserialize = "b")`.
13751fn parse_serde_rename(
13752 meta: &syn::meta::ParseNestedMeta,
13753 mut set: impl FnMut(bool, LitStr) -> syn::Result<()>,
13754) -> syn::Result<()> {
13755 if meta.input.peek(syn::Token![=]) {
13756 let s: LitStr = meta.value()?.parse()?;
13757 set(true, s.clone())?;
13758 return set(false, s);
13759 }
13760 meta.parse_nested_meta(|inner| {
13761 let is_ser = inner.path.is_ident("serialize");
13762 if !is_ser && !inner.path.is_ident("deserialize") {
13763 return Err(inner.error("expected `serialize` or `deserialize`"));
13764 }
13765 set(is_ser, inner.value()?.parse()?)
13766 })
13767}
13768
13769/// Container `rename_all` rules: (serialize, deserialize).
13770fn parse_serde_container_rules(
13771 input: &DeriveInput,
13772) -> syn::Result<(Option<SerdeRenameRule>, Option<SerdeRenameRule>)> {
13773 let (mut ser, mut de) = (None, None);
13774 for attr in input.attrs.iter().filter(|a| a.path().is_ident("serde")) {
13775 attr.parse_nested_meta(|meta| {
13776 if meta.path.is_ident("rename_all") {
13777 return parse_serde_rename(&meta, |is_ser, s| {
13778 let rule = Some(SerdeRenameRule::parse(&s)?);
13779 if is_ser {
13780 ser = rule;
13781 } else {
13782 de = rule;
13783 }
13784 Ok(())
13785 });
13786 }
13787 skip_serde_meta_value(&meta)
13788 })?;
13789 }
13790 Ok((ser, de))
13791}
13792
13793fn parse_serde_field_attrs(field: &syn::Field) -> syn::Result<SerdeWireAttrs> {
13794 let mut out = SerdeWireAttrs::default();
13795 for attr in field.attrs.iter().filter(|a| a.path().is_ident("serde")) {
13796 attr.parse_nested_meta(|meta| {
13797 if meta.path.is_ident("rename") {
13798 return parse_serde_rename(&meta, |is_ser, s| {
13799 if is_ser {
13800 out.rename_ser = Some(s.value());
13801 } else {
13802 out.rename_de = Some(s.value());
13803 }
13804 Ok(())
13805 });
13806 }
13807 if meta.path.is_ident("skip") {
13808 out.skip_ser = true;
13809 out.skip_de = true;
13810 return Ok(());
13811 }
13812 if meta.path.is_ident("skip_serializing") {
13813 out.skip_ser = true;
13814 return Ok(());
13815 }
13816 if meta.path.is_ident("skip_deserializing") {
13817 out.skip_de = true;
13818 return Ok(());
13819 }
13820 if meta.path.is_ident("skip_serializing_if") {
13821 let s: LitStr = meta.value()?.parse()?;
13822 out.skip_ser_if = Some(s.parse()?);
13823 return Ok(());
13824 }
13825 skip_serde_meta_value(&meta)
13826 })?;
13827 }
13828 Ok(out)
13829}
13830
13831fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
13832 let mut out = SerializerFieldAttrs::default();
13833 for attr in &field.attrs {
13834 if !attr.path().is_ident("serializer") {
13835 continue;
13836 }
13837 attr.parse_nested_meta(|meta| {
13838 if meta.path.is_ident("read_only") {
13839 out.read_only = true;
13840 return Ok(());
13841 }
13842 if meta.path.is_ident("write_only") {
13843 out.write_only = true;
13844 return Ok(());
13845 }
13846 if meta.path.is_ident("skip") {
13847 out.skip = true;
13848 return Ok(());
13849 }
13850 if meta.path.is_ident("source") {
13851 let s: LitStr = meta.value()?.parse()?;
13852 out.source = Some(s.value());
13853 return Ok(());
13854 }
13855 if meta.path.is_ident("method") {
13856 let s: LitStr = meta.value()?.parse()?;
13857 out.method = Some(s.value());
13858 return Ok(());
13859 }
13860 if meta.path.is_ident("validate") {
13861 let s: LitStr = meta.value()?.parse()?;
13862 out.validate = Some(s.value());
13863 return Ok(());
13864 }
13865 if meta.path.is_ident("many") {
13866 let _eq: syn::Token![=] = meta.input.parse()?;
13867 out.many = Some(meta.input.parse()?);
13868 return Ok(());
13869 }
13870 if meta.path.is_ident("nested") {
13871 out.nested = true;
13872 // Optional strict flag inside parentheses:
13873 // #[serializer(nested(strict))]
13874 if meta.input.peek(syn::token::Paren) {
13875 meta.parse_nested_meta(|inner| {
13876 if inner.path.is_ident("strict") {
13877 out.nested_strict = true;
13878 return Ok(());
13879 }
13880 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
13881 })?;
13882 }
13883 return Ok(());
13884 }
13885 if meta.path.is_ident("slug") {
13886 let s: LitStr = meta.value()?.parse()?;
13887 out.slug = Some(s.value());
13888 return Ok(());
13889 }
13890 if meta.path.is_ident("max_length") {
13891 let lit: syn::LitInt = meta.value()?.parse()?;
13892 out.max_length = Some(lit.base10_parse::<u64>()?);
13893 return Ok(());
13894 }
13895 if meta.path.is_ident("min_length") {
13896 let lit: syn::LitInt = meta.value()?.parse()?;
13897 out.min_length = Some(lit.base10_parse::<u64>()?);
13898 return Ok(());
13899 }
13900 if meta.path.is_ident("min") {
13901 let lit: syn::LitInt = meta.value()?.parse()?;
13902 out.min = Some(lit.base10_parse::<i64>()?);
13903 return Ok(());
13904 }
13905 if meta.path.is_ident("max") {
13906 let lit: syn::LitInt = meta.value()?.parse()?;
13907 out.max = Some(lit.base10_parse::<i64>()?);
13908 return Ok(());
13909 }
13910 Err(meta.error(
13911 "unknown serializer field attribute (supported: \
13912 `read_only`, `write_only`, `source`, `skip`, `method`, \
13913 `validate`, `nested`, `many`, `slug`, `max_length`, \
13914 `min_length`, `min`, `max`)",
13915 ))
13916 })?;
13917 }
13918 // Validate: read_only + write_only is nonsensical
13919 if out.read_only && out.write_only {
13920 return Err(syn::Error::new_spanned(
13921 field,
13922 "a field cannot be both `read_only` and `write_only`",
13923 ));
13924 }
13925 if out.method.is_some() && out.source.is_some() {
13926 return Err(syn::Error::new_spanned(
13927 field,
13928 "`method` and `source` are mutually exclusive — `method` computes \
13929 the value from a method, `source` reads it from a different model field",
13930 ));
13931 }
13932 if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
13933 return Err(syn::Error::new_spanned(
13934 field,
13935 "`slug` is mutually exclusive with `method`, `nested`, and `many` \
13936 — pick one strategy for populating the field",
13937 ));
13938 }
13939 Ok(out)
13940}
13941
13942fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
13943 let root = rustango_root();
13944 let struct_name = &input.ident;
13945 let struct_name_lit = struct_name.to_string();
13946
13947 let Data::Struct(data) = &input.data else {
13948 return Err(syn::Error::new_spanned(
13949 struct_name,
13950 "Serializer can only be derived on structs",
13951 ));
13952 };
13953 let Fields::Named(named) = &data.fields else {
13954 return Err(syn::Error::new_spanned(
13955 struct_name,
13956 "Serializer requires a struct with named fields",
13957 ));
13958 };
13959
13960 let container = parse_serializer_container_attrs(input)?;
13961 let model_path = &container.model;
13962
13963 // Classify each field. `ty` is only consumed by the
13964 // `#[cfg(feature = "openapi")]` block below, but we always
13965 // capture it to keep the field-info build a single pass.
13966 #[allow(dead_code)]
13967 struct FieldInfo {
13968 ident: syn::Ident,
13969 ty: syn::Type,
13970 attrs: SerializerFieldAttrs,
13971 serde: SerdeWireAttrs,
13972 /// JSON key in the output / in a request body.
13973 ser_name: String,
13974 de_name: String,
13975 }
13976 let (ser_rule, de_rule) = parse_serde_container_rules(input)?;
13977 let mut fields_info: Vec<FieldInfo> = Vec::new();
13978 for field in &named.named {
13979 let ident = field.ident.clone().expect("named field has ident");
13980 let attrs = parse_serializer_field_attrs(field)?;
13981 let serde = parse_serde_field_attrs(field)?;
13982 let name = ident_name(&ident);
13983 let wire = |rename: &Option<String>, rule: Option<SerdeRenameRule>| {
13984 rename
13985 .clone()
13986 .unwrap_or_else(|| rule.map_or_else(|| name.clone(), |r| r.apply(&name)))
13987 };
13988 fields_info.push(FieldInfo {
13989 ser_name: wire(&serde.rename_ser, ser_rule),
13990 de_name: wire(&serde.rename_de, de_rule),
13991 ident,
13992 ty: field.ty.clone(),
13993 attrs,
13994 serde,
13995 });
13996 }
13997
13998 // Generate from_model body: struct literal with each field assigned.
13999 let from_model_fields = fields_info.iter().map(|fi| {
14000 let ident = &fi.ident;
14001 if let Some(_inner) = &fi.attrs.many {
14002 // Many — collection field. Initialize empty; caller
14003 // populates via the macro-emitted set_<field> helper
14004 // after fetching the M2M children.
14005 quote! { #ident: ::std::vec::Vec::new() }
14006 } else if let Some(method) = &fi.attrs.method {
14007 // SerializerMethodField: call Self::<method>(&model) to
14008 // compute the value. Method signature must be
14009 // `fn <method>(model: &T) -> <field type>`.
14010 let method_ident = syn::Ident::new(method, ident.span());
14011 quote! { #ident: Self::#method_ident(model) }
14012 } else if let Some(slug_field) = &fi.attrs.slug {
14013 // v0.44 — SlugRelatedField. Source defaults to the field
14014 // name on this struct; override via `source = "..."`. The
14015 // source field on the model is expected to be a
14016 // `ForeignKey<T>`; the slug field on the parent is named
14017 // by the attribute value. When the FK is unloaded the
14018 // field falls back to `Default::default()` — same
14019 // graceful-degrade contract as `nested`; a NULL
14020 // `Option<ForeignKey>` renders `None`.
14021 let src_name = fi
14022 .attrs
14023 .source
14024 .as_deref()
14025 .unwrap_or(&ident_name(&fi.ident))
14026 .to_owned();
14027 let src_ident = ident_from_name(&src_name, ident.span());
14028 let slug_ident = syn::Ident::new(slug_field, ident.span());
14029 quote! {
14030 #ident: #root::serializer::LoadedRelation::render(
14031 &model.#src_ident,
14032 |__loaded| ::core::clone::Clone::clone(&__loaded.#slug_ident),
14033 ::core::default::Default::default,
14034 )
14035 }
14036 } else if fi.attrs.nested {
14037 // Nested serializer. Source defaults to the field name on
14038 // this struct; override via `source = "..."`. The source
14039 // field on the model is a `ForeignKey<T>` or
14040 // `Option<ForeignKey<T>>`, read through `LoadedRelation`.
14041 //
14042 // Behavior matrix (tweakable per-field):
14043 // * FK loaded → nested object materializes via
14044 // ChildSerializer::from_model(parent).
14045 // * FK unloaded → fall back to ChildSerializer::default()
14046 // (so prod doesn't crash on a missing
14047 // prefetch — just renders a blank nested
14048 // object). Add `#[serializer(nested(
14049 // strict))]` to keep the v0.18.1
14050 // panic-on-unloaded behavior for tests
14051 // that want hard guardrails.
14052 // * NULL `Option<ForeignKey>` → `None` in an `Option<Child>`
14053 // field (JSON null), strict or not.
14054 let src_name = fi.attrs.source.as_deref().unwrap_or(&ident_name(&fi.ident)).to_owned();
14055 let src_ident = ident_from_name(&src_name, ident.span());
14056 let unloaded = if fi.attrs.nested_strict {
14057 let panic_msg = format!(
14058 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
14059 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
14060 );
14061 quote! { || ::core::panic!("{}", #panic_msg) }
14062 } else {
14063 quote! { ::core::default::Default::default }
14064 };
14065 quote! {
14066 #ident: #root::serializer::LoadedRelation::render(
14067 &model.#src_ident,
14068 |__loaded| #root::serializer::ModelSerializer::from_model(__loaded),
14069 #unloaded,
14070 )
14071 }
14072 } else if fi.attrs.write_only || fi.attrs.skip {
14073 // Not read from model — use default
14074 quote! { #ident: ::core::default::Default::default() }
14075 } else if let Some(src) = &fi.attrs.source {
14076 let src_ident = ident_from_name(src, ident.span());
14077 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
14078 } else {
14079 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
14080 }
14081 });
14082
14083 // is_writable predicate (also used by writable_lits /
14084 // writable_source_lits below).
14085 let is_writable = |fi: &&FieldInfo| {
14086 !fi.attrs.read_only
14087 && !fi.attrs.skip
14088 && !fi.serde.skip_de
14089 && fi.attrs.method.is_none()
14090 && !fi.attrs.nested
14091 && fi.attrs.many.is_none()
14092 && fi.attrs.slug.is_none()
14093 };
14094
14095 // Declarative field constraints: one block
14096 // per writable field, run inside `validate()`. Each resolves its
14097 // bounds as the serializer attr when given (`#[serializer(max_length
14098 // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
14099 // `max` / `choices`), then dispatches on the field's JSON value via
14100 // `forms::validators::check_value` (string → length/choices, integer
14101 // → min/max).
14102 let opt_usize = |v: Option<u64>| match v {
14103 Some(n) => {
14104 let n = n as usize;
14105 quote!(::core::option::Option::Some(#n))
14106 }
14107 None => quote!(::core::option::Option::None),
14108 };
14109 #[cfg(feature = "openapi")]
14110 let opt_u64 = |v: Option<u64>| match v {
14111 Some(n) => quote!(::core::option::Option::Some(#n)),
14112 None => quote!(::core::option::Option::None),
14113 };
14114 let opt_i64 = |v: Option<i64>| match v {
14115 Some(n) => quote!(::core::option::Option::Some(#n)),
14116 None => quote!(::core::option::Option::None),
14117 };
14118 let constraint_blocks: Vec<_> = fields_info
14119 .iter()
14120 .filter(is_writable)
14121 .map(|fi| {
14122 let ident = &fi.ident;
14123 let fname = &fi.de_name;
14124 let mname = fi
14125 .attrs
14126 .source
14127 .clone()
14128 .unwrap_or_else(|| ident_name(&fi.ident));
14129 let attr_max_len = opt_usize(fi.attrs.max_length);
14130 let attr_min_len = opt_usize(fi.attrs.min_length);
14131 let attr_min = opt_i64(fi.attrs.min);
14132 let attr_max = opt_i64(fi.attrs.max);
14133 // A bound the field's type cannot take is a compile error, not a no-op.
14134 let ty = &fi.ty;
14135 let len_check = (fi.attrs.max_length.is_some() || fi.attrs.min_length.is_some())
14136 .then(|| quote! { #root::forms::validators::assert_length_bounded::<#ty>(); });
14137 let num_check = (fi.attrs.min.is_some() || fi.attrs.max.is_some())
14138 .then(|| quote! { #root::forms::validators::assert_number_bounded::<#ty>(); });
14139 quote! {
14140 #len_check
14141 #num_check
14142 if __sent(#fname) {
14143 let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
14144 let __max_length: ::core::option::Option<usize> = #attr_max_len
14145 .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
14146 let __min_length: ::core::option::Option<usize> = #attr_min_len;
14147 let __min: ::core::option::Option<i64> =
14148 #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
14149 let __max: ::core::option::Option<i64> =
14150 #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
14151 let __choices = __sf.and_then(|__f| __f.choices);
14152 let __v = #root::__serde_json::to_value(&self.#ident)
14153 .unwrap_or(#root::__serde_json::Value::Null);
14154 #root::forms::validators::check_value(
14155 #fname, &__v, __max_length, __min_length, __min, __max, __choices,
14156 &mut __errors,
14157 );
14158 }
14159 }
14160 })
14161 .collect();
14162 let has_constraints = !constraint_blocks.is_empty();
14163
14164 // Per-field validators. Emit a
14165 // `validate(&self)` method that runs each user-defined validator
14166 // and aggregates errors into `FormErrors`.
14167 let validator_calls: Vec<_> = fields_info
14168 .iter()
14169 .filter_map(|fi| {
14170 let ident = &fi.ident;
14171 let name_lit = &fi.de_name;
14172 let method = fi.attrs.validate.as_ref()?;
14173 let method_ident = syn::Ident::new(method, ident.span());
14174 Some(quote! {
14175 if __sent(#name_lit) {
14176 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
14177 __errors.add(#name_lit.to_owned(), __e);
14178 }
14179 }
14180 })
14181 })
14182 .collect();
14183 // #436 — cross-field `validate(&self)`. If the
14184 // container declared `#[serializer(validate = "fn_name")]`,
14185 // the macro-generated `validate(&self)` runs every per-field
14186 // validator first, then calls the user's cross-field method,
14187 // merging its `FormErrors` into the per-field errors. Either
14188 // alone is enough to emit the wrapper.
14189 let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
14190 quote! {
14191 // Merge cross-field errors into the per-field bucket so
14192 // a single .validate() call surfaces both layers.
14193 if let ::core::result::Result::Err(__cross) = self.#method_ident() {
14194 __errors.merge(__cross);
14195 }
14196 }
14197 });
14198 let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
14199 // Anything to run? Declarative constraints (for any writable field) +
14200 // per-field validators + cross-field hook.
14201 let has_run_validations = has_validators || has_constraints;
14202 // Shared body: declarative field constraints first (length / range /
14203 // choices, serializer-attr-or-model), then per-field validators, then
14204 // the cross-field hook. Field rules run only where `__sent(name)` (#1995).
14205 let checks = quote! {
14206 let mut __errors = #root::forms::FormErrors::default();
14207 #( #constraint_blocks )*
14208 #( #validator_calls )*
14209 #cross_validate_call
14210 if __errors.is_empty() {
14211 ::core::result::Result::Ok(())
14212 } else {
14213 ::core::result::Result::Err(__errors)
14214 }
14215 };
14216 let validate_body = quote! {
14217 let __sent = |_: &str| true;
14218 #checks
14219 };
14220 // PATCH: only the keys the body carries.
14221 let validate_patch_body = quote! {
14222 let __sent = |__n: &str| __obj.is_some_and(|__o| __o.contains_key(__n));
14223 #checks
14224 };
14225 // Inherent `validate(&self)` — kept for back-compat with direct
14226 // `serializer.validate()` calls that don't import `ModelSerializer`.
14227 // Emitted only when the serializer declares per-field/cross-field
14228 // validators (unchanged rule) so it never collides with a
14229 // hand-written inherent `validate`.
14230 let validate_method = if has_validators {
14231 quote! {
14232 impl #struct_name {
14233 /// Run the declarative field constraints, every
14234 /// `#[serializer(validate = "...")]` per-field validator,
14235 /// and (when declared) the container-level cross-field
14236 /// validator. Aggregates errors into `FormErrors` keyed by
14237 /// the field name. Returns `Ok(())` when all pass.
14238 pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
14239 #validate_body
14240 }
14241 }
14242 }
14243 } else {
14244 quote! {}
14245 };
14246 // Trait-method override so the type-erased ViewSet write path can
14247 // dispatch `ModelSerializer::validate` generically. Emitted whenever
14248 // there's anything to run (declarative constraints inherited from the
14249 // model count); otherwise the trait's default no-op is used.
14250 let trait_validate_override = if has_run_validations {
14251 quote! {
14252 fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
14253 #validate_body
14254 }
14255 }
14256 } else {
14257 quote! {}
14258 };
14259
14260 // For every `#[serializer(many = S)]` field, emit a
14261 // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
14262 // helper that maps the parents through `S::from_model`.
14263 let many_setters: Vec<_> = fields_info
14264 .iter()
14265 .filter_map(|fi| {
14266 let many_ty = fi.attrs.many.as_ref()?;
14267 let ident = &fi.ident;
14268 let setter = syn::Ident::new(&format!("set_{}", ident_name(ident)), ident.span());
14269 Some(quote! {
14270 /// Populate this `many` field by mapping each parent model
14271 /// through the inner serializer's `from_model`. Call after
14272 /// fetching the M2M / one-to-many children since
14273 /// `from_model` itself can't await an SQL query.
14274 pub fn #setter(
14275 &mut self,
14276 models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
14277 ) -> &mut Self {
14278 self.#ident = models.iter()
14279 .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
14280 .collect();
14281 self
14282 }
14283 })
14284 })
14285 .collect();
14286 let many_setters_impl = if many_setters.is_empty() {
14287 quote! {}
14288 } else {
14289 quote! {
14290 impl #struct_name {
14291 #( #many_setters )*
14292 }
14293 }
14294 };
14295
14296 // Generate custom Serialize: skip write_only and serde-skipped fields
14297 let output_fields: Vec<_> = fields_info
14298 .iter()
14299 .filter(|fi| !fi.attrs.write_only && !fi.serde.skip_ser)
14300 .collect();
14301 // `skip_serializing_if` fields count only when rendered.
14302 let output_field_count = output_fields.iter().map(|fi| {
14303 let ident = &fi.ident;
14304 if let Some(pred) = &fi.serde.skip_ser_if {
14305 quote! { + if #pred(&self.#ident) { 0 } else { 1 } }
14306 } else {
14307 quote! { + 1 }
14308 }
14309 });
14310 let output_field_count = quote! { 0usize #( #output_field_count )* };
14311 let serialize_fields = output_fields.iter().map(|fi| {
14312 let ident = &fi.ident;
14313 let name_lit = &fi.ser_name;
14314 if let Some(pred) = &fi.serde.skip_ser_if {
14315 quote! {
14316 if #pred(&self.#ident) {
14317 __state.skip_field(#name_lit)?;
14318 } else {
14319 __state.serialize_field(#name_lit, &self.#ident)?;
14320 }
14321 }
14322 } else {
14323 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
14324 }
14325 });
14326
14327 // writable_fields: normal + write_only.
14328 // Exclude:
14329 // - `read_only` — server-computed.
14330 // - `skip` — caller sets manually post-from_model.
14331 // - `method` — computed from a Self::fn(&model) call; accepting
14332 // it on write is meaningless.
14333 // - `nested` / `many` — populated from related-model data, not
14334 // from a field on the wire body.
14335 // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
14336 // `many` in `writable_fields()`, which made the ViewSet write
14337 // path accept those fields from the JSON body and try to bind
14338 // them to the SQL UPDATE — a silent no-op at best, a type
14339 // mismatch at worst.
14340 // (`is_writable` is defined above, near the constraint codegen.)
14341 let writable_lits: Vec<_> = fields_info
14342 .iter()
14343 .filter(is_writable)
14344 .map(|fi| fi.de_name.clone())
14345 .collect();
14346
14347 // `writable_source_fields`: the MODEL field names of writable
14348 // serializer fields (source-resolved). The ViewSet write path skips
14349 // every model column NOT in this set, so `read_only` / `method` /
14350 // computed fields a client posts are ignored instead of written.
14351 let writable_source_lits: Vec<String> = fields_info
14352 .iter()
14353 .filter(is_writable)
14354 .map(|fi| {
14355 fi.attrs
14356 .source
14357 .clone()
14358 .unwrap_or_else(|| ident_name(&fi.ident))
14359 })
14360 .collect();
14361
14362 // `readable_source_fields`: model columns the JSON output copies
14363 // straight from the model, so `?ordering=` cannot sort on a column
14364 // the API never shows (#1845).
14365 let readable: Vec<&FieldInfo> = fields_info
14366 .iter()
14367 .filter(|fi| {
14368 !fi.attrs.write_only
14369 && !fi.serde.skip_ser
14370 && !fi.attrs.skip
14371 && fi.attrs.method.is_none()
14372 && !fi.attrs.nested
14373 && fi.attrs.many.is_none()
14374 && fi.attrs.slug.is_none()
14375 })
14376 .collect();
14377 let readable_source_lits: Vec<String> = readable
14378 .iter()
14379 .map(|fi| {
14380 fi.attrs
14381 .source
14382 .clone()
14383 .unwrap_or_else(|| ident_name(&fi.ident))
14384 })
14385 .collect();
14386 // Their JSON keys, so the ViewSet finds a renamed cursor column.
14387 let readable_lits: Vec<String> = readable.iter().map(|fi| fi.ser_name.clone()).collect();
14388
14389 // `from_writable_json`: build a partial instance for input
14390 // validation. Writable fields are parsed from the JSON body (keyed
14391 // by serializer field name); every other field defaults. Per-field
14392 // type errors collect into `FormErrors` keyed by the field name.
14393 // Construction is field-by-field (not `Self::default()`) so it works
14394 // for serializers regardless of a struct-level `Default`.
14395 let from_writable_json_inits: Vec<_> = fields_info
14396 .iter()
14397 .map(|fi| {
14398 let ident = &fi.ident;
14399 let fname = &fi.de_name;
14400 let ty = &fi.ty;
14401 if is_writable(&fi) {
14402 quote! {
14403 #ident: match __obj.and_then(|__o| __o.get(#fname)) {
14404 ::core::option::Option::Some(__v) => {
14405 match #root::__serde_json::from_value::<#ty>(
14406 ::core::clone::Clone::clone(__v),
14407 ) {
14408 ::core::result::Result::Ok(__x) => __x,
14409 ::core::result::Result::Err(__e) => {
14410 __errors.add(#fname.to_owned(), __e.to_string());
14411 ::core::default::Default::default()
14412 }
14413 }
14414 }
14415 ::core::option::Option::None => ::core::default::Default::default(),
14416 }
14417 }
14418 } else {
14419 quote! { #ident: ::core::default::Default::default() }
14420 }
14421 })
14422 .collect();
14423 // `validate_patch`: overlay each sent writable field on the stored row.
14424 let patch_overlays: Vec<_> = fields_info
14425 .iter()
14426 .filter(is_writable)
14427 .map(|fi| {
14428 let ident = &fi.ident;
14429 let fname = &fi.de_name;
14430 let ty = &fi.ty;
14431 quote! {
14432 if let ::core::option::Option::Some(__v) = __obj.and_then(|__o| __o.get(#fname)) {
14433 match #root::__serde_json::from_value::<#ty>(::core::clone::Clone::clone(__v)) {
14434 ::core::result::Result::Ok(__x) => __merged.#ident = __x,
14435 ::core::result::Result::Err(__e) => {
14436 __errors.add(#fname.to_owned(), __e.to_string());
14437 }
14438 }
14439 }
14440 }
14441 })
14442 .collect();
14443 let patch_validate_call = if has_run_validations {
14444 quote! { __merged.__rustango_validate_patch(__obj) }
14445 } else {
14446 quote! { let _ = &__merged; ::core::result::Result::Ok(()) }
14447 };
14448 let patch_validate_helper = if has_run_validations {
14449 quote! {
14450 impl #struct_name {
14451 #[doc(hidden)]
14452 fn __rustango_validate_patch(
14453 &self,
14454 __obj: ::core::option::Option<&#root::__serde_json::Map<::std::string::String, #root::__serde_json::Value>>,
14455 ) -> ::core::result::Result<(), #root::forms::FormErrors> {
14456 #validate_patch_body
14457 }
14458 }
14459 }
14460 } else {
14461 quote! {}
14462 };
14463
14464 // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
14465 // Only includes fields shown in JSON output (skips write_only). For each
14466 // `Option<T>` field, omit from `required` and add `.nullable()`.
14467 let openapi_impl = {
14468 #[cfg(feature = "openapi")]
14469 {
14470 let property_calls = output_fields.iter().map(|fi| {
14471 let name_lit = &fi.ser_name;
14472 let ty = &fi.ty;
14473 let nullable_call = if is_option(ty) {
14474 quote! { .nullable() }
14475 } else {
14476 quote! {}
14477 };
14478 // The bounds `validate()` enforces on a written field.
14479 let limits_call = is_writable(fi).then(|| {
14480 let mname = fi
14481 .attrs
14482 .source
14483 .clone()
14484 .unwrap_or_else(|| ident_name(&fi.ident));
14485 let (a, b, c, d) = (
14486 opt_u64(fi.attrs.max_length),
14487 opt_u64(fi.attrs.min_length),
14488 opt_i64(fi.attrs.min),
14489 opt_i64(fi.attrs.max),
14490 );
14491 quote! {
14492 .field_limits(
14493 <#model_path as #root::core::Model>::SCHEMA.field(#mname),
14494 #a, #b, #c, #d,
14495 )
14496 }
14497 });
14498 quote! {
14499 .property(
14500 #name_lit,
14501 <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
14502 #limits_call
14503 #nullable_call,
14504 )
14505 }
14506 });
14507 let required_lits: Vec<_> = output_fields
14508 .iter()
14509 .filter(|fi| !is_option(&fi.ty) && fi.serde.skip_ser_if.is_none())
14510 .map(|fi| fi.ser_name.clone())
14511 .collect();
14512 quote! {
14513 impl #root::openapi::OpenApiSchema for #struct_name {
14514 fn openapi_schema() -> #root::openapi::Schema {
14515 #root::openapi::Schema::object()
14516 #( #property_calls )*
14517 .required([ #( #required_lits ),* ])
14518 }
14519 }
14520 }
14521 }
14522 #[cfg(not(feature = "openapi"))]
14523 {
14524 quote! {}
14525 }
14526 };
14527
14528 Ok(quote! {
14529 impl #root::serializer::ModelSerializer for #struct_name {
14530 type Model = #model_path;
14531
14532 fn from_model(model: &Self::Model) -> Self {
14533 Self {
14534 #( #from_model_fields ),*
14535 }
14536 }
14537
14538 fn writable_fields() -> &'static [&'static str] {
14539 &[ #( #writable_lits ),* ]
14540 }
14541
14542 fn writable_source_fields() -> &'static [&'static str] {
14543 &[ #( #writable_source_lits ),* ]
14544 }
14545
14546 fn readable_source_fields() -> &'static [&'static str] {
14547 &[ #( #readable_source_lits ),* ]
14548 }
14549
14550 fn readable_fields() -> &'static [&'static str] {
14551 &[ #( #readable_lits ),* ]
14552 }
14553
14554 fn from_writable_json(
14555 __body: &#root::__serde_json::Value,
14556 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
14557 let mut __errors = #root::forms::FormErrors::default();
14558 let __obj = __body.as_object();
14559 let __out = Self {
14560 #( #from_writable_json_inits ),*
14561 };
14562 if __errors.is_empty() {
14563 ::core::result::Result::Ok(__out)
14564 } else {
14565 ::core::result::Result::Err(__errors)
14566 }
14567 }
14568
14569 fn validate_patch(
14570 __stored: &Self::Model,
14571 __body: &#root::__serde_json::Value,
14572 ) -> ::core::result::Result<(), #root::forms::FormErrors> {
14573 let mut __errors = #root::forms::FormErrors::default();
14574 let __obj = __body.as_object();
14575 #[allow(unused_mut)]
14576 let mut __merged = <Self as #root::serializer::ModelSerializer>::from_model(__stored);
14577 #( #patch_overlays )*
14578 if !__errors.is_empty() {
14579 return ::core::result::Result::Err(__errors);
14580 }
14581 #patch_validate_call
14582 }
14583
14584 #trait_validate_override
14585 }
14586
14587 #patch_validate_helper
14588
14589 impl #root::__serde::Serialize for #struct_name {
14590 fn serialize<S>(&self, serializer: S)
14591 -> ::core::result::Result<S::Ok, S::Error>
14592 where
14593 S: #root::__serde::Serializer,
14594 {
14595 use #root::__serde::ser::SerializeStruct;
14596 let mut __state = serializer.serialize_struct(
14597 #struct_name_lit,
14598 #output_field_count,
14599 )?;
14600 #( #serialize_fields )*
14601 __state.end()
14602 }
14603 }
14604
14605 #openapi_impl
14606
14607 #validate_method
14608
14609 #many_setters_impl
14610 })
14611}
14612
14613/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
14614/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
14615/// when the feature is off.
14616#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
14617fn is_option(ty: &syn::Type) -> bool {
14618 if let syn::Type::Path(p) = ty {
14619 if let Some(last) = p.path.segments.last() {
14620 return last.ident == "Option";
14621 }
14622 }
14623 false
14624}
14625
14626#[cfg(test)]
14627mod main_attr_tests {
14628 use super::*;
14629
14630 fn expand(args: &str) -> String {
14631 let body = quote! { async fn main() { run().await } };
14632 expand_main(args.parse().unwrap(), body)
14633 .expect("expansion failed")
14634 .to_string()
14635 }
14636
14637 /// Asserts on the expansion rather than on `parse_logging`,
14638 /// because the defect was a subscriber reaching the output, not
14639 /// an argument being misread (#1465).
14640 #[test]
14641 fn logging_false_emits_no_subscriber() {
14642 let out = expand("logging = false");
14643 assert!(
14644 !out.contains("logging :: Setup"),
14645 "`logging = false` must not install a subscriber:\n{out}",
14646 );
14647 assert!(
14648 out.contains("block_on"),
14649 "the runtime must still be built:\n{out}",
14650 );
14651 }
14652
14653 #[test]
14654 fn default_still_installs_a_subscriber() {
14655 // The control. Without it, an `expand_main` that dropped the
14656 // prologue unconditionally would pass the test above while
14657 // leaving every default project with no logging at all.
14658 let out = expand("");
14659 assert!(
14660 out.contains("logging :: Setup"),
14661 "the default entrypoint must still install one:\n{out}",
14662 );
14663 // `./app serve` or `./app --port 8080` must keep logging to stdout:
14664 // only `Cli`, which knows its verbs, moves logs to stderr.
14665 assert!(
14666 !out.contains("args ()"),
14667 "the entrypoint must not route logs by argv:\n{out}",
14668 );
14669 }
14670
14671 #[test]
14672 fn logging_false_composes_with_flavor() {
14673 let out = expand("flavor = \"current_thread\", logging = false");
14674 assert!(!out.contains("logging :: Setup"), "{out}");
14675 assert!(out.contains("new_current_thread"), "{out}");
14676 }
14677
14678 #[test]
14679 fn logging_true_is_the_default_shape() {
14680 // `logging = true` is accepted and means the default, so the
14681 // match must not fire on the substring `logging`.
14682 assert!(parse_logging(&"logging = true".parse().unwrap()));
14683 assert!(!parse_logging(&"logging=false".parse().unwrap()));
14684 }
14685}
14686
14687#[cfg(test)]
14688mod main_arg_warning_tests {
14689 use super::*;
14690
14691 fn expand(args: &str) -> String {
14692 let body = quote! { async fn main() { run().await } };
14693 expand_main(args.parse().unwrap(), body)
14694 .expect("expansion failed")
14695 .to_string()
14696 }
14697
14698 /// A typo or unknown key used to be dropped without a word.
14699 #[test]
14700 fn unknown_or_misspelt_args_warn() {
14701 for args in ["logging = flase", "multi_thread", "workers = 4"] {
14702 let out = expand(args);
14703 assert!(out.contains("unknown argument"), "{args}:\n{out}");
14704 }
14705 }
14706
14707 #[test]
14708 fn supported_args_do_not_warn() {
14709 for args in [
14710 "",
14711 r#"flavor = "current_thread""#,
14712 r#"flavor = "multi_thread", logging = false"#,
14713 "logging = true",
14714 "worker_threads = 4, start_paused = true",
14715 r#"crate = "tokio", unhandled_panic = "shutdown_runtime""#,
14716 r#"flavor = "curent_thread""#,
14717 ] {
14718 assert!(!expand(args).contains("RUSTANGO_WARNING"), "{args}");
14719 }
14720 }
14721}
14722
14723#[cfg(test)]
14724mod embed_migrations_tests {
14725 use super::*;
14726
14727 fn expand(path: &str) -> String {
14728 expand_embed_migrations(quote! { #path })
14729 .expect("expansion failed")
14730 .to_string()
14731 }
14732
14733 #[test]
14734 fn missing_dir_warns() {
14735 let out = expand("./no_such_migrations_dir");
14736 assert!(out.contains("deprecated"), "missing dir must warn:\n{out}");
14737 assert!(out.contains("no_such_migrations_dir"), "{out}");
14738 }
14739
14740 #[test]
14741 fn existing_dir_does_not_warn() {
14742 let out = expand("./src");
14743 assert!(!out.contains("deprecated"), "{out}");
14744 }
14745}
14746
14747#[cfg(test)]
14748mod field_attr_tests {
14749 use super::*;
14750
14751 fn error_for(field: syn::Field) -> Option<String> {
14752 let attrs = parse_field_attrs(&field).unwrap();
14753 let kind = detect_type(&field.ty).unwrap().kind;
14754 check_bound_compatibility(&field, &attrs, kind)
14755 .err()
14756 .map(|e| e.to_string())
14757 }
14758
14759 /// `citext`, `vector` and `geometry` on the wrong type are errors, not no-ops (#1937).
14760 #[test]
14761 fn type_specific_attrs_reject_other_types() {
14762 let f: syn::FieldsNamed = syn::parse_quote!({
14763 #[rustango(citext)] a: i64,
14764 #[rustango(vector(dims = 3))] b: String,
14765 #[rustango(geometry(srid = 4326))] c: String,
14766 #[rustango(citext)] d: String,
14767 });
14768 let errs: Vec<_> = f.named.into_iter().map(error_for).collect();
14769 assert!(errs[0].as_deref().unwrap().contains("citext"), "{errs:?}");
14770 assert!(errs[1].as_deref().unwrap().contains("vector"), "{errs:?}");
14771 assert!(errs[2].as_deref().unwrap().contains("geometry"), "{errs:?}");
14772 assert_eq!(errs[3], None);
14773 }
14774
14775 /// `manager(ext)` emitted a trait nobody could add methods to; it is refused (#2132).
14776 #[test]
14777 fn manager_ext_is_refused() {
14778 let input: DeriveInput = syn::parse_quote! {
14779 #[rustango(table = "p", manager(ext = "PostManagerExt"))]
14780 struct Post { id: i64 }
14781 };
14782 let err = parse_container_attrs(&input).err().expect("refused");
14783 assert!(err.to_string().contains("#2132"), "{err}");
14784 }
14785
14786 /// `set_null` on a NOT NULL `ForeignKey<T>` would fail every parent delete.
14787 #[test]
14788 fn set_null_needs_an_optional_foreign_key() {
14789 let input: DeriveInput = syn::parse_quote! {
14790 #[rustango(table = "c")]
14791 struct Child {
14792 #[rustango(primary_key)] id: i64,
14793 #[rustango(on_delete = "set_null")] parent: ForeignKey<Parent>,
14794 }
14795 };
14796 let err = expand(&input).err().expect("refused");
14797 assert!(err.to_string().contains("set_null"), "{err}");
14798 let input: DeriveInput = syn::parse_quote! {
14799 #[rustango(table = "c")]
14800 struct Child {
14801 #[rustango(primary_key)] id: i64,
14802 #[rustango(on_delete = "set_null")] parent: Option<ForeignKey<Parent>>,
14803 }
14804 };
14805 assert!(expand(&input).is_ok());
14806 }
14807
14808 /// The same for `fk = "…"` / `o2o = "…"` on a plain column.
14809 #[test]
14810 fn set_null_needs_a_nullable_fk_column() {
14811 let input: DeriveInput = syn::parse_quote! {
14812 #[rustango(table = "c")]
14813 struct Child {
14814 #[rustango(primary_key)] id: i64,
14815 #[rustango(fk = "p", on_delete = "set_null")] parent_id: i64,
14816 #[rustango(o2o = "q", on_delete = "set_null")] other_id: Option<i64>,
14817 }
14818 };
14819 let err = expand(&input).err().expect("refused");
14820 assert!(err.to_string().contains("set_null"), "{err}");
14821 let input: DeriveInput = syn::parse_quote! {
14822 #[rustango(table = "c")]
14823 struct Child {
14824 #[rustango(primary_key)] id: i64,
14825 #[rustango(fk = "p", on_delete = "set_null")] parent_id: std::option::Option<i64>,
14826 #[rustango(o2o = "q", on_delete = "set_null")] other_id: ::core::option::Option<i64>,
14827 #[rustango(on_delete = "set_null")] parent: core::option::Option<ForeignKey<Parent>>,
14828 }
14829 };
14830 assert!(expand(&input).is_ok());
14831 }
14832
14833 /// An index or `get_latest_by` naming no column warns; it does not fail.
14834 #[test]
14835 fn unknown_index_columns_warn() {
14836 let input: DeriveInput = syn::parse_quote! {
14837 #[rustango(
14838 table = "w",
14839 unique_together = "a, nope",
14840 index_together = "a, b_typo",
14841 index("missing"),
14842 get_latest_by = "-when_typo"
14843 )]
14844 struct W { #[rustango(primary_key)] id: i64, a: i64, b: i64 }
14845 };
14846 let out = expand(&input).expect("still compiles").to_string();
14847 for name in ["nope", "b_typo", "missing", "when_typo"] {
14848 assert!(out.contains(&format!("`{name}`")), "{name}:\n{out}");
14849 }
14850 }
14851
14852 /// Real columns, field names for `get_latest_by` and expression
14853 /// indexes emit nothing new.
14854 #[test]
14855 fn known_index_columns_do_not_warn() {
14856 let input: DeriveInput = syn::parse_quote! {
14857 #[rustango(
14858 table = "w",
14859 unique_together = "a, b_col",
14860 index("lower(a)"),
14861 get_latest_by = "-b"
14862 )]
14863 struct W {
14864 #[rustango(primary_key)] id: i64,
14865 a: i64,
14866 #[rustango(column = "b_col")] b: i64,
14867 }
14868 };
14869 let out = expand(&input).expect("compiles").to_string();
14870 assert!(!out.contains("RUSTANGO_WARNING"), "{out}");
14871 }
14872
14873 /// Two FKs to one parent would emit the same reverse accessor twice.
14874 #[test]
14875 fn reverse_accessor_clash_names_related_name() {
14876 let input: DeriveInput = syn::parse_quote! {
14877 #[rustango(table = "m")]
14878 struct Message {
14879 #[rustango(primary_key)] id: i64,
14880 sender: ForeignKey<User>,
14881 recipient: ForeignKey<User>,
14882 }
14883 };
14884 let err = expand(&input).err().expect("refused");
14885 assert!(err.to_string().contains("related_name"), "{err}");
14886 }
14887
14888 /// `related_name = "+"` emits no reverse accessor, so a parent from
14889 /// another crate, which cannot take an inherent impl, still derives.
14890 #[test]
14891 fn related_name_plus_skips_the_reverse_accessor() {
14892 let input: DeriveInput = syn::parse_quote! {
14893 #[rustango(table = "m")]
14894 struct Message {
14895 #[rustango(primary_key)] id: i64,
14896 sender: ForeignKey<User>,
14897 #[rustango(related_name = "+")] recipient: ForeignKey<User>,
14898 #[rustango(related_name = "+")] owner: ForeignKey<other::Account>,
14899 }
14900 };
14901 let out = expand(&input).expect("derives").to_string();
14902 assert_eq!(out.matches("message_set_pool").count(), 1, "{out}");
14903 assert!(!out.contains("impl other :: Account {"), "{out}");
14904 }
14905
14906 fn field_error(src: &str) -> Option<String> {
14907 let f: syn::FieldsNamed = syn::parse_str(&format!("{{ {src} }}")).unwrap();
14908 let field = f.named.into_iter().next().unwrap();
14909 match parse_field_attrs(&field) {
14910 Err(e) => Some(e.to_string()),
14911 Ok(_) => error_for(field),
14912 }
14913 }
14914
14915 /// Flag attrs take `= true` like `blank`; `= false` is refused by name.
14916 #[test]
14917 fn flag_attrs_accept_eq_true() {
14918 for key in [
14919 "primary_key",
14920 "unique",
14921 "soft_delete",
14922 "index",
14923 "auto_now",
14924 "auto_now_add",
14925 ] {
14926 let f: syn::FieldsNamed =
14927 syn::parse_str(&format!("{{ #[rustango({key} = true)] a: i64 }}")).unwrap();
14928 let field = f.named.into_iter().next().unwrap();
14929 assert!(parse_field_attrs(&field).is_ok(), "{key} = true");
14930 let err = field_error(&format!("#[rustango({key} = false)] a: i64")).unwrap();
14931 assert!(err.contains(&format!("`{key} = false`")), "{err}");
14932 }
14933 let f: syn::Field =
14934 syn::parse_quote!(#[rustango(primary_key = true, unique = true)] a: i64);
14935 let attrs = parse_field_attrs(&f).unwrap();
14936 assert!(attrs.primary_key && attrs.unique);
14937 }
14938
14939 /// An unknown key is named, the nearest key suggested and the list shown.
14940 #[test]
14941 fn unknown_key_is_named_with_a_suggestion() {
14942 let err = field_error("#[rustango(max_lenght = 5)] a: String").unwrap();
14943 assert!(err.contains("`max_lenght`"), "{err}");
14944 assert!(err.contains("did you mean `max_length`"), "{err}");
14945 assert!(err.contains("Supported: column"), "{err}");
14946 let input: DeriveInput = syn::parse_quote! {
14947 #[rustango(tabel = "p")]
14948 struct P { id: i64 }
14949 };
14950 let err = parse_container_attrs(&input).err().unwrap().to_string();
14951 assert!(
14952 err.contains("`tabel`") && err.contains("did you mean `table`"),
14953 "{err}"
14954 );
14955 let err = field_error("#[rustango(zzzzzz)] a: i64").unwrap();
14956 assert!(!err.contains("did you mean"), "{err}");
14957 }
14958
14959 /// The listed keys are the parsed keys; a stale list would mislead.
14960 #[test]
14961 fn listed_keys_are_all_known() {
14962 for key in FIELD_ATTR_KEYS {
14963 let err = field_error(&format!("#[rustango({key})] a: i64")).unwrap_or_default();
14964 assert!(!err.contains("unknown rustango field"), "{key}: {err}");
14965 }
14966 for key in CONTAINER_ATTR_KEYS {
14967 let input: DeriveInput =
14968 syn::parse_str(&format!("#[rustango({key})] struct P {{ id: i64 }}")).unwrap();
14969 let err = parse_container_attrs(&input)
14970 .err()
14971 .map(|e| e.to_string())
14972 .unwrap_or_default();
14973 assert!(!err.contains("unknown rustango container"), "{key}: {err}");
14974 }
14975 }
14976
14977 /// `default` is raw SQL: a bare word or a non-number on a numeric field is refused.
14978 #[test]
14979 fn default_shapes_only_sqlite_takes_are_refused() {
14980 for bad in [
14981 r#"#[rustango(default = "draft")] s: String"#,
14982 r#"#[rustango(default = "draft")] s: Option<String>"#,
14983 r#"#[rustango(default = "abc")] n: i32"#,
14984 r#"#[rustango(default = "'abc'")] n: i64"#,
14985 r#"#[rustango(default = "true")] n: i32"#,
14986 r#"#[rustango(default = "")] n: i32"#,
14987 r#"#[rustango(default = "'x'")] n: f64"#,
14988 ] {
14989 assert!(field_error(bad).is_some(), "{bad} must be refused");
14990 }
14991 let err = field_error(r#"#[rustango(default = "draft")] s: String"#).unwrap();
14992 assert!(err.contains(r#"default = "'draft'""#), "{err}");
14993 // Valid on Postgres alone, so the message says that, not "a column".
14994 let err = field_error(r#"#[rustango(default = "current_user")] s: String"#).unwrap();
14995 assert!(
14996 err.contains("Postgres only") && !err.contains("column"),
14997 "{err}"
14998 );
14999 for good in [
15000 r#"#[rustango(default = "'draft'")] s: String"#,
15001 r#"#[rustango(default = "")] s: String"#,
15002 r#"#[rustango(default = "now()")] s: String"#,
15003 r#"#[rustango(default = "CURRENT_TIMESTAMP")] s: String"#,
15004 r#"#[rustango(default = "NULL")] s: Option<String>"#,
15005 r#"#[rustango(default = "0")] n: i32"#,
15006 r#"#[rustango(default = "-1")] n: i64"#,
15007 r#"#[rustango(default = "'5'")] n: i64"#,
15008 r#"#[rustango(default = "(1 + 2)")] n: i64"#,
15009 r#"#[rustango(default = "1.5")] n: f64"#,
15010 r#"#[rustango(default = "true")] b: bool"#,
15011 ] {
15012 assert_eq!(field_error(good), None, "{good}");
15013 }
15014 }
15015
15016 /// A raw ident derives names without `r#`; this used to panic (#1937).
15017 #[test]
15018 fn raw_idents_derive_plain_names() {
15019 let ident: syn::Ident = syn::parse_quote!(r#type);
15020 assert_eq!(column_type_ident(&ident).to_string(), "type_col");
15021 assert_eq!(ident_from_name("type", ident.span()).to_string(), "r#type");
15022 }
15023}
15024
15025#[cfg(test)]
15026mod model_attr_name_tests {
15027 use super::*;
15028
15029 fn container_error(item: proc_macro2::TokenStream) -> Option<String> {
15030 let input: DeriveInput = syn::parse2(item).unwrap();
15031 parse_container_attrs(&input).err().map(|e| e.to_string())
15032 }
15033
15034 fn field_error(field: proc_macro2::TokenStream) -> Option<String> {
15035 let f: syn::FieldsNamed = syn::parse2(quote!({ #field })).unwrap();
15036 let field = f.named.into_iter().next().unwrap();
15037 parse_field_attrs(&field).err().map(|e| e.to_string())
15038 }
15039
15040 /// `#[rgo(...)]` reads exactly as `#[rustango(...)]`.
15041 #[test]
15042 fn rgo_is_read_like_rustango() {
15043 let long: DeriveInput = syn::parse_quote! {
15044 #[rustango(table = "t", display = "name")] struct S;
15045 };
15046 let short: DeriveInput = syn::parse_quote! {
15047 #[rgo(table = "t", display = "name")] struct S;
15048 };
15049 let (long, short) = (
15050 parse_container_attrs(&long).unwrap(),
15051 parse_container_attrs(&short).unwrap(),
15052 );
15053 assert_eq!(long.table, short.table);
15054 assert_eq!(short.table.as_deref(), Some("t"));
15055 assert_eq!(long.display.map(|d| d.0), short.display.map(|d| d.0));
15056 }
15057
15058 #[test]
15059 fn mixing_names_on_one_item_or_field_is_refused() {
15060 let err = container_error(quote! {
15061 #[rustango(table = "t")] #[rgo(display = "name")] struct S;
15062 })
15063 .expect("a mix on an item must fail");
15064 assert!(
15065 err.contains("`#[rustango(...)]` and `#[rgo(...)]`"),
15066 "{err}"
15067 );
15068 let err = field_error(quote! { #[rgo(primary_key)] #[rustango(column = "c")] a: i64 })
15069 .expect("a mix on a field must fail");
15070 assert!(
15071 err.contains("`#[rgo(...)]` and `#[rustango(...)]`"),
15072 "{err}"
15073 );
15074 // Either name repeated alone is fine.
15075 assert_eq!(
15076 container_error(quote! { #[rgo(table = "t")] #[rgo(display = "name")] struct S; }),
15077 None
15078 );
15079 assert_eq!(
15080 field_error(quote! { #[rustango(primary_key)] #[rustango(column = "c")] a: i64 }),
15081 None
15082 );
15083 }
15084
15085 /// A hint quotes the attribute under the name the user wrote.
15086 #[test]
15087 fn hints_use_the_written_name() {
15088 let err = container_error(quote! { #[rgo(table = "t", unique_together = "a")] struct S; })
15089 .expect("one column must fail");
15090 assert!(err.contains("#[rgo(unique)]"), "{err}");
15091 let err =
15092 container_error(quote! { #[rustango(table = "t", index_together = "a")] struct S; })
15093 .expect("one column must fail");
15094 assert!(err.contains("#[rustango(index)]"), "{err}");
15095 }
15096}