rustango_macros/lib.rs
1//! Proc-macros for rustango.
2//!
3//! v0.1 ships `#[derive(Model)]`, which emits:
4//! * a `Model` impl carrying a static `ModelSchema`,
5//! * an `inventory::submit!` so the model is discoverable from the registry,
6//! * an inherent `objects()` returning a `QuerySet<Self>`,
7//! * a `sqlx::FromRow` impl so query results decode into the struct.
8
9use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::quote;
12use syn::{
13 parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields, GenericArgument, LitStr,
14 PathArguments, Type, TypePath,
15};
16
17/// Resolve the consumer's local name for the `rustango` crate so the
18/// macro-emitted code keeps compiling when a downstream `Cargo.toml`
19/// renames the dep (`[dependencies] orm = { package = "rustango", … }`)
20/// or when the standalone `rustango-orm` crate ships in a future
21/// slice of the [orm-extract epic](https://github.com/ujeenet/rustango/issues/149).
22///
23/// Returns one of:
24/// - `quote!(::rustango)` — the consumer IS the `rustango` crate
25/// itself. We emit the absolute path `::rustango` (NOT `crate`)
26/// because examples and integration tests inside the rustango
27/// package compile as separate binaries — their own `crate::`
28/// namespace is the example/test file, not rustango's lib root.
29/// The absolute `::rustango` resolves correctly in both contexts
30/// (rustango lib code AND rustango/examples/*.rs).
31/// - `quote!(::ident)` — the consumer renamed the dep; emit the
32/// user's chosen ident.
33/// - `quote!(::rustango)` — fallback when `proc-macro-crate` can't
34/// read the manifest (rare; preserves today's behavior).
35///
36/// Issue [#142](https://github.com/ujeenet/rustango/issues/142).
37fn rustango_root() -> TokenStream2 {
38 use proc_macro_crate::{crate_name, FoundCrate};
39 match crate_name("rustango") {
40 Ok(FoundCrate::Itself) => quote!(::rustango),
41 Ok(FoundCrate::Name(name)) => {
42 let ident = proc_macro2::Ident::new(&name, proc_macro2::Span::call_site());
43 quote!(::#ident)
44 }
45 Err(_) => quote!(::rustango),
46 }
47}
48
49/// Derive a `Model` impl. See crate docs for the supported attributes.
50#[proc_macro_derive(Model, attributes(rustango))]
51pub fn derive_model(input: TokenStream) -> TokenStream {
52 let input = parse_macro_input!(input as DeriveInput);
53 expand(&input)
54 .unwrap_or_else(syn::Error::into_compile_error)
55 .into()
56}
57
58/// Derive a `router(prefix, pool) -> axum::Router` associated method on a
59/// marker struct, wiring the full CRUD ViewSet in one annotation.
60///
61/// ```ignore
62/// #[derive(ViewSet)]
63/// #[viewset(
64/// model = Post,
65/// fields = "id, title, body, author_id",
66/// filter_fields = "author_id",
67/// search_fields = "title, body",
68/// ordering = "-published_at",
69/// page_size = 20,
70/// )]
71/// pub struct PostViewSet;
72///
73/// // Mount into your app:
74/// let app = Router::new()
75/// .merge(PostViewSet::router("/api/posts", pool.clone()));
76/// ```
77///
78/// Attributes:
79/// * `model = TypeName` — *required*. The `#[derive(Model)]` struct whose
80/// `SCHEMA` constant drives the endpoints.
81/// * `fields = "a, b, c"` — scalar fields included in list/retrieve JSON
82/// and accepted on create/update (default: all scalar fields).
83/// * `filter_fields = "a, b"` — fields filterable via `?a=v` query params.
84/// * `search_fields = "a, b"` — fields searched by `?search=...`.
85/// * `ordering = "a, -b"` — default list ordering; prefix `-` for DESC.
86/// * `page_size = N` — default page size (default: 20, max: 1000).
87/// * `read_only` — flag; wires only `list` + `retrieve` (no mutations).
88/// * `serializer = SomeSerializer` — render list / retrieve / create
89/// responses through a `#[derive(Serializer)]` type instead of the
90/// default field-level projection (`read_only` / `source` / `method`
91/// / `write_only` overrides apply). Tri-dialect. Requires the
92/// `serializer` feature.
93/// * `permissions(list = "...", retrieve = "...", create = "...",
94/// update = "...", destroy = "...")` — codenames required per action.
95#[proc_macro_derive(ViewSet, attributes(viewset))]
96pub fn derive_viewset(input: TokenStream) -> TokenStream {
97 let input = parse_macro_input!(input as DeriveInput);
98 expand_viewset(&input)
99 .unwrap_or_else(syn::Error::into_compile_error)
100 .into()
101}
102
103/// Derive `rustango::forms::Form` (slice 8.4B). Generates a
104/// `parse(&HashMap<String, String>) -> Result<Self, FormErrors>` impl
105/// that walks every named field and:
106///
107/// * Parses the string value into the field's Rust type (`String`,
108/// `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
109/// nullable case).
110/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
111/// `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
112/// validators in declaration order, returning `FormError::Parse`
113/// on the first failure.
114///
115/// Example:
116///
117/// ```ignore
118/// #[derive(Form)]
119/// pub struct CreateItemForm {
120/// #[form(min_length = 1, max_length = 64)]
121/// pub name: String,
122/// #[form(min = 0, max = 150)]
123/// pub age: i32,
124/// pub active: bool,
125/// pub email: Option<String>,
126/// }
127///
128/// let parsed = CreateItemForm::parse(&form_map)?;
129/// ```
130#[proc_macro_derive(Form, attributes(form))]
131pub fn derive_form(input: TokenStream) -> TokenStream {
132 let input = parse_macro_input!(input as DeriveInput);
133 expand_form(&input)
134 .unwrap_or_else(syn::Error::into_compile_error)
135 .into()
136}
137
138/// Derive `rustango::serializer::ModelSerializer` for a struct.
139/// (intra-doc link disabled — the macro crate doesn't depend on
140/// `rustango` itself, so rustdoc can't resolve the path.)
141///
142/// # Container attribute (required)
143/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
144///
145/// # Field attributes
146/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
147/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
148/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
149/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
150/// - `#[serializer(method = "fn_name")]` — DRF `SerializerMethodField`: calls `Self::fn_name(&model)` for the field value; excluded from `writable_fields()`
151/// - `#[serializer(nested)]` / `nested(strict)` — auto-resolves nested serializer from a loaded `ForeignKey`; excluded from `writable_fields()`
152/// - `#[serializer(many = ChildSerializer)]` — collection of nested serializers; populated via macro-emitted `set_<field>(&[Child::Model])`; excluded from `writable_fields()`
153/// - `#[serializer(slug = "name")]` — DRF `SlugRelatedField`: clones `model.<source>.value()?.name`; excluded from `writable_fields()` (v0.44)
154/// - `#[serializer(validate = "fn_name")]` — per-field validator surfaced by `Self::validate(&self)`
155/// - `#[serializer(max_length = N)]` / `min_length` / `min` / `max` — declarative bounds checked on
156/// write; auto-inherit from the model's `FieldSchema` (`max_length`/`min`/`max`/`choices`) when
157/// no attr is given, override it when given (`min_length` is serializer-only)
158///
159/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
160#[proc_macro_derive(Serializer, attributes(serializer))]
161pub fn derive_serializer(input: TokenStream) -> TokenStream {
162 let input = parse_macro_input!(input as DeriveInput);
163 expand_serializer(&input)
164 .unwrap_or_else(syn::Error::into_compile_error)
165 .into()
166}
167
168/// Bake every `*.json` migration file in a directory into the binary
169/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
170/// of `(name, json_content)` pairs, lex-sorted by file stem.
171///
172/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
173/// behaviour as `migrate(pool, dir)` but with no filesystem access.
174/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
175/// (i.e. the crate that invokes the macro). Default is
176/// `"./migrations"` if no argument is supplied.
177///
178/// ```ignore
179/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
180/// // or:
181/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
182///
183/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
184/// ```
185///
186/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
187/// file's `name` field must equal its file stem, every `prev`
188/// reference must point to another migration in the same directory,
189/// and the JSON must parse. A broken chain — orphan `prev`, missing
190/// predecessor, malformed file — fails at macro-expansion time with
191/// a clear `compile_error!`. *No other Django-shape Rust framework
192/// validates migration chains at compile time*: Cot's migrations are
193/// imperative Rust code (no static chain), Loco's are SeaORM
194/// up/down (same), Rwf's are raw SQL (no chain at all).
195///
196/// Each migration is included via `include_str!` so cargo's rebuild
197/// detection picks up file *content* changes. **Caveat:** cargo
198/// doesn't watch directory listings, so adding or removing a
199/// migration file inside the dir won't auto-trigger a rebuild — run
200/// `cargo clean` (or just bump any other source file) when you add
201/// new migrations during embedded development.
202#[proc_macro]
203pub fn embed_migrations(input: TokenStream) -> TokenStream {
204 expand_embed_migrations(input.into())
205 .unwrap_or_else(syn::Error::into_compile_error)
206 .into()
207}
208
209/// `Q!()` — Django-shape filter syntax compile-time-resolved against
210/// typed columns. Issue #269 / T1.7.
211///
212/// Each invocation lowers to the equivalent typed-column method call:
213///
214/// ```ignore
215/// // These expand identically:
216/// Q!(User.email__icontains = "alice")
217/// User::email.ilike("%alice%")
218/// ```
219///
220/// Field-name typos fail the build (the macro emits `User::no_such_field`
221/// which doesn't exist) — the headline ergonomic win of this slice over
222/// Django's stringly-typed `__lookup` filters.
223///
224/// # Supported lookup suffixes
225///
226/// * bare `=` / `__exact` → `.eq(value)`
227/// * `__iexact` → `.ilike(value)` (case-insensitive equality, no wildcards)
228/// * `__ne` → `.ne(value)`
229/// * `__gt` / `__gte` / `__lt` / `__lte` → corresponding comparison
230/// * `__contains` / `__icontains` → `.like("%v%")` / `.ilike("%v%")`
231/// * `__startswith` / `__istartswith` → `.like("v%")` / `.ilike("v%")`
232/// * `__endswith` / `__iendswith` → `.like("%v")` / `.ilike("%v")`
233/// * `__in` → `.is_in(iterable)`
234/// * `__not_in` → `.not_in(iterable)`
235/// * `__isnull = true` → `.is_null()`; `__isnull = false` → `.is_not_null()`
236/// * `__between` accepts a tuple literal `(lo, hi)` → `.between(lo, hi)`
237/// * `__regex` / `__iregex` → `.regex(pattern)` / `.iregex(pattern)`
238///
239/// Unknown suffixes fail the build with a `compile_error!` pointing at
240/// the lookup token.
241///
242/// # Combine
243///
244/// Each `Q!()` returns a `TypedFilter<Model>` — chain via the existing
245/// `.and()` / `.or()` / `.not()` methods:
246///
247/// ```ignore
248/// User::objects()
249/// .where_(
250/// Q!(User.active = true)
251/// .and(Q!(User.email__icontains = "alice"))
252/// )
253/// .fetch(&pool).await?;
254/// ```
255///
256/// All emitted code routes through existing per-dialect writers — no new
257/// SQL emission machinery. Tri-dialect support is inherent.
258#[allow(non_snake_case)]
259#[proc_macro]
260pub fn Q(input: TokenStream) -> TokenStream {
261 expand_q(input.into())
262 .unwrap_or_else(syn::Error::into_compile_error)
263 .into()
264}
265
266/// `#[rustango::main]` — the Django-shape runserver entrypoint. Wraps
267/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
268/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
269/// functions are zero-boilerplate:
270///
271/// ```ignore
272/// #[rustango::main]
273/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
274/// rustango::server::Builder::from_env().await?
275/// .migrate("migrations").await?
276/// .api(my_app::urls::api())
277/// .seed_with(my_app::seed::run).await?
278/// .serve("0.0.0.0:8080").await
279/// }
280/// ```
281///
282/// Optional `flavor = "current_thread"` passes through to
283/// `#[tokio::main]`; default is the multi-threaded runtime.
284///
285/// Pulls `tracing-subscriber` into the rustango crate behind the
286/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
287/// out get plain `#[tokio::main]` ergonomics without the dependency.
288#[proc_macro_attribute]
289pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
290 expand_main(args.into(), item.into())
291 .unwrap_or_else(syn::Error::into_compile_error)
292 .into()
293}
294
295fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
296 let mut input: syn::ItemFn = syn::parse2(item)?;
297 if input.sig.asyncness.is_none() {
298 return Err(syn::Error::new(
299 input.sig.ident.span(),
300 "`#[rustango::main]` must wrap an `async fn`",
301 ));
302 }
303
304 // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
305 // to `#[tokio::main]`. Tokio's proc-macro internally emits
306 // `::tokio::*` paths that resolve against the user crate's deps,
307 // so calling it through the rustango re-export still requires the
308 // user to add tokio to their own Cargo.toml. Building the
309 // runtime ourselves keeps the dep transitive through the
310 // `runtime` feature on rustango.
311 //
312 // Parse optional `flavor = "current_thread"` / `flavor =
313 // "multi_thread"` from the attribute args. Unknown args are
314 // tolerated (forward-compat with tokio's own arg surface).
315 let root = rustango_root();
316 let flavor = parse_flavor(&args);
317 let builder_call = match flavor {
318 Flavor::CurrentThread => quote! {
319 #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
320 },
321 Flavor::MultiThread => quote! {
322 #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
323 },
324 };
325
326 // Detach the user body and rewrite `main` as a sync fn that
327 // builds the runtime and blocks on the async body.
328 let user_body = input.block.clone();
329 input.sig.asyncness = None;
330 input.block = syn::parse2(quote! {{
331 {
332 use #root::__private_runtime::tracing_subscriber::{self, EnvFilter};
333 // Colour only when stdout is a terminal, and never under
334 // `NO_COLOR` — `Color::Auto`'s rule, called rather than
335 // copied.
336 //
337 // This was inlined at first, on a comment claiming
338 // `rustango::logging` is gated on `admin` + `tenancy`. It is
339 // not: `pub mod logging` is ungated and its contents are
340 // `runtime`-gated — the same feature that gates this macro
341 // (`pub use rustango_macros::main` is `#[cfg(feature =
342 // "runtime")]`). So wherever this expands, `Color` is
343 // reachable, and the copy was justified by a gate that does
344 // not exist. `should_colour` carries a deliberate
345 // `NO_COLOR`-set-but-empty subtlety that the inline happened
346 // to match today and nothing kept matching tomorrow.
347 //
348 // Without any of it the subscriber inherits
349 // tracing-subscriber's default, `cfg!(feature = "ansi")` —
350 // true since the framework turned that feature on — so
351 // `./app > app.log` and every container redirecting stdout
352 // collected escape codes. `Setup::install` has its own
353 // check; this is the default entrypoint and had none.
354 let __ansi = #root::logging::Color::Auto.should_colour();
355 // `try_init` so duplicate installers (e.g. tests already
356 // holding a subscriber) don't panic.
357 let _ = tracing_subscriber::fmt()
358 .with_env_filter(
359 EnvFilter::try_from_default_env()
360 .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
361 )
362 .with_ansi(__ansi)
363 .try_init();
364 }
365 let __rt = #builder_call
366 .enable_all()
367 .build()
368 .expect("failed to build tokio runtime");
369 __rt.block_on(async move #user_body)
370 }})?;
371
372 Ok(quote! {
373 #input
374 })
375}
376
377enum Flavor {
378 MultiThread,
379 CurrentThread,
380}
381
382fn parse_flavor(args: &TokenStream2) -> Flavor {
383 // Cheap parser: look for the literal token sequence
384 // `flavor = "current_thread"`. Everything else (including
385 // bare `multi_thread` or no args) defaults to multi-thread.
386 let s = args.to_string();
387 if s.contains("current_thread") {
388 Flavor::CurrentThread
389 } else {
390 Flavor::MultiThread
391 }
392}
393
394/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
395struct QInput {
396 base_path: syn::Path,
397 field: syn::Ident,
398 value: syn::Expr,
399}
400
401impl syn::parse::Parse for QInput {
402 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
403 let base_path: syn::Path = input.parse()?;
404 input.parse::<syn::Token![.]>()?;
405 let field: syn::Ident = input.parse()?;
406 input.parse::<syn::Token![=]>()?;
407 let value: syn::Expr = input.parse()?;
408 Ok(QInput {
409 base_path,
410 field,
411 value,
412 })
413 }
414}
415
416fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
417 let q: QInput = syn::parse2(input)?;
418 let root = rustango_root();
419 let field_str = q.field.to_string();
420 let field_span = q.field.span();
421 let (base, suffix) = match field_str.find("__") {
422 Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
423 None => (field_str.as_str(), ""),
424 };
425 if base.is_empty() {
426 return Err(syn::Error::new(
427 field_span,
428 "Q!(): field name is empty before `__` suffix",
429 ));
430 }
431 let base_ident = syn::Ident::new(base, field_span);
432 let value = &q.value;
433 let path = &q.base_path;
434
435 // Most suffixes map directly to a Column method with the value
436 // forwarded unchanged. Some need value-shape massaging (wildcards
437 // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
438 // ISNULL). Unknown suffixes fail the build.
439 let expanded = match suffix {
440 "" | "exact" => quote! {
441 #root::core::Column::eq(#path::#base_ident, #value)
442 },
443 "ne" => quote! {
444 #root::core::Column::ne(#path::#base_ident, #value)
445 },
446 "gt" => quote! {
447 #root::core::Column::gt(#path::#base_ident, #value)
448 },
449 "gte" => quote! {
450 #root::core::Column::gte(#path::#base_ident, #value)
451 },
452 "lt" => quote! {
453 #root::core::Column::lt(#path::#base_ident, #value)
454 },
455 "lte" => quote! {
456 #root::core::Column::lte(#path::#base_ident, #value)
457 },
458 // The escaped-LIKE lookups delegate to the typed Column
459 // methods, which escape the value and pair it with the
460 // *Escaped ops so `%` / `_` match literally on every dialect
461 // (#1257) — the macro no longer owns any pattern wrapping.
462 "iexact" => quote! {
463 #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
464 },
465 "contains" => quote! {
466 #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
467 },
468 "icontains" => quote! {
469 #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
470 },
471 "startswith" => quote! {
472 #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
473 },
474 "istartswith" => quote! {
475 #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
476 },
477 "endswith" => quote! {
478 #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
479 },
480 "iendswith" => quote! {
481 #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
482 },
483 "in" => quote! {
484 #root::core::Column::is_in(#path::#base_ident, #value)
485 },
486 "not_in" => quote! {
487 #root::core::Column::not_in(#path::#base_ident, #value)
488 },
489 "isnull" => {
490 // Must be a bool literal at macro time so we can route to
491 // is_null vs is_not_null without a runtime branch.
492 let b = match value {
493 syn::Expr::Lit(syn::ExprLit {
494 lit: syn::Lit::Bool(b),
495 ..
496 }) => b.value(),
497 _ => {
498 return Err(syn::Error::new_spanned(
499 value,
500 "Q!(): `__isnull` requires a `true` or `false` literal",
501 ));
502 }
503 };
504 if b {
505 quote! { #root::core::Column::is_null(#path::#base_ident) }
506 } else {
507 quote! { #root::core::Column::is_not_null(#path::#base_ident) }
508 }
509 }
510 "between" => {
511 // Accept a tuple literal `(lo, hi)`.
512 let tuple = match value {
513 syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
514 _ => {
515 return Err(syn::Error::new_spanned(
516 value,
517 "Q!(): `__between` requires a tuple literal `(lo, hi)`",
518 ));
519 }
520 };
521 let lo = &tuple.elems[0];
522 let hi = &tuple.elems[1];
523 quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
524 }
525 "regex" => quote! {
526 #root::core::Column::regex(#path::#base_ident, #value)
527 },
528 "iregex" => quote! {
529 #root::core::Column::iregex(#path::#base_ident, #value)
530 },
531 _ => {
532 return Err(syn::Error::new(
533 field_span,
534 format!(
535 "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",
536 suffix
537 ),
538 ));
539 }
540 };
541 Ok(expanded)
542}
543
544fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
545 // Default to "./migrations" if invoked without args.
546 let path_str = if input.is_empty() {
547 "./migrations".to_string()
548 } else {
549 let lit: LitStr = syn::parse2(input)?;
550 lit.value()
551 };
552
553 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
554 syn::Error::new(
555 proc_macro2::Span::call_site(),
556 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
557 )
558 })?;
559 let abs = std::path::Path::new(&manifest).join(&path_str);
560
561 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
562 if abs.is_dir() {
563 let read = std::fs::read_dir(&abs).map_err(|e| {
564 syn::Error::new(
565 proc_macro2::Span::call_site(),
566 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
567 )
568 })?;
569 for entry in read.flatten() {
570 let path = entry.path();
571 if !path.is_file() {
572 continue;
573 }
574 if path.extension().and_then(|s| s.to_str()) != Some("json") {
575 continue;
576 }
577 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
578 continue;
579 };
580 entries.push((stem.to_owned(), path));
581 }
582 }
583 entries.sort_by(|a, b| a.0.cmp(&b.0));
584
585 // Compile-time chain validation: read each migration's JSON,
586 // pull `name` and `prev` (file-stem-keyed for the chain check),
587 // and verify every `prev` points to another migration in the
588 // slice. Mismatches between the file stem and the embedded
589 // `name` field — and broken `prev` chains — fail at MACRO
590 // EXPANSION time so a misshapen migration set never compiles.
591 //
592 // This is the v0.4 Slice 5 distinguisher: rustango's JSON
593 // migrations + a Rust proc-macro that reads them is the unique
594 // combo nothing else in the Django-shape Rust camp can match
595 // (Cot's are imperative Rust code, Loco's are SeaORM up/down,
596 // Rwf's are raw SQL — none have a static chain to validate).
597 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
598 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
599 for (stem, path) in &entries {
600 let raw = std::fs::read_to_string(path).map_err(|e| {
601 syn::Error::new(
602 proc_macro2::Span::call_site(),
603 format!(
604 "embed_migrations!: cannot read {} for chain validation: {e}",
605 path.display()
606 ),
607 )
608 })?;
609 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
610 syn::Error::new(
611 proc_macro2::Span::call_site(),
612 format!(
613 "embed_migrations!: {} is not valid JSON: {e}",
614 path.display()
615 ),
616 )
617 })?;
618 let name = json
619 .get("name")
620 .and_then(|v| v.as_str())
621 .ok_or_else(|| {
622 syn::Error::new(
623 proc_macro2::Span::call_site(),
624 format!(
625 "embed_migrations!: {} is missing the `name` field",
626 path.display()
627 ),
628 )
629 })?
630 .to_owned();
631 if name != *stem {
632 return Err(syn::Error::new(
633 proc_macro2::Span::call_site(),
634 format!(
635 "embed_migrations!: file stem `{stem}` does not match the migration's \
636 `name` field `{name}` — rename the file or fix the JSON",
637 ),
638 ));
639 }
640 let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
641 chain_names.push(name.clone());
642 prev_refs.push((name, prev));
643 }
644
645 let name_set: std::collections::HashSet<&str> =
646 chain_names.iter().map(String::as_str).collect();
647 for (name, prev) in &prev_refs {
648 if let Some(p) = prev {
649 if !name_set.contains(p.as_str()) {
650 return Err(syn::Error::new(
651 proc_macro2::Span::call_site(),
652 format!(
653 "embed_migrations!: broken migration chain — `{name}` declares \
654 prev=`{p}` but no migration with that name exists in {}",
655 abs.display()
656 ),
657 ));
658 }
659 }
660 }
661
662 let pairs: Vec<TokenStream2> = entries
663 .iter()
664 .map(|(name, path)| {
665 let path_lit = path.display().to_string();
666 quote! { (#name, ::core::include_str!(#path_lit)) }
667 })
668 .collect();
669
670 Ok(quote! {
671 {
672 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
673 __RUSTANGO_EMBEDDED
674 }
675 })
676}
677
678fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
679 let root = rustango_root();
680 let struct_name = &input.ident;
681
682 let Data::Struct(data) = &input.data else {
683 return Err(syn::Error::new_spanned(
684 struct_name,
685 "Model can only be derived on structs",
686 ));
687 };
688 let Fields::Named(named) = &data.fields else {
689 return Err(syn::Error::new_spanned(
690 struct_name,
691 "Model requires a struct with named fields",
692 ));
693 };
694
695 let container = parse_container_attrs(input)?;
696 let table = container
697 .table
698 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
699 let model_name = struct_name.to_string();
700
701 let collected = collect_fields(named, &table)?;
702
703 // Validate that #[rustango(display = "…")] names a real field.
704 if let Some((ref display, span)) = container.display {
705 if !collected.field_names.iter().any(|n| n == display) {
706 return Err(syn::Error::new(
707 span,
708 format!("`display = \"{display}\"` does not match any field on this struct"),
709 ));
710 }
711 }
712 let display = container.display.map(|(name, _)| name);
713 let app_label = container.app.clone();
714
715 // Validate admin field-name lists against declared field names.
716 // Note: `list_display` is intentionally NOT validated here. As of
717 // v0.32 it may also reference inventory-registered computed
718 // fields (via `register_admin_computed!`) whose existence the
719 // macro can't see at compile time — they're submitted from any
720 // crate that depends on rustango. The runtime list-view resolves
721 // unknown names against the inventory + silently drops the
722 // truly-bogus ones, which is the cheaper trade-off versus
723 // forcing a per-Model attr to opt out.
724 if let Some(admin) = &container.admin {
725 for (label, list) in [
726 ("search_fields", &admin.search_fields),
727 ("readonly_fields", &admin.readonly_fields),
728 ("list_filter", &admin.list_filter),
729 ] {
730 if let Some((names, span)) = list {
731 for name in names {
732 if !collected.field_names.iter().any(|n| n == name) {
733 return Err(syn::Error::new(
734 *span,
735 format!(
736 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
737 ),
738 ));
739 }
740 }
741 }
742 }
743 if let Some((pairs, span)) = &admin.ordering {
744 for (name, _) in pairs {
745 if !collected.field_names.iter().any(|n| n == name) {
746 return Err(syn::Error::new(
747 *span,
748 format!(
749 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
750 ),
751 ));
752 }
753 }
754 }
755 if let Some((groups, span)) = &admin.fieldsets {
756 for (_, fields) in groups {
757 for name in fields {
758 if !collected.field_names.iter().any(|n| n == name) {
759 return Err(syn::Error::new(
760 *span,
761 format!(
762 "`fieldsets`: \"{name}\" is not a declared field on this struct"
763 ),
764 ));
765 }
766 }
767 }
768 }
769 }
770 if let Some(audit) = &container.audit {
771 if let Some((names, span)) = &audit.track {
772 for name in names {
773 if !collected.field_names.iter().any(|n| n == name) {
774 return Err(syn::Error::new(
775 *span,
776 format!(
777 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
778 ),
779 ));
780 }
781 }
782 }
783 }
784
785 // Issue #291 / T2.5 — validate each `default_order` column name
786 // against the model's collected fields. Typos fail at macro-expand
787 // time, not at the database.
788 for (col, _desc, span) in &container.default_order {
789 if !collected.field_names.iter().any(|n| n == col) {
790 return Err(syn::Error::new(
791 *span,
792 format!(
793 "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
794 ),
795 ));
796 }
797 }
798
799 // Build the audit_track list for ModelSchema: None when no audit attr,
800 // Some(empty) when audit present without track, Some(names) when explicit.
801 let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
802 audit
803 .track
804 .as_ref()
805 .map(|(names, _)| names.clone())
806 .unwrap_or_default()
807 });
808
809 // Merge field-level indexes into the container's index list.
810 let mut all_indexes: Vec<IndexAttr> = container.indexes;
811 for field in &named.named {
812 let ident = field.ident.as_ref().expect("named");
813 let col = to_snake_case(&ident.to_string()); // column name fallback
814 // Re-parse field attrs to check for index flag
815 if let Ok(fa) = parse_field_attrs(field) {
816 if fa.index {
817 let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
818 let auto_name = if fa.index_unique {
819 format!("{table}_{col_name}_uq_idx")
820 } else {
821 format!("{table}_{col_name}_idx")
822 };
823 all_indexes.push(IndexAttr {
824 name: fa.index_name.or(Some(auto_name)),
825 columns: vec![col_name],
826 unique: fa.index_unique,
827 method: fa.index_method,
828 where_clause: None,
829 include: Vec::new(),
830 });
831 }
832 }
833 }
834
835 let model_impl = model_impl_tokens(
836 struct_name,
837 &model_name,
838 &table,
839 display.as_deref(),
840 app_label.as_deref(),
841 container.admin.as_ref(),
842 &container.default_order,
843 &collected.field_schemas,
844 collected.soft_delete_column.as_deref(),
845 container.permissions,
846 audit_track_names.as_deref(),
847 &container.m2m,
848 &all_indexes,
849 &container.checks,
850 &container.excludes,
851 &container.composite_fks,
852 &container.generic_fks,
853 container.scope.as_deref(),
854 container.is_view,
855 container.verbose_name.as_deref(),
856 container.verbose_name_plural.as_deref(),
857 container.managed,
858 container.base_manager_name.as_deref(),
859 container.order_with_respect_to.as_deref(),
860 container.proxy,
861 &container.required_db_features,
862 container.required_db_vendor.as_deref(),
863 container.default_related_name.as_deref(),
864 container.db_table_comment.as_deref(),
865 container
866 .get_latest_by
867 .as_ref()
868 .map(|(c, d)| (c.as_str(), *d)),
869 &container.extra_permissions,
870 &container.default_permissions,
871 &container.global_scopes,
872 &container.reverse_has_relations,
873 &container.generic_has_relations,
874 );
875 let module_ident = column_module_ident(struct_name);
876 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
877 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
878 let track_set: Option<std::collections::HashSet<&str>> = audit
879 .track
880 .as_ref()
881 .map(|(names, _)| names.iter().map(String::as_str).collect());
882 collected
883 .column_entries
884 .iter()
885 .filter(|c| {
886 track_set
887 .as_ref()
888 .map_or(true, |s| s.contains(c.name.as_str()))
889 })
890 .collect()
891 });
892 let inherent_impl = inherent_impl_tokens(
893 struct_name,
894 &collected,
895 collected.primary_key.as_ref(),
896 &column_consts,
897 audited_fields.as_deref(),
898 &all_indexes,
899 &container.manager_fns,
900 );
901 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
902 let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
903 let reverse_helpers = reverse_helper_tokens(
904 struct_name,
905 &collected.fk_relations,
906 container.default_related_name.as_deref(),
907 );
908 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
909 let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
910 // Issue #817 — `#[rustango(through(...))]` accessors.
911 let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
912 // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
913 let reverse_has_accessors =
914 reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
915 let generic_fk_accessors = generic_fk_accessor_tokens(
916 struct_name,
917 &container.generic_fks,
918 &collected.column_entries,
919 );
920
921 // Issue #271 / T1.9 — `#[rustango(manager(ext = "FooManagerExt"))]`
922 // emits an empty extension trait so users can add methods via
923 // `impl FooManagerExt for QuerySet<Foo>` without hand-writing the
924 // trait declaration. See `crates/rustango/src/manager.rs` for the
925 // pattern this replaces.
926 let manager_trait = container.manager_ext.as_ref().map(|name| {
927 let model_name_str = struct_name.to_string();
928 let doc = format!(
929 "Custom-Manager extension trait for [`{model_name_str}`]. \
930 Generated by `#[rustango(manager(ext = ...))]`. Add methods \
931 via `impl {name} for QuerySet<{model_name_str}> {{ ... }}`."
932 );
933 quote! {
934 #[doc = #doc]
935 pub trait #name: ::core::marker::Sized {}
936 }
937 });
938
939 Ok(quote! {
940 #model_impl
941 #inherent_impl
942 #from_row_impl
943 #column_module
944 #reverse_helpers
945 #m2m_accessors
946 #generic_m2m_accessors
947 #through_accessors
948 #reverse_has_accessors
949 #generic_fk_accessors
950 #manager_trait
951
952 #root::core::inventory::submit! {
953 #root::core::ModelEntry {
954 schema: <#struct_name as #root::core::Model>::SCHEMA,
955 // `module_path!()` evaluates at the registration site,
956 // so a Model declared in `crate::blog::models` records
957 // `"<crate>::blog::models"` and `resolved_app_label()`
958 // can infer "blog" without an explicit attribute.
959 module_path: ::core::module_path!(),
960 }
961 }
962 })
963}
964
965/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
966/// matches `field_name` against the model's FK fields and, for a
967/// match, decodes the FK target via the parent's macro-generated
968/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
969/// `ForeignKey::Loaded` on `self`.
970///
971/// Always emitted (with empty arms for FK-less models, which
972/// return `Ok(false)` for any field name) so the `T: LoadRelated`
973/// trait bound on `fetch_on` is universally satisfied — users
974/// never have to think about implementing it.
975fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
976 let root = rustango_root();
977 let arms = fk_relations.iter().map(|rel| {
978 let parent_ty = &rel.parent_type;
979 let fk_col = rel.fk_column.as_str();
980 // FK field's Rust ident matches its SQL column name in v0.8
981 // (no `column = "..."` rename ships on FK fields).
982 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
983 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
984 let assign = if rel.nullable {
985 quote! {
986 self.#field_ident = ::core::option::Option::Some(
987 #root::sql::ForeignKey::loaded(_pk, _parent),
988 );
989 }
990 } else {
991 quote! {
992 self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
993 }
994 };
995 quote! {
996 #fk_col => {
997 let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
998 // Audit #451 — multi-hop `select_related("a__b__c")`:
999 // stitch the deeper relation onto this parent first,
1000 // decoding it at the accumulated `__next_alias`. The
1001 // parent type also impls `LoadRelated`, so this recurses
1002 // the FK chain to arbitrary depth.
1003 if let ::core::option::Option::Some(__r) = __rest {
1004 let _ = #root::sql::LoadRelated::__rustango_load_related(
1005 &mut _parent, row, __r, &__next_alias,
1006 )?;
1007 }
1008 // Loud-in-debug, default-in-release: a divergence
1009 // between the FK field's declared `K` (drives the
1010 // expected `SqlValue::<Variant>`) and the parent's
1011 // `__rustango_pk_value` output is a macro-internal
1012 // invariant break — surfacing the panic in dev
1013 // catches it before users hit silent PK=0 corruption.
1014 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1015 #root::core::SqlValue::#variant_ident(v) => v,
1016 _other => {
1017 ::core::debug_assert!(
1018 false,
1019 "rustango macro bug: load_related on FK `{}` expected \
1020 SqlValue::{} from parent's __rustango_pk_value but got \
1021 {:?} — file a bug at https://github.com/ujeenet/rustango",
1022 #fk_col,
1023 ::core::stringify!(#variant_ident),
1024 _other,
1025 );
1026 #default_expr
1027 }
1028 };
1029 #assign
1030 ::core::result::Result::Ok(true)
1031 }
1032 }
1033 });
1034 quote! {
1035 #[cfg(feature = "postgres")]
1036 impl #root::sql::LoadRelated for #struct_name {
1037 #[allow(unused_variables)]
1038 fn __rustango_load_related(
1039 &mut self,
1040 row: &#root::sql::sqlx::postgres::PgRow,
1041 field_name: &str,
1042 alias: &str,
1043 ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1044 // Audit #451 — split the multi-hop path: `base` is the FK
1045 // on THIS model, `__rest` (if any) is the remaining chain
1046 // to stitch onto the loaded parent. `__next_alias` is the
1047 // accumulated join alias the parent's columns live under
1048 // (`{alias}__{next-hop}`), matching `lower_select_related`.
1049 let (__base, __rest): (&str, ::core::option::Option<&str>) =
1050 match field_name.split_once("__") {
1051 ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1052 ::core::option::Option::None => (field_name, ::core::option::Option::None),
1053 };
1054 let __next_alias: ::std::string::String = match __rest {
1055 ::core::option::Option::Some(__r) => {
1056 let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1057 ::std::format!("{}__{}", alias, __rb)
1058 }
1059 ::core::option::Option::None => ::std::string::String::new(),
1060 };
1061 match __base {
1062 #( #arms )*
1063 _ => ::core::result::Result::Ok(false),
1064 }
1065 }
1066 }
1067 }
1068}
1069
1070/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1071/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1072/// which expands to a `LoadRelatedMy` impl when rustango is built with
1073/// the `mysql` feature, and to nothing otherwise. The decoded parent
1074/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1075/// decoder, also batch8) so the dual emission is symmetric across
1076/// backends.
1077fn load_related_impl_my_tokens(
1078 struct_name: &syn::Ident,
1079 fk_relations: &[FkRelation],
1080) -> TokenStream2 {
1081 let root = rustango_root();
1082 let arms = fk_relations.iter().map(|rel| {
1083 let parent_ty = &rel.parent_type;
1084 let fk_col = rel.fk_column.as_str();
1085 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1086 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1087 let assign = if rel.nullable {
1088 quote! {
1089 __self.#field_ident = ::core::option::Option::Some(
1090 #root::sql::ForeignKey::loaded(_pk, _parent),
1091 );
1092 }
1093 } else {
1094 quote! {
1095 __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1096 }
1097 };
1098 // `self` IS hygiene-tracked through macro_rules — emitted from
1099 // a different context than the `&mut self` parameter inside
1100 // the macro_rules-expanded fn. Pass it through as `__self`
1101 // and let the macro_rules rebind it to the receiver.
1102 quote! {
1103 #fk_col => {
1104 let mut _parent: #parent_ty =
1105 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1106 // Audit #451 — multi-hop: stitch the deeper relation onto
1107 // the parent at the accumulated alias (see PG twin).
1108 if let ::core::option::Option::Some(__r) = __rest {
1109 let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1110 &mut _parent, row, __r, &__next_alias,
1111 )?;
1112 }
1113 // See note in `load_related_impl_tokens` (PG twin) —
1114 // the same loud-in-debug invariant guard.
1115 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1116 #root::core::SqlValue::#variant_ident(v) => v,
1117 _other => {
1118 ::core::debug_assert!(
1119 false,
1120 "rustango macro bug: load_related on FK `{}` expected \
1121 SqlValue::{} from parent's __rustango_pk_value but got \
1122 {:?} — file a bug at https://github.com/ujeenet/rustango",
1123 #fk_col,
1124 ::core::stringify!(#variant_ident),
1125 _other,
1126 );
1127 #default_expr
1128 }
1129 };
1130 #assign
1131 ::core::result::Result::Ok(true)
1132 }
1133 }
1134 });
1135 quote! {
1136 #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1137 #( #arms )*
1138 });
1139 }
1140}
1141
1142/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1143/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1144/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1145fn load_related_impl_sqlite_tokens(
1146 struct_name: &syn::Ident,
1147 fk_relations: &[FkRelation],
1148) -> TokenStream2 {
1149 let root = rustango_root();
1150 let arms = fk_relations.iter().map(|rel| {
1151 let parent_ty = &rel.parent_type;
1152 let fk_col = rel.fk_column.as_str();
1153 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1154 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1155 let assign = if rel.nullable {
1156 quote! {
1157 __self.#field_ident = ::core::option::Option::Some(
1158 #root::sql::ForeignKey::loaded(_pk, _parent),
1159 );
1160 }
1161 } else {
1162 quote! {
1163 __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1164 }
1165 };
1166 quote! {
1167 #fk_col => {
1168 let mut _parent: #parent_ty =
1169 <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1170 // Audit #451 — multi-hop: stitch the deeper relation onto
1171 // the parent at the accumulated alias (see PG twin).
1172 if let ::core::option::Option::Some(__r) = __rest {
1173 let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1174 &mut _parent, row, __r, &__next_alias,
1175 )?;
1176 }
1177 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1178 #root::core::SqlValue::#variant_ident(v) => v,
1179 _other => {
1180 ::core::debug_assert!(
1181 false,
1182 "rustango macro bug: load_related on FK `{}` expected \
1183 SqlValue::{} from parent's __rustango_pk_value but got \
1184 {:?} — file a bug at https://github.com/ujeenet/rustango",
1185 #fk_col,
1186 ::core::stringify!(#variant_ident),
1187 _other,
1188 );
1189 #default_expr
1190 }
1191 };
1192 #assign
1193 ::core::result::Result::Ok(true)
1194 }
1195 }
1196 });
1197 quote! {
1198 #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1199 #( #arms )*
1200 });
1201 }
1202}
1203
1204/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1205/// matches `field_name` against the model's FK fields and returns
1206/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1207/// group children by parent PK.
1208///
1209/// Always emitted (with `_ => None` for FK-less models) so the
1210/// trait bound on `fetch_with_prefetch` is universally satisfied.
1211fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1212 let root = rustango_root();
1213 let arms = fk_relations.iter().map(|rel| {
1214 let fk_col = rel.fk_column.as_str();
1215 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1216 if rel.pk_kind == DetectedKind::I64 {
1217 // i64 FK — return the stored PK so prefetch_related can
1218 // group children by it. Nullable variant unwraps via
1219 // `as_ref().map(...)`: an unset (NULL) FK column yields
1220 // `None` and that child sits out of the grouping (correct
1221 // semantics — it has no parent to attach to).
1222 if rel.nullable {
1223 quote! {
1224 #fk_col => self.#field_ident
1225 .as_ref()
1226 .map(|fk| #root::sql::ForeignKey::pk(fk)),
1227 }
1228 } else {
1229 quote! {
1230 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1231 }
1232 }
1233 } else {
1234 // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1235 // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1236 // i64-keyed grouping path — the trait signature is
1237 // `Option<i64>` and a non-i64 PK can't lower into it.
1238 // The FK still works for everything else (CRUD, lazy
1239 // load via `.get()`, select_related JOINs); only the
1240 // bulk prefetch grouper needs the integer key.
1241 quote! {
1242 #fk_col => ::core::option::Option::None,
1243 }
1244 }
1245 });
1246 // PK-type-agnostic version: every FK arm emits an
1247 // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1248 // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1249 // opt OUT of the legacy i64 method (it returns None) but opt IN
1250 // here.
1251 let value_arms = fk_relations.iter().map(|rel| {
1252 let fk_col = rel.fk_column.as_str();
1253 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1254 if rel.nullable {
1255 quote! {
1256 #fk_col => self.#field_ident
1257 .as_ref()
1258 .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1259 #root::sql::ForeignKey::pk(fk)
1260 )),
1261 }
1262 } else {
1263 quote! {
1264 #fk_col => ::core::option::Option::Some(
1265 ::core::convert::Into::<#root::core::SqlValue>::into(
1266 self.#field_ident.pk()
1267 )
1268 ),
1269 }
1270 }
1271 });
1272 quote! {
1273 impl #root::sql::FkPkAccess for #struct_name {
1274 #[allow(unused_variables)]
1275 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1276 match field_name {
1277 #( #arms )*
1278 _ => ::core::option::Option::None,
1279 }
1280 }
1281 #[allow(unused_variables)]
1282 fn __rustango_fk_pk_value(
1283 &self,
1284 field_name: &str,
1285 ) -> ::core::option::Option<#root::core::SqlValue> {
1286 match field_name {
1287 #( #value_arms )*
1288 _ => ::core::option::Option::None,
1289 }
1290 }
1291 }
1292 }
1293}
1294
1295/// For every `ForeignKey<Parent>` field on `Child`, emit
1296/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1297/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
1298/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
1299/// — the canonical reverse-FK fetch. One round trip, no N+1.
1300///
1301/// **PG-only emission**: the accessor is bounded on
1302/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1303/// both of which are gated behind the `postgres` cargo feature. The
1304/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1305/// model derive itself compiles on tri-dialect / sqlite-only
1306/// downstream builds — the accessor just isn't materialised. A tri-
1307/// dialect `_set_pool` variant is a separate follow-up.
1308fn reverse_helper_tokens(
1309 child_ident: &syn::Ident,
1310 fk_relations: &[FkRelation],
1311 default_related_name: Option<&str>,
1312) -> TokenStream2 {
1313 let root = rustango_root();
1314 if fk_relations.is_empty() {
1315 return TokenStream2::new();
1316 }
1317 // Method-name resolution per FK (issue #816 + follow-up):
1318 // 1. Field-level `#[rustango(related_name = "...")]` on the FK
1319 // itself — wins over everything else. Django's
1320 // `ForeignKey(related_name="...")`.
1321 // 2. Container-level `default_related_name = "..."` on the
1322 // child — Django's `class Meta: default_related_name`.
1323 // Applies to every FK on this model that didn't override.
1324 // 3. Fallback: `<child_snake>_set` — Django's `<child>_set`
1325 // convention. `Post` → `post_set`, `BlogComment` →
1326 // `blog_comment_set`. Avoids English-plural edge cases.
1327 //
1328 // The PG-on-executor variant keeps the resolved name; the
1329 // tri-dialect `_pool` variant appends `_pool` to it (matches the
1330 // framework's convention for the `&Pool` flavor of every helper).
1331 let default_pg_suffix = default_related_name
1332 .map(str::to_owned)
1333 .unwrap_or_else(|| format!("{}_set", to_snake_case(&child_ident.to_string())));
1334 let impls = fk_relations.iter().map(|rel| {
1335 let pg_suffix = rel
1336 .related_name
1337 .clone()
1338 .unwrap_or_else(|| default_pg_suffix.clone());
1339 let pool_suffix = format!("{}_pool", pg_suffix);
1340 let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1341 let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1342 let parent_ty = &rel.parent_type;
1343 let fk_col = rel.fk_column.as_str();
1344 let doc = format!(
1345 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1346 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1347 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1348 further `{child_ident}::objects()` filters via direct queryset use."
1349 );
1350 let pool_doc = format!(
1351 "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1352 [`#root::sql::Pool`] and dispatches per backend so the \
1353 reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1354 Use this from framework code that holds a `&Pool` (admin, \
1355 tenancy resolver, viewset handlers); reach for the executor- \
1356 bound variant when you already have a typed `sqlx::Executor`."
1357 );
1358 quote! {
1359 #[cfg(feature = "postgres")]
1360 impl #parent_ty {
1361 #[doc = #doc]
1362 ///
1363 /// # Errors
1364 /// Returns [`#root::sql::ExecError`] for SQL-writing
1365 /// or driver failures.
1366 pub async fn #pg_method_ident<'_c, _E>(
1367 &self,
1368 _executor: _E,
1369 ) -> ::core::result::Result<
1370 ::std::vec::Vec<#child_ident>,
1371 #root::sql::ExecError,
1372 >
1373 where
1374 _E: #root::sql::sqlx::Executor<
1375 '_c,
1376 Database = #root::sql::sqlx::Postgres,
1377 >,
1378 {
1379 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1380 #root::query::QuerySet::<#child_ident>::new()
1381 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1382 .fetch_on(_executor)
1383 .await
1384 }
1385 }
1386
1387 impl #parent_ty {
1388 #[doc = #pool_doc]
1389 ///
1390 /// # Errors
1391 /// Returns [`#root::sql::ExecError`] for SQL-writing
1392 /// or driver failures.
1393 pub async fn #pool_method_ident(
1394 &self,
1395 pool: &#root::sql::Pool,
1396 ) -> ::core::result::Result<
1397 ::std::vec::Vec<#child_ident>,
1398 #root::sql::ExecError,
1399 > {
1400 use #root::sql::FetcherPool as _;
1401 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1402 #root::query::QuerySet::<#child_ident>::new()
1403 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1404 .fetch(pool)
1405 .await
1406 }
1407 }
1408 }
1409 });
1410 quote! { #( #impls )* }
1411}
1412
1413/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1414/// relation declared on the model.
1415/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1416/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1417///
1418/// Closes #239 + #240 — the Django-shape `comment.content_object` /
1419/// `comment.content_object = post` ergonomics on top of the existing
1420/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1421///
1422/// `column_entries` is passed so we can resolve each `ct_column` /
1423/// `pk_column` SQL name back to its Rust field ident — the macro
1424/// only sees the column-side strings in the attribute, but the
1425/// emitted accessor needs to read the actual struct field.
1426fn generic_fk_accessor_tokens(
1427 struct_name: &syn::Ident,
1428 generic_fks: &[GenericFkAttr],
1429 column_entries: &[ColumnEntry],
1430) -> TokenStream2 {
1431 let root = rustango_root();
1432 if generic_fks.is_empty() {
1433 return TokenStream2::new();
1434 }
1435 let methods = generic_fks.iter().filter_map(|gfk| {
1436 // Resolve `ct_column` + `pk_column` to the struct's Rust
1437 // field idents. A typo (column name doesn't match any field)
1438 // emits no method for that registration — the user will see
1439 // the compiler reject the SCHEMA literal anyway, so there's
1440 // a clear error path without us double-reporting.
1441 let ct_ident = column_entries
1442 .iter()
1443 .find(|c| c.column == gfk.ct_column)
1444 .map(|c| c.ident.clone())?;
1445 let pk_ident = column_entries
1446 .iter()
1447 .find(|c| c.column == gfk.pk_column)
1448 .map(|c| c.ident.clone())?;
1449
1450 let accessor_ident =
1451 syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1452 let setter_ident =
1453 syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1454 let name_literal = gfk.name.as_str();
1455
1456 Some(quote! {
1457 #[doc = concat!(
1458 "Resolve the polymorphic `",
1459 #name_literal,
1460 "` relation. Reads `self.",
1461 stringify!(#ct_ident),
1462 "` + `self.",
1463 stringify!(#pk_ident),
1464 "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1465 "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1466 #name_literal,
1467 "\", ...))]`."
1468 )]
1469 pub async fn #accessor_ident(
1470 &self,
1471 pool: &#root::sql::Pool,
1472 ) -> ::core::result::Result<
1473 ::core::option::Option<#root::__serde_json::Value>,
1474 #root::sql::ExecError,
1475 > {
1476 let gfk = #root::contenttypes::GenericForeignKey::new(
1477 self.#ct_ident as i64,
1478 self.#pk_ident as i64,
1479 );
1480 gfk.get_object(pool).await
1481 }
1482
1483 #[doc = concat!(
1484 "Set the polymorphic `",
1485 #name_literal,
1486 "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1487 stringify!(#ct_ident),
1488 "` and `self.",
1489 stringify!(#pk_ident),
1490 "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1491 #name_literal,
1492 "\", ...))]`."
1493 )]
1494 pub async fn #setter_ident<T: #root::core::Model>(
1495 &mut self,
1496 pool: &#root::sql::Pool,
1497 target_pk: i64,
1498 ) -> ::core::result::Result<(), #root::sql::ExecError> {
1499 let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1500 pool,
1501 target_pk,
1502 ).await?;
1503 self.#ct_ident = gfk.content_type_id as _;
1504 self.#pk_ident = gfk.object_pk as _;
1505 ::core::result::Result::Ok(())
1506 }
1507 })
1508 });
1509 quote! {
1510 impl #struct_name {
1511 #( #methods )*
1512 }
1513 }
1514}
1515
1516fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1517 let root = rustango_root();
1518 if m2m_relations.is_empty() {
1519 return TokenStream2::new();
1520 }
1521 let methods = m2m_relations.iter().map(|rel| {
1522 let method_name = format!("{}_m2m", rel.name);
1523 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1524 let through = rel.through.as_str();
1525 let src_col = rel.src.as_str();
1526 let dst_col = rel.dst.as_str();
1527 quote! {
1528 pub fn #method_ident(&self) -> #root::sql::M2MManager {
1529 #root::sql::M2MManager {
1530 src_pk: self.__rustango_pk_value(),
1531 through: #through,
1532 src_col: #src_col,
1533 dst_col: #dst_col,
1534 }
1535 }
1536 }
1537 });
1538 quote! {
1539 impl #struct_name {
1540 #( #methods )*
1541 }
1542 }
1543}
1544
1545/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1546/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1547fn generic_m2m_accessor_tokens(
1548 struct_name: &syn::Ident,
1549 relations: &[GenericM2MAttr],
1550) -> TokenStream2 {
1551 let root = rustango_root();
1552 if relations.is_empty() {
1553 return TokenStream2::new();
1554 }
1555 let methods = relations.iter().map(|rel| {
1556 let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1557 let through = rel.through.as_str();
1558 let pk_col = rel.pk_column.as_str();
1559 let ct_col = rel.ct_column.as_str();
1560 let related_col = rel.related_column.as_str();
1561 quote! {
1562 pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1563 #root::sql::GenericM2MManager {
1564 src_pk: self.__rustango_pk_value(),
1565 src_schema: <Self as #root::core::Model>::SCHEMA,
1566 through: #through,
1567 pk_col: #pk_col,
1568 ct_col: #ct_col,
1569 dst_col: #related_col,
1570 }
1571 }
1572 }
1573 });
1574 quote! {
1575 impl #struct_name {
1576 #( #methods )*
1577 }
1578 }
1579}
1580
1581/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1582/// associated functions for each `#[rustango(reverse_has(...))]`
1583/// attribute. Issue #830.
1584///
1585/// The two emitted functions return ready-to-use `WhereExpr` nodes
1586/// that downstream callers drop into
1587/// `QuerySet::<Self>::where_raw(...)`:
1588///
1589/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1590/// <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1591/// `whereHas` parity (without the closure-style sub-predicate
1592/// refinement — that's a follow-up; users can layer additional
1593/// predicates by constructing the SelectQuery themselves and
1594/// calling `where_raw(exists(query))` from `crate::core::subquery`).
1595/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1596/// `whereDoesntHave` parity.
1597///
1598/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1599/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1600/// machinery threads the outer-table reference through automatically
1601/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1602fn reverse_has_accessor_tokens(
1603 struct_name: &syn::Ident,
1604 reverse_has_relations: &[ReverseHasAttr],
1605) -> TokenStream2 {
1606 let root = rustango_root();
1607 if reverse_has_relations.is_empty() {
1608 return TokenStream2::new();
1609 }
1610 let methods = reverse_has_relations.iter().map(|rel| {
1611 let exists_name = format!("{}_exists_expr", rel.name);
1612 let not_exists_name = format!("{}_not_exists_expr", rel.name);
1613 let count_name = format!("{}_count", rel.name);
1614 let fetch_name = format!("{}_fetch", rel.name);
1615 let first_name = format!("{}_first", rel.name);
1616 let pluck_name = format!("{}_pluck", rel.name);
1617 let accessor_name = rel.name.as_str();
1618 let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1619 let not_exists_ident = syn::Ident::new(¬_exists_name, struct_name.span());
1620 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1621 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1622 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1623 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1624 let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1625 let child = &rel.child;
1626 let child_fk_column = rel.child_fk_column.as_str();
1627 let self_pk_column = rel.self_pk_column.as_str();
1628 let exists_doc = format!(
1629 "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1630 FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1631 Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1632 filter to {struct_name}s with at least one matching child.",
1633 );
1634 let not_exists_doc = format!(
1635 "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1636 (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1637 <outer>.{self_pk_column})`. Drop into \
1638 `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1639 {struct_name}s with **no** matching child.",
1640 );
1641 let count_doc = format!(
1642 "Eloquent `$model->{name}->count()` analog — returns \
1643 the number of `{child}` rows whose `{child_fk_column}` \
1644 matches this `{struct_name}` instance's primary key. \
1645 Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1646 {child_fk_column} = <self.pk>`.",
1647 name = rel.name,
1648 );
1649 let accessor_doc = format!(
1650 "Eloquent `$model->{name}` accessor — returns a \
1651 `QuerySet<{child}>` filtered to rows whose \
1652 `{child_fk_column}` matches this `{struct_name}` \
1653 instance's primary key. **Chainable**: compose `.filter()` \
1654 / `.order_by()` / `.limit()` etc. on top, then call \
1655 `.fetch(&pool)` (the QuerySet trait method) when \
1656 done. For the simple \"fetch all\" hot path with no \
1657 further composition, prefer the bare-name \
1658 `{name}_fetch(&pool)` companion.",
1659 name = rel.name,
1660 );
1661 let fetch_doc = format!(
1662 "Eloquent `$model->{name}->get()` — bare-name hot-path \
1663 over `{name}(&self).fetch(&pool)`. Use this when \
1664 you don't need further `.filter()` / `.order_by()` \
1665 composition; falls back to the chainable accessor when \
1666 you do. Avoids the `_pool` suffix on the most common \
1667 call-site shape.",
1668 name = rel.name,
1669 );
1670 quote! {
1671 #[doc = #accessor_doc]
1672 pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1673 #root::query::QuerySet::<#child>::new()
1674 .filter(#child_fk_column, self.__rustango_pk_value())
1675 }
1676
1677 #[doc = #fetch_doc]
1678 pub async fn #fetch_ident(
1679 &self,
1680 pool: &#root::sql::Pool,
1681 ) -> ::core::result::Result<
1682 ::std::vec::Vec<#child>,
1683 #root::sql::ExecError,
1684 > {
1685 use #root::sql::FetcherPool as _;
1686 self.#accessor_ident().fetch(pool).await
1687 }
1688
1689 /// Eloquent `$model->relation->first()` / `hasOne`
1690 /// semantics — bare-name shortcut over
1691 /// `self.<name>().first(&pool)`. Returns `None` when no
1692 /// child rows match. Useful when the relation is
1693 /// nominally many-to-one in shape but at most one row is
1694 /// expected (latest comment, primary tag, etc.).
1695 pub async fn #first_ident(
1696 &self,
1697 pool: &#root::sql::Pool,
1698 ) -> ::core::result::Result<
1699 ::core::option::Option<#child>,
1700 #root::sql::ExecError,
1701 > {
1702 self.#accessor_ident().first(pool).await
1703 }
1704
1705 /// Eloquent `$model->relation->pluck($col)` — project a
1706 /// single column from the child rows into a `Vec<U>`.
1707 /// Skips the typed `Child` decode, which is cheaper when
1708 /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1709 pub async fn #pluck_ident<U>(
1710 &self,
1711 col: &'static str,
1712 pool: &#root::sql::Pool,
1713 ) -> ::core::result::Result<
1714 ::std::vec::Vec<U>,
1715 #root::sql::ExecError,
1716 >
1717 where
1718 U: #root::sql::MaybePgScalar
1719 + #root::sql::MaybeMyScalar
1720 + #root::sql::MaybeSqliteScalar
1721 + ::core::marker::Send
1722 + ::core::marker::Unpin,
1723 {
1724 self.#accessor_ident()
1725 .values_list_flat(col)
1726 .fetch::<U>(pool)
1727 .await
1728 }
1729
1730 #[doc = #exists_doc]
1731 pub fn #exists_ident() -> #root::core::WhereExpr {
1732 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1733 let child_schema =
1734 <#child as #root::core::Model>::SCHEMA;
1735 let inner = SelectQuery {
1736 where_clause: WhereExpr::ExprCompare {
1737 lhs: Expr::Column(#child_fk_column),
1738 op: Op::Eq,
1739 rhs: Expr::OuterRef(#self_pk_column),
1740 },
1741 ..SelectQuery::new(child_schema)
1742 };
1743 WhereExpr::Exists(::std::boxed::Box::new(inner))
1744 }
1745
1746 #[doc = #not_exists_doc]
1747 pub fn #not_exists_ident() -> #root::core::WhereExpr {
1748 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1749 let child_schema =
1750 <#child as #root::core::Model>::SCHEMA;
1751 let inner = SelectQuery {
1752 where_clause: WhereExpr::ExprCompare {
1753 lhs: Expr::Column(#child_fk_column),
1754 op: Op::Eq,
1755 rhs: Expr::OuterRef(#self_pk_column),
1756 },
1757 ..SelectQuery::new(child_schema)
1758 };
1759 WhereExpr::NotExists(::std::boxed::Box::new(inner))
1760 }
1761
1762 #[doc = #count_doc]
1763 pub async fn #count_ident(
1764 &self,
1765 pool: &#root::sql::Pool,
1766 ) -> ::core::result::Result<
1767 i64,
1768 #root::sql::ExecError,
1769 > {
1770 use #root::sql::CounterPool as _;
1771 #root::query::QuerySet::<#child>::new()
1772 .filter(#child_fk_column, self.__rustango_pk_value())
1773 .count(pool)
1774 .await
1775 }
1776 }
1777 });
1778 quote! {
1779 impl #struct_name {
1780 #( #methods )*
1781 }
1782 }
1783}
1784
1785/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1786/// `#[rustango(through(...))]` attribute. Issue #817.
1787///
1788/// Each accessor builds a correlated subquery via
1789/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1790/// intermediate table, filters on the FK column pointing at this
1791/// model, and projects the intermediate PK. The outer queryset
1792/// filters the far model by its FK-to-intermediate column being in
1793/// that set.
1794///
1795/// The returned `QuerySet<Far>` is **chainable** — the subquery
1796/// lives inside a `where_raw` clause so the user's later
1797/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1798/// disturb it.
1799///
1800/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1801/// SQLite — no LATERAL or backend-specific syntax involved.
1802fn through_accessor_tokens(
1803 struct_name: &syn::Ident,
1804 through_relations: &[ThroughAttr],
1805) -> TokenStream2 {
1806 let root = rustango_root();
1807 if through_relations.is_empty() {
1808 return TokenStream2::new();
1809 }
1810 let methods = through_relations.iter().map(|rel| {
1811 let method_name = format!("{}_through", rel.name);
1812 let count_name = format!("{}_through_count", rel.name);
1813 let fetch_name = format!("{}_through_fetch", rel.name);
1814 let first_name = format!("{}_through_first", rel.name);
1815 let pluck_name = format!("{}_through_pluck", rel.name);
1816 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1817 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1818 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1819 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1820 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1821 let far = &rel.far;
1822 let intermediate = &rel.intermediate;
1823 let far_fk_column = rel.far_fk_column.as_str();
1824 let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1825 let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1826 let doc = format!(
1827 "Eloquent `hasManyThrough` accessor — returns a \
1828 `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1829 instance through the intermediate `{intermediate}` table. \
1830 Generated SQL shape: \
1831 `… WHERE {far_fk_column} IN (SELECT \
1832 {intermediate_pk_column} FROM <{intermediate}> WHERE \
1833 {intermediate_fk_column} = self.pk)`. Chainable like any \
1834 other QuerySet — compose `.filter()` / `.order_by()` / \
1835 `.limit()` etc. on top.",
1836 );
1837 let count_doc = format!(
1838 "Eloquent `$model->{name}->count()` analog for the \
1839 through-relation — returns the number of `{far}` rows \
1840 reachable through `{intermediate}`. Equivalent to \
1841 `self.{name}_through().count(pool)` but spelled \
1842 as a bare instance method for parity with the \
1843 `reverse_has` `<name>_count` shape.",
1844 name = rel.name,
1845 );
1846 quote! {
1847 #[doc = #doc]
1848 pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1849 use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1850 let intermediate_schema =
1851 <#intermediate as #root::core::Model>::SCHEMA;
1852 let sub = SelectQuery {
1853 where_clause: WhereExpr::Predicate(Filter {
1854 column: #intermediate_fk_column,
1855 op: Op::Eq,
1856 value: self.__rustango_pk_value(),
1857 }),
1858 projection: ::core::option::Option::Some(
1859 ::std::vec![#intermediate_pk_column],
1860 ),
1861 ..SelectQuery::new(intermediate_schema)
1862 };
1863 #root::query::QuerySet::<#far>::new().where_raw(
1864 WhereExpr::InSubquery {
1865 column: #far_fk_column,
1866 negated: false,
1867 subquery: ::std::boxed::Box::new(sub),
1868 },
1869 )
1870 }
1871
1872 #[doc = #count_doc]
1873 pub async fn #count_ident(
1874 &self,
1875 pool: &#root::sql::Pool,
1876 ) -> ::core::result::Result<
1877 i64,
1878 #root::sql::ExecError,
1879 > {
1880 use #root::sql::CounterPool as _;
1881 self.#method_ident().count(pool).await
1882 }
1883
1884 /// Eloquent `$model->relation->get()` for the
1885 /// through-relation — bare-name hot-path over
1886 /// `self.<name>_through().fetch(&pool)`. Use this
1887 /// when you don't need further `.filter()` /
1888 /// `.order_by()` composition; falls back to the
1889 /// chainable accessor when you do. Avoids the `_pool`
1890 /// suffix on the most common call-site shape.
1891 pub async fn #fetch_ident(
1892 &self,
1893 pool: &#root::sql::Pool,
1894 ) -> ::core::result::Result<
1895 ::std::vec::Vec<#far>,
1896 #root::sql::ExecError,
1897 > {
1898 use #root::sql::FetcherPool as _;
1899 self.#method_ident().fetch(pool).await
1900 }
1901
1902 /// Eloquent `hasOneThrough` analog — bare-name shortcut
1903 /// over `self.<name>_through().first(&pool)`. Returns
1904 /// `None` when no far rows are reachable through the
1905 /// intermediate. Useful when at most one row is
1906 /// expected (latest comment by country, primary tag,
1907 /// etc.).
1908 pub async fn #first_ident(
1909 &self,
1910 pool: &#root::sql::Pool,
1911 ) -> ::core::result::Result<
1912 ::core::option::Option<#far>,
1913 #root::sql::ExecError,
1914 > {
1915 self.#method_ident().first(pool).await
1916 }
1917
1918 /// Pluck a single column from the far rows into `Vec<U>`
1919 /// — cheaper than the typed `<Far>` decode when only one
1920 /// scalar column is needed.
1921 pub async fn #pluck_ident<U>(
1922 &self,
1923 col: &'static str,
1924 pool: &#root::sql::Pool,
1925 ) -> ::core::result::Result<
1926 ::std::vec::Vec<U>,
1927 #root::sql::ExecError,
1928 >
1929 where
1930 U: #root::sql::MaybePgScalar
1931 + #root::sql::MaybeMyScalar
1932 + #root::sql::MaybeSqliteScalar
1933 + ::core::marker::Send
1934 + ::core::marker::Unpin,
1935 {
1936 self.#method_ident()
1937 .values_list_flat(col)
1938 .fetch::<U>(pool)
1939 .await
1940 }
1941 }
1942 });
1943 quote! {
1944 impl #struct_name {
1945 #( #methods )*
1946 }
1947 }
1948}
1949
1950struct ColumnEntry {
1951 /// The struct field ident, used both for the inherent const name on
1952 /// the model and for the inner column type's name.
1953 ident: syn::Ident,
1954 /// The struct's field type, used as `Column::Value`.
1955 value_ty: Type,
1956 /// Rust-side field name (e.g. `"id"`).
1957 name: String,
1958 /// SQL-side column name (e.g. `"user_id"`).
1959 column: String,
1960 /// `#root::core::FieldType::I64` etc.
1961 field_type_tokens: TokenStream2,
1962}
1963
1964struct CollectedFields {
1965 field_schemas: Vec<TokenStream2>,
1966 from_row_inits: Vec<TokenStream2>,
1967 /// Aliased counterparts of `from_row_inits` — read columns via
1968 /// `format!("{prefix}__{col}")` aliases so a Model can be
1969 /// decoded from a JOINed row's projected target columns.
1970 from_aliased_row_inits: Vec<TokenStream2>,
1971 /// Static column-name list — used by the simple insert path
1972 /// (no `Auto<T>` fields). Aligned with `insert_values`.
1973 insert_columns: Vec<TokenStream2>,
1974 /// Static `Into<SqlValue>` expressions, one per field. Aligned
1975 /// with `insert_columns`. Used by the simple insert path only.
1976 insert_values: Vec<TokenStream2>,
1977 /// Per-field push expressions for the dynamic (Auto-aware)
1978 /// insert path. Each statement either unconditionally pushes
1979 /// `(column, value)` or, for an `Auto<T>` field, conditionally
1980 /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
1981 insert_pushes: Vec<TokenStream2>,
1982 /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
1983 /// when `has_auto == false`.
1984 returning_cols: Vec<TokenStream2>,
1985 /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
1986 /// field. Run after `insert_returning` to populate the model.
1987 auto_assigns: Vec<TokenStream2>,
1988 /// `(ident, column_literal)` pairs for every Auto field. Used by
1989 /// the bulk_insert codegen to rebuild assigns against `_row_mut`
1990 /// instead of `self`.
1991 auto_field_idents: Vec<(syn::Ident, String)>,
1992 /// #1028 — `(ident, column)` for each `generated_as` field. Drives
1993 /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
1994 /// back into the struct after insert (MySQL has no RETURNING → the
1995 /// field stays at its placeholder, deferred, matching Django 6.0).
1996 generated_field_idents: Vec<(syn::Ident, String)>,
1997 /// Inner `T` of the first `Auto<T>` field, for the MySQL
1998 /// `LAST_INSERT_ID()` assignment in `AssignAutoPkPool`.
1999 first_auto_value_ty: Option<Type>,
2000 /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
2001 /// by the all-Auto-Unset bulk path (Auto cols dropped from
2002 /// `columns`).
2003 bulk_pushes_no_auto: Vec<TokenStream2>,
2004 /// Bulk-insert per-row pushes for **all fields including Auto**.
2005 /// Used by the all-Auto-Set bulk path (Auto col included with the
2006 /// caller-supplied value).
2007 bulk_pushes_all: Vec<TokenStream2>,
2008 /// Column-name literals for non-Auto fields only (paired with
2009 /// `bulk_pushes_no_auto`).
2010 bulk_columns_no_auto: Vec<TokenStream2>,
2011 /// Column-name literals for every field including Auto (paired
2012 /// with `bulk_pushes_all`).
2013 bulk_columns_all: Vec<TokenStream2>,
2014 /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2015 /// + the loop that asserts every row matches. One pair per Auto
2016 /// field. Empty when `has_auto == false`.
2017 bulk_auto_uniformity: Vec<TokenStream2>,
2018 /// Identifier of the first Auto field, used as the witness for
2019 /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2020 first_auto_ident: Option<syn::Ident>,
2021 /// `true` if any field on the struct is `Auto<T>`.
2022 has_auto: bool,
2023 /// `true` when the primary-key field's Rust type is `Auto<T>`.
2024 /// Gates `save()` codegen — only Auto PKs let us infer
2025 /// insert-vs-update from the in-memory value.
2026 pk_is_auto: bool,
2027 /// `Assignment` constructors for every non-PK column. Drives the
2028 /// UPDATE branch of `save()`.
2029 update_assignments: Vec<TokenStream2>,
2030 /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2031 /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2032 upsert_update_columns: Vec<TokenStream2>,
2033 primary_key: Option<(syn::Ident, String)>,
2034 column_entries: Vec<ColumnEntry>,
2035 /// Rust-side field names, in declaration order. Used to validate
2036 /// container attributes like `display = "…"`.
2037 field_names: Vec<String>,
2038 /// FK fields on this child model. Drives the reverse-relation
2039 /// helper emit — for each FK, the macro adds an inherent
2040 /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2041 /// method on the parent type.
2042 fk_relations: Vec<FkRelation>,
2043 /// SQL column name of the `#[rustango(soft_delete)]` field, if
2044 /// the model has one. Drives emission of the `soft_delete_on` /
2045 /// `restore_on` inherent methods. At most one such column per
2046 /// model is allowed; collect_fields rejects duplicates.
2047 soft_delete_column: Option<String>,
2048 /// Rust field ident of the `#[rustango(soft_delete)]` field —
2049 /// companion to `soft_delete_column` for emitting predicates
2050 /// that need to read the field off `&self` (e.g. `trashed()`).
2051 soft_delete_field_ident: Option<syn::Ident>,
2052}
2053
2054#[derive(Clone)]
2055struct FkRelation {
2056 /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2057 /// helper is emitted as `impl <ParentType> { … }`.
2058 parent_type: Type,
2059 /// SQL column name on the child table for this FK (e.g. `"author"`).
2060 /// Used in the generated `WHERE <fk_column> = $1` clause.
2061 fk_column: String,
2062 /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2063 /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2064 /// default `ForeignKey<T>` (no explicit K); other kinds when the
2065 /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2066 pk_kind: DetectedKind,
2067 /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2068 /// FK column). Drives the `Some(...)` wrapping in load_related
2069 /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2070 /// the codegen matches the field's declared shape.
2071 nullable: bool,
2072 /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2073 /// override. When set, the reverse helper picks this name instead
2074 /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2075 /// #816 (issue's "Related" note re: per-FK override).
2076 related_name: Option<String>,
2077}
2078
2079fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2080 let root = rustango_root();
2081 let cap = named.named.len();
2082 let mut out = CollectedFields {
2083 field_schemas: Vec::with_capacity(cap),
2084 from_row_inits: Vec::with_capacity(cap),
2085 from_aliased_row_inits: Vec::with_capacity(cap),
2086 insert_columns: Vec::with_capacity(cap),
2087 insert_values: Vec::with_capacity(cap),
2088 insert_pushes: Vec::with_capacity(cap),
2089 returning_cols: Vec::new(),
2090 auto_assigns: Vec::new(),
2091 auto_field_idents: Vec::new(),
2092 generated_field_idents: Vec::new(),
2093 first_auto_value_ty: None,
2094 bulk_pushes_no_auto: Vec::with_capacity(cap),
2095 bulk_pushes_all: Vec::with_capacity(cap),
2096 bulk_columns_no_auto: Vec::with_capacity(cap),
2097 bulk_columns_all: Vec::with_capacity(cap),
2098 bulk_auto_uniformity: Vec::new(),
2099 first_auto_ident: None,
2100 has_auto: false,
2101 pk_is_auto: false,
2102 update_assignments: Vec::with_capacity(cap),
2103 upsert_update_columns: Vec::with_capacity(cap),
2104 primary_key: None,
2105 column_entries: Vec::with_capacity(cap),
2106 field_names: Vec::with_capacity(cap),
2107 fk_relations: Vec::new(),
2108 soft_delete_column: None,
2109 soft_delete_field_ident: None,
2110 };
2111
2112 for field in &named.named {
2113 let info = process_field(field, table)?;
2114 out.field_names.push(info.ident.to_string());
2115 out.field_schemas.push(info.schema);
2116 out.from_row_inits.push(info.from_row_init);
2117 out.from_aliased_row_inits.push(info.from_aliased_row_init);
2118 if let Some(parent_ty) = info.fk_inner.clone() {
2119 out.fk_relations.push(FkRelation {
2120 parent_type: parent_ty,
2121 fk_column: info.column.clone(),
2122 pk_kind: info.fk_pk_kind,
2123 nullable: info.nullable,
2124 related_name: info.related_name.clone(),
2125 });
2126 }
2127 if info.soft_delete {
2128 if out.soft_delete_column.is_some() {
2129 return Err(syn::Error::new_spanned(
2130 field,
2131 "only one field may be marked `#[rustango(soft_delete)]`",
2132 ));
2133 }
2134 out.soft_delete_column = Some(info.column.clone());
2135 out.soft_delete_field_ident = Some(info.ident.clone());
2136 }
2137 let column = info.column.as_str();
2138 let ident = info.ident;
2139 // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2140 // skip every write path — Postgres recomputes the value
2141 // from EXPR. Push only the column-entry record (so typed
2142 // column constants still exist for filtering / projection)
2143 // and the schema literal (already pushed above) and move
2144 // on. No insert_columns/values, no insert_pushes, no
2145 // bulk_*, no update_assignments, no upsert_update_columns,
2146 // no returning_cols.
2147 if info.generated_as.is_some() {
2148 out.column_entries.push(ColumnEntry {
2149 ident: ident.clone(),
2150 value_ty: info.value_ty.clone(),
2151 name: ident.to_string(),
2152 column: info.column.clone(),
2153 field_type_tokens: info.field_type_tokens,
2154 });
2155 // #1028 — refresh the DB-computed value after insert on
2156 // RETURNING-capable backends. The column joins `returning_cols`
2157 // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2158 // ident is recorded so the `AssignAutoPkPool` impl decodes it
2159 // back into the struct. MySQL has no `INSERT … RETURNING`, so
2160 // it keeps the placeholder (deferred refresh, matching Django).
2161 out.returning_cols.push(quote!(#column));
2162 out.generated_field_idents
2163 .push((ident.clone(), info.column.clone()));
2164 continue;
2165 }
2166 out.insert_columns.push(quote!(#column));
2167 out.insert_values.push(quote! {
2168 ::core::convert::Into::<#root::core::SqlValue>::into(
2169 ::core::clone::Clone::clone(&self.#ident)
2170 )
2171 });
2172 if info.auto {
2173 out.has_auto = true;
2174 if out.first_auto_ident.is_none() {
2175 out.first_auto_ident = Some(ident.clone());
2176 out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
2177 }
2178 // `default_uuid_v7` (issue #823) generates the PK Rust-side
2179 // before binding, so the value is already in
2180 // `self.#ident` after the insert_push — RETURNING is
2181 // redundant. Skip adding this column to returning_cols /
2182 // auto_assigns / auto_field_idents to avoid (a) an
2183 // unnecessary RETURNING column on every dialect, and (b)
2184 // the MySQL `LAST_INSERT_ID()` path that can only fill an
2185 // integer PK.
2186 if !info.default_uuid_v7 {
2187 out.returning_cols.push(quote!(#column));
2188 out.auto_field_idents
2189 .push((ident.clone(), info.column.clone()));
2190 out.auto_assigns.push(quote! {
2191 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2192 });
2193 }
2194 if info.default_uuid_v7 {
2195 // Rust-side UUIDv7 generation (issue #823, Eloquent
2196 // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2197 // and bind; Auto::Set → bind the user's value. The
2198 // column is ALWAYS present in the INSERT statement —
2199 // no RETURNING / no DB DEFAULT needed.
2200 out.insert_pushes.push(quote! {
2201 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2202 self.#ident = #root::sql::Auto::Set(
2203 #root::__uuid::Uuid::now_v7(),
2204 );
2205 }
2206 if let #root::sql::Auto::Set(_v) = &self.#ident {
2207 _columns.push(#column);
2208 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2209 ::core::clone::Clone::clone(_v)
2210 ));
2211 }
2212 });
2213 } else {
2214 out.insert_pushes.push(quote! {
2215 if let #root::sql::Auto::Set(_v) = &self.#ident {
2216 _columns.push(#column);
2217 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2218 ::core::clone::Clone::clone(_v)
2219 ));
2220 }
2221 });
2222 }
2223 // Bulk: Auto fields appear only in the all-Set path,
2224 // never in the Unset path (we drop them from `columns`).
2225 out.bulk_columns_all.push(quote!(#column));
2226 out.bulk_pushes_all.push(quote! {
2227 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2228 ::core::clone::Clone::clone(&_row.#ident)
2229 ));
2230 });
2231 // Uniformity check: every row's Auto state must match the
2232 // first row's. Mixed Set/Unset within one bulk_insert is
2233 // rejected here so the column list stays consistent.
2234 let ident_clone = ident.clone();
2235 out.bulk_auto_uniformity.push(quote! {
2236 for _r in rows.iter().skip(1) {
2237 if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2238 return ::core::result::Result::Err(
2239 #root::sql::ExecError::Sql(
2240 #root::sql::SqlError::BulkAutoMixed
2241 )
2242 );
2243 }
2244 }
2245 });
2246 } else {
2247 out.insert_pushes.push(quote! {
2248 _columns.push(#column);
2249 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2250 ::core::clone::Clone::clone(&self.#ident)
2251 ));
2252 });
2253 // Bulk: non-Auto fields appear in BOTH paths.
2254 out.bulk_columns_no_auto.push(quote!(#column));
2255 out.bulk_columns_all.push(quote!(#column));
2256 let push_expr = quote! {
2257 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2258 ::core::clone::Clone::clone(&_row.#ident)
2259 ));
2260 };
2261 out.bulk_pushes_no_auto.push(push_expr.clone());
2262 out.bulk_pushes_all.push(push_expr);
2263 }
2264 if info.primary_key {
2265 if out.primary_key.is_some() {
2266 return Err(syn::Error::new_spanned(
2267 field,
2268 "only one field may be marked `#[rustango(primary_key)]`",
2269 ));
2270 }
2271 out.primary_key = Some((ident.clone(), info.column.clone()));
2272 if info.auto {
2273 out.pk_is_auto = true;
2274 }
2275 } else if info.auto_now_add {
2276 // Immutable post-insert: skip from UPDATE entirely.
2277 } else if info.auto_now {
2278 // `auto_now` columns: bind `chrono::Utc::now()` on every
2279 // UPDATE so the column is always overridden with the
2280 // wall-clock at write time, regardless of what value the
2281 // user left in the struct field.
2282 out.update_assignments.push(quote! {
2283 #root::core::Assignment {
2284 column: #column,
2285 value: ::core::convert::Into::<#root::core::Expr>::into(
2286 ::core::convert::Into::<#root::core::SqlValue>::into(
2287 #root::__chrono::Utc::now()
2288 )
2289 ),
2290 }
2291 });
2292 out.upsert_update_columns.push(quote!(#column));
2293 } else {
2294 out.update_assignments.push(quote! {
2295 #root::core::Assignment {
2296 column: #column,
2297 value: ::core::convert::Into::<#root::core::Expr>::into(
2298 ::core::convert::Into::<#root::core::SqlValue>::into(
2299 ::core::clone::Clone::clone(&self.#ident)
2300 )
2301 ),
2302 }
2303 });
2304 out.upsert_update_columns.push(quote!(#column));
2305 }
2306 out.column_entries.push(ColumnEntry {
2307 ident: ident.clone(),
2308 value_ty: info.value_ty.clone(),
2309 name: ident.to_string(),
2310 column: info.column.clone(),
2311 field_type_tokens: info.field_type_tokens,
2312 });
2313 }
2314 Ok(out)
2315}
2316
2317fn model_impl_tokens(
2318 struct_name: &syn::Ident,
2319 model_name: &str,
2320 table: &str,
2321 display: Option<&str>,
2322 app_label: Option<&str>,
2323 admin: Option<&AdminAttrs>,
2324 default_order: &[(String, bool, proc_macro2::Span)],
2325 field_schemas: &[TokenStream2],
2326 soft_delete_column: Option<&str>,
2327 permissions: bool,
2328 audit_track: Option<&[String]>,
2329 m2m_relations: &[M2MAttr],
2330 indexes: &[IndexAttr],
2331 checks: &[CheckAttr],
2332 excludes: &[ExcludeAttr],
2333 composite_fks: &[CompositeFkAttr],
2334 generic_fks: &[GenericFkAttr],
2335 scope: Option<&str>,
2336 is_view: bool,
2337 verbose_name: Option<&str>,
2338 verbose_name_plural: Option<&str>,
2339 managed: bool,
2340 base_manager_name: Option<&str>,
2341 order_with_respect_to: Option<&str>,
2342 proxy: bool,
2343 required_db_features: &[String],
2344 required_db_vendor: Option<&str>,
2345 default_related_name: Option<&str>,
2346 db_table_comment: Option<&str>,
2347 get_latest_by: Option<(&str, bool)>,
2348 extra_permissions: &[(String, String)],
2349 default_permissions: &[String],
2350 global_scopes: &[GlobalScopeAttr],
2351 reverse_has_relations: &[ReverseHasAttr],
2352 generic_has_relations: &[GenericHasAttr],
2353) -> TokenStream2 {
2354 let root = rustango_root();
2355 let display_tokens = if let Some(name) = display {
2356 quote!(::core::option::Option::Some(#name))
2357 } else {
2358 quote!(::core::option::Option::None)
2359 };
2360 let app_label_tokens = if let Some(name) = app_label {
2361 quote!(::core::option::Option::Some(#name))
2362 } else {
2363 quote!(::core::option::Option::None)
2364 };
2365 let soft_delete_tokens = if let Some(col) = soft_delete_column {
2366 quote!(::core::option::Option::Some(#col))
2367 } else {
2368 quote!(::core::option::Option::None)
2369 };
2370 let audit_track_tokens = match audit_track {
2371 None => quote!(::core::option::Option::None),
2372 Some(names) => {
2373 let lits = names.iter().map(|n| n.as_str());
2374 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2375 }
2376 };
2377 let admin_tokens = admin_config_tokens(admin);
2378 // Default `tenant` so single-tenant projects (no `scope` attr
2379 // anywhere) keep the v0.24.x behavior. Container-attr parser
2380 // already validated the value is "registry" or "tenant".
2381 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2382 Some("registry") => quote!(#root::core::ModelScope::Registry),
2383 _ => quote!(#root::core::ModelScope::Tenant),
2384 };
2385 let verbose_name_tokens = optional_str(verbose_name);
2386 let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2387 let base_manager_name_tokens = optional_str(base_manager_name);
2388 let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2389 let required_db_features_lits: Vec<&str> =
2390 required_db_features.iter().map(String::as_str).collect();
2391 let required_db_vendor_tokens = optional_str(required_db_vendor);
2392 let default_related_name_tokens = optional_str(default_related_name);
2393 let db_table_comment_tokens = optional_str(db_table_comment);
2394 let get_latest_by_tokens = match get_latest_by {
2395 Some((col, desc)) => {
2396 quote!(::core::option::Option::Some((#col, #desc)))
2397 }
2398 None => quote!(::core::option::Option::None),
2399 };
2400 let extra_permission_tokens: Vec<_> = extra_permissions
2401 .iter()
2402 .map(|(c, l)| quote!((#c, #l)))
2403 .collect();
2404 let default_permission_tokens: Vec<_> = default_permissions
2405 .iter()
2406 .map(|action| quote!(#action))
2407 .collect();
2408 let indexes_tokens = indexes.iter().map(|idx| {
2409 // When no explicit `name = "..."` was given, derive a stable,
2410 // collision-free name from the table + columns (Django-shape
2411 // `<table>_<col>_<col>_idx`) instead of a shared literal that
2412 // would clash the moment a model declares two unnamed indexes.
2413 // Capped at Postgres's 63-char identifier limit.
2414 let derived_name = idx.name.clone().unwrap_or_else(|| {
2415 let mut n = format!("{table}_{}_idx", idx.columns.join("_"));
2416 if n.len() > 63 {
2417 n.truncate(63);
2418 }
2419 n
2420 });
2421 let name = derived_name.as_str();
2422 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2423 let unique = idx.unique;
2424 // Map the parsed method string onto the IndexMethod enum
2425 // variant — kept at the codegen layer so the IR doesn't
2426 // carry the string form.
2427 let method_variant = match idx.method.as_str() {
2428 "gin" => quote!(#root::core::IndexMethod::Gin),
2429 "gist" => quote!(#root::core::IndexMethod::Gist),
2430 "brin" => quote!(#root::core::IndexMethod::Brin),
2431 "spgist" => quote!(#root::core::IndexMethod::SpGist),
2432 "hash" => quote!(#root::core::IndexMethod::Hash),
2433 "bloom" => quote!(#root::core::IndexMethod::Bloom),
2434 _ => quote!(#root::core::IndexMethod::BTree),
2435 };
2436 let where_clause = match &idx.where_clause {
2437 Some(s) => quote!(::core::option::Option::Some(#s)),
2438 None => quote!(::core::option::Option::None),
2439 };
2440 let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2441 quote! {
2442 #root::core::IndexSchema {
2443 name: #name,
2444 columns: &[ #(#cols),* ],
2445 unique: #unique,
2446 method: #method_variant,
2447 where_clause: #where_clause,
2448 include: &[ #(#include_lits),* ],
2449 }
2450 }
2451 });
2452 let checks_tokens = checks.iter().map(|c| {
2453 let name = c.name.as_str();
2454 let expr = c.expr.as_str();
2455 quote! {
2456 #root::core::CheckConstraint {
2457 name: #name,
2458 expr: #expr,
2459 }
2460 }
2461 });
2462 let excludes_tokens = excludes.iter().map(|e| {
2463 let name = e.name.as_str();
2464 let using = e.using.as_str();
2465 let element_tokens = e.elements.iter().map(|(col, op)| {
2466 let col_s = col.as_str();
2467 let op_s = op.as_str();
2468 quote!((#col_s, #op_s))
2469 });
2470 let where_tokens = match e.where_clause.as_deref() {
2471 Some(w) => quote!(::core::option::Option::Some(#w)),
2472 None => quote!(::core::option::Option::None),
2473 };
2474 quote! {
2475 #root::core::ExclusionConstraint {
2476 name: #name,
2477 using: #using,
2478 elements: &[ #(#element_tokens),* ],
2479 where_clause: #where_tokens,
2480 }
2481 }
2482 });
2483 let composite_fk_tokens = composite_fks.iter().map(|rel| {
2484 let name = rel.name.as_str();
2485 let to = rel.to.as_str();
2486 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2487 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2488 quote! {
2489 #root::core::CompositeFkRelation {
2490 name: #name,
2491 to: #to,
2492 from: &[ #(#from_cols),* ],
2493 on: &[ #(#on_cols),* ],
2494 }
2495 }
2496 });
2497 let generic_fk_tokens = generic_fks.iter().map(|rel| {
2498 let name = rel.name.as_str();
2499 let ct_col = rel.ct_column.as_str();
2500 let pk_col = rel.pk_column.as_str();
2501 quote! {
2502 #root::core::GenericRelation {
2503 name: #name,
2504 ct_column: #ct_col,
2505 pk_column: #pk_col,
2506 }
2507 }
2508 });
2509 // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2510 // no `#[rustango(default_order = "...")]` attribute was supplied.
2511 let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2512 let col_lit = col.as_str();
2513 quote! { (#col_lit, #desc) }
2514 });
2515
2516 // Issue #820 — `global_scopes` slice literal. Empty when no
2517 // `#[rustango(global_scope(...))]` attribute was supplied. The
2518 // `apply` path is re-emitted verbatim so it resolves in the
2519 // consumer's scope; the name is stored as a string literal.
2520 let global_scope_tokens = global_scopes.iter().map(|s| {
2521 let name = s.name.as_str();
2522 let apply = &s.apply;
2523 quote! {
2524 #root::core::GlobalScope {
2525 name: #name,
2526 apply: #apply,
2527 }
2528 }
2529 });
2530
2531 let m2m_tokens = m2m_relations.iter().map(|rel| {
2532 let name = rel.name.as_str();
2533 let to = rel.to.as_str();
2534 let through = rel.through.as_str();
2535 let src = rel.src.as_str();
2536 let dst = rel.dst.as_str();
2537 let auto_create = rel.auto_create;
2538 quote! {
2539 #root::core::M2MRelation {
2540 name: #name,
2541 to: #to,
2542 through: #through,
2543 src_col: #src,
2544 dst_col: #dst,
2545 auto_create: #auto_create,
2546 }
2547 }
2548 });
2549 // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2550 // when the model declares `#[rustango(reverse_has(...))]`. Each
2551 // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2552 // a const expression. Models without reverse_has fall through to
2553 // the trait's empty default — no override emitted.
2554 let reverse_relations_override = if reverse_has_relations.is_empty() {
2555 quote!()
2556 } else {
2557 let entries = reverse_has_relations.iter().map(|rel| {
2558 let name = rel.name.as_str();
2559 let child = &rel.child;
2560 let child_fk_column = rel.child_fk_column.as_str();
2561 let self_pk_column = rel.self_pk_column.as_str();
2562 quote! {
2563 #root::core::ReverseRelation {
2564 name: #name,
2565 child_schema: <#child as #root::core::Model>::SCHEMA,
2566 child_fk_column: #child_fk_column,
2567 self_pk_column: #self_pk_column,
2568 }
2569 }
2570 });
2571 quote! {
2572 fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2573 const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2574 RELS
2575 }
2576 }
2577 };
2578 // Issue #830 — `Model::generic_reverse_relations()` override for
2579 // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2580 let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2581 quote!()
2582 } else {
2583 let entries = generic_has_relations.iter().map(|rel| {
2584 let name = rel.name.as_str();
2585 let child = &rel.child;
2586 let ct_column = rel.ct_column.as_str();
2587 let pk_column = rel.pk_column.as_str();
2588 let self_pk_column = rel.self_pk_column.as_str();
2589 quote! {
2590 #root::core::GenericReverseRelation {
2591 name: #name,
2592 child_schema: <#child as #root::core::Model>::SCHEMA,
2593 ct_column: #ct_column,
2594 pk_column: #pk_column,
2595 self_pk_column: #self_pk_column,
2596 }
2597 }
2598 });
2599 quote! {
2600 fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2601 const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2602 RELS
2603 }
2604 }
2605 };
2606 quote! {
2607 impl #root::core::Model for #struct_name {
2608 const SCHEMA: &'static #root::core::ModelSchema = &#root::core::ModelSchema {
2609 name: #model_name,
2610 table: #table,
2611 fields: &[ #(#field_schemas),* ],
2612 display: #display_tokens,
2613 app_label: #app_label_tokens,
2614 admin: #admin_tokens,
2615 soft_delete_column: #soft_delete_tokens,
2616 permissions: #permissions,
2617 audit_track: #audit_track_tokens,
2618 m2m: &[ #(#m2m_tokens),* ],
2619 indexes: &[ #(#indexes_tokens),* ],
2620 check_constraints: &[ #(#checks_tokens),* ],
2621 exclusion_constraints: &[ #(#excludes_tokens),* ],
2622 composite_relations: &[ #(#composite_fk_tokens),* ],
2623 generic_relations: &[ #(#generic_fk_tokens),* ],
2624 scope: #scope_tokens,
2625 default_order: &[ #(#default_order_tokens),* ],
2626 is_view: #is_view,
2627 verbose_name: #verbose_name_tokens,
2628 verbose_name_plural: #verbose_name_plural_tokens,
2629 managed: #managed,
2630 base_manager_name: #base_manager_name_tokens,
2631 order_with_respect_to: #order_with_respect_to_tokens,
2632 proxy: #proxy,
2633 required_db_features: &[ #(#required_db_features_lits),* ],
2634 required_db_vendor: #required_db_vendor_tokens,
2635 default_related_name: #default_related_name_tokens,
2636 db_table_comment: #db_table_comment_tokens,
2637 get_latest_by: #get_latest_by_tokens,
2638 extra_permissions: &[ #(#extra_permission_tokens),* ],
2639 default_permissions: &[ #(#default_permission_tokens),* ],
2640 global_scopes: &[ #(#global_scope_tokens),* ],
2641 };
2642
2643 #reverse_relations_override
2644 #generic_reverse_relations_override
2645 }
2646 }
2647}
2648
2649/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2650/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2651/// otherwise a static reference to a populated `AdminConfig`.
2652fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2653 let root = rustango_root();
2654 let Some(admin) = admin else {
2655 return quote!(::core::option::Option::None);
2656 };
2657
2658 let list_display = admin
2659 .list_display
2660 .as_ref()
2661 .map(|(v, _)| v.as_slice())
2662 .unwrap_or(&[]);
2663 let list_display_lits = list_display.iter().map(|s| s.as_str());
2664
2665 let search_fields = admin
2666 .search_fields
2667 .as_ref()
2668 .map(|(v, _)| v.as_slice())
2669 .unwrap_or(&[]);
2670 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2671
2672 let readonly_fields = admin
2673 .readonly_fields
2674 .as_ref()
2675 .map(|(v, _)| v.as_slice())
2676 .unwrap_or(&[]);
2677 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2678
2679 let list_filter = admin
2680 .list_filter
2681 .as_ref()
2682 .map(|(v, _)| v.as_slice())
2683 .unwrap_or(&[]);
2684 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2685
2686 let actions = admin
2687 .actions
2688 .as_ref()
2689 .map(|(v, _)| v.as_slice())
2690 .unwrap_or(&[]);
2691 let actions_lits = actions.iter().map(|s| s.as_str());
2692
2693 let fieldsets = admin
2694 .fieldsets
2695 .as_ref()
2696 .map(|(v, _)| v.as_slice())
2697 .unwrap_or(&[]);
2698 let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
2699 let title = title.as_str();
2700 let field_lits = fields.iter().map(|s| s.as_str());
2701 quote!(#root::core::Fieldset {
2702 title: #title,
2703 fields: &[ #( #field_lits ),* ],
2704 })
2705 });
2706
2707 let list_display_links = admin
2708 .list_display_links
2709 .as_ref()
2710 .map(|(v, _)| v.as_slice())
2711 .unwrap_or(&[]);
2712 let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2713
2714 let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2715 let actions_on_top = admin.actions_on_top.unwrap_or(true);
2716 let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2717 let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2718
2719 let prepopulated = admin
2720 .prepopulated_fields
2721 .as_ref()
2722 .map(|(v, _)| v.as_slice())
2723 .unwrap_or(&[]);
2724 let prepopulated_tokens = prepopulated.iter().map(|(target, sources)| {
2725 let target = target.as_str();
2726 let source_lits = sources.iter().map(|s| s.as_str());
2727 quote!(#root::core::PrepopulatedField {
2728 target: #target,
2729 sources: &[ #( #source_lits ),* ],
2730 })
2731 });
2732
2733 let raw_id_fields = admin
2734 .raw_id_fields
2735 .as_ref()
2736 .map(|(v, _)| v.as_slice())
2737 .unwrap_or(&[]);
2738 let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2739
2740 let autocomplete_fields = admin
2741 .autocomplete_fields
2742 .as_ref()
2743 .map(|(v, _)| v.as_slice())
2744 .unwrap_or(&[]);
2745 let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
2746
2747 // #352 — list_select_related accepts "all" | "none" | "field, field, …".
2748 let list_select_related_tokens = match admin.list_select_related.as_deref() {
2749 None | Some("all") => quote!(#root::core::ListSelectRelated::All),
2750 Some("none") => quote!(#root::core::ListSelectRelated::None),
2751 Some(raw) => {
2752 let names: Vec<&str> = raw
2753 .split(',')
2754 .map(str::trim)
2755 .filter(|s| !s.is_empty())
2756 .collect();
2757 quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
2758 }
2759 };
2760
2761 // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
2762 // a Vec<(String, String)>. Empty / unset → no overrides.
2763 let formfield_pairs: Vec<(&str, &str)> = admin
2764 .formfield_overrides
2765 .as_ref()
2766 .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
2767 .unwrap_or_default();
2768 let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
2769 let field = *field;
2770 let widget = *widget;
2771 quote!((#field, #widget))
2772 });
2773
2774 let list_per_page = admin.list_per_page.unwrap_or(0);
2775
2776 let ordering_pairs = admin
2777 .ordering
2778 .as_ref()
2779 .map(|(v, _)| v.as_slice())
2780 .unwrap_or(&[]);
2781 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
2782 let name = name.as_str();
2783 let desc = *desc;
2784 quote!((#name, #desc))
2785 });
2786
2787 quote! {
2788 ::core::option::Option::Some(&#root::core::AdminConfig {
2789 list_display: &[ #( #list_display_lits ),* ],
2790 search_fields: &[ #( #search_fields_lits ),* ],
2791 list_per_page: #list_per_page,
2792 ordering: &[ #( #ordering_tokens ),* ],
2793 readonly_fields: &[ #( #readonly_fields_lits ),* ],
2794 list_filter: &[ #( #list_filter_lits ),* ],
2795 actions: &[ #( #actions_lits ),* ],
2796 fieldsets: &[ #( #fieldset_tokens ),* ],
2797 list_display_links: &[ #( #list_display_links_lits ),* ],
2798 search_help_text: #search_help_text,
2799 actions_on_top: #actions_on_top,
2800 actions_on_bottom: #actions_on_bottom,
2801 date_hierarchy: #date_hierarchy,
2802 prepopulated_fields: &[ #( #prepopulated_tokens ),* ],
2803 raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
2804 autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
2805 list_select_related: #list_select_related_tokens,
2806 formfield_overrides: &[ #( #formfield_tokens ),* ],
2807 })
2808 }
2809}
2810
2811fn inherent_impl_tokens(
2812 struct_name: &syn::Ident,
2813 fields: &CollectedFields,
2814 primary_key: Option<&(syn::Ident, String)>,
2815 column_consts: &TokenStream2,
2816 audited_fields: Option<&[&ColumnEntry]>,
2817 indexes: &[IndexAttr],
2818 manager_fns: &[syn::Ident],
2819) -> TokenStream2 {
2820 let root = rustango_root();
2821 // Audit-emit fragments threaded into write paths. Non-empty only
2822 // when the model carries `#[rustango(audit(...))]`. They reborrow
2823 // `_executor` (a `&mut PgConnection` for audited models — the
2824 // macro switches the signature below) so the data write and the
2825 // audit INSERT both run on the same caller-supplied connection.
2826 let executor_passes_to_data_write = if audited_fields.is_some() {
2827 quote!(&mut *_executor)
2828 } else {
2829 quote!(_executor)
2830 };
2831 let executor_param = if audited_fields.is_some() {
2832 quote!(_executor: &mut #root::sql::sqlx::PgConnection)
2833 } else {
2834 quote!(_executor: _E)
2835 };
2836 let executor_generics = if audited_fields.is_some() {
2837 quote!()
2838 } else {
2839 quote!(<'_c, _E>)
2840 };
2841 let executor_where = if audited_fields.is_some() {
2842 quote!()
2843 } else {
2844 quote! {
2845 where
2846 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
2847 }
2848 };
2849 // For audited models the `_on` methods take `&mut PgConnection`, so
2850 // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
2851 // must acquire a connection first. Non-audited models keep the
2852 // direct delegation since `&PgPool` IS an Executor.
2853 let pool_to_save_on = if audited_fields.is_some() {
2854 quote! {
2855 let mut _conn = pool.acquire().await?;
2856 self.save_on(&mut *_conn).await
2857 }
2858 } else {
2859 quote!(self.save_on(pool).await)
2860 };
2861 let pool_to_insert_on = if audited_fields.is_some() {
2862 quote! {
2863 let mut _conn = pool.acquire().await?;
2864 self.insert_on(&mut *_conn).await
2865 }
2866 } else {
2867 quote!(self.insert_on(pool).await)
2868 };
2869 let pool_to_delete_on = if audited_fields.is_some() {
2870 quote! {
2871 let mut _conn = pool.acquire().await?;
2872 self.delete_on(&mut *_conn).await
2873 }
2874 } else {
2875 quote!(self.delete_on(pool).await)
2876 };
2877 let pool_to_bulk_insert_on = if audited_fields.is_some() {
2878 quote! {
2879 let mut _conn = pool.acquire().await?;
2880 Self::bulk_insert_on(rows, &mut *_conn).await
2881 }
2882 } else {
2883 quote!(Self::bulk_insert_on(rows, pool).await)
2884 };
2885 // Pre-existing bug surfaced by batch 22's first audited Auto<T>
2886 // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
2887 // directly, but `upsert_on` for audited models takes
2888 // `&mut PgConnection` (the audit emit needs a real connection).
2889 // Add the missing acquire shim to keep audited Auto-PK upsert
2890 // compiling.
2891 let pool_to_upsert_on = if audited_fields.is_some() {
2892 quote! {
2893 let mut _conn = pool.acquire().await?;
2894 self.upsert_on(&mut *_conn).await
2895 }
2896 } else {
2897 quote!(self.upsert_on(pool).await)
2898 };
2899
2900 // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
2901 // (audit-on-connection over &Pool needs a bi-dialect transaction
2902 // helper, deferred). Two body shapes:
2903 // - has_auto: build InsertQuery skipping Auto::Unset columns,
2904 // request Auto cols in `returning`, dispatch via
2905 // `insert_returning_pool`, then on the returned `PgRow` /
2906 // `MySqlAutoId(id)` enum — pull each Auto field from the PG
2907 // row OR drop the single i64 into the first Auto field on MySQL
2908 // (multi-Auto models on MySQL error at runtime since
2909 // `LAST_INSERT_ID()` only reports one)
2910 // - non-Auto: build InsertQuery with explicit columns/values and
2911 // call `insert_pool` (no returning needed)
2912 // pool_insert_method body for the audited Auto-PK case is moved
2913 // to after audit_pair_tokens / audit_pk_to_string (they live
2914 // ~150 lines below). This block keeps the non-audited and
2915 // non-Auto branches in place — the audited Auto-PK arm is
2916 // computed below and merged via the dispatch helper variable.
2917 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
2918 // Audited models with explicit (non-Auto) PKs go through
2919 // the non-Auto insert path below — the audit emit is one
2920 // round-trip after the INSERT inside the same tx via
2921 // audit::save_one_with_audit? No, INSERT semantics
2922 // differ. For non-Auto PK + audited, route through a
2923 // dedicated insert + audit emit on the same tx, but defer
2924 // the macro emission to the audit-bundle-aware block below
2925 // — this `quote!()` placeholder gets overwritten there.
2926 quote!()
2927 } else if audited_fields.is_some() && fields.has_auto {
2928 // Audited Auto-PK insert_pool — assembled after the audit
2929 // bundles. Placeholder; real emission below.
2930 quote!()
2931 } else if fields.has_auto {
2932 let pushes = &fields.insert_pushes;
2933 let returning_cols = &fields.returning_cols;
2934 // When every `Auto<T>` field is filled Rust-side
2935 // (`default_uuid_v7`, issue #823), there is no column to read
2936 // back from the database — `returning_cols` is empty. Route
2937 // through plain `insert_pool` instead of
2938 // `insert_returning_pool` to skip the redundant RETURNING /
2939 // LAST_INSERT_ID round-trip.
2940 if fields.returning_cols.is_empty() {
2941 quote! {
2942 /// Insert this row against either backend. Every
2943 /// `Auto<T>` PK on this model is filled Rust-side
2944 /// (e.g. `default_uuid_v7`) before binding, so no
2945 /// RETURNING round-trip is needed.
2946 ///
2947 /// # Errors
2948 /// As [`Self::insert`].
2949 pub async fn insert_pool(
2950 &mut self,
2951 pool: &#root::sql::Pool,
2952 ) -> ::core::result::Result<(), #root::sql::ExecError> {
2953 let mut _columns: ::std::vec::Vec<&'static str> =
2954 ::std::vec::Vec::new();
2955 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2956 ::std::vec::Vec::new();
2957 #( #pushes )*
2958 let _query = #root::core::InsertQuery {
2959 model: <Self as #root::core::Model>::SCHEMA,
2960 columns: _columns,
2961 values: _values,
2962 returning: ::std::vec::Vec::new(),
2963 on_conflict: ::core::option::Option::None,
2964 };
2965 #root::sql::insert_pool(pool, &_query).await
2966 }
2967
2968 /// Eloquent `Model::insertOrIgnore()` — same shape
2969 /// as the auto-PK branch above. Returns `Ok(true)`
2970 /// when inserted, `Ok(false)` when a conflict caused
2971 /// the INSERT to silently skip.
2972 ///
2973 /// # Errors
2974 /// As [`Self::insert`].
2975 pub async fn insert_or_ignore(
2976 &mut self,
2977 pool: &#root::sql::Pool,
2978 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
2979 let mut _columns: ::std::vec::Vec<&'static str> =
2980 ::std::vec::Vec::new();
2981 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2982 ::std::vec::Vec::new();
2983 #( #pushes )*
2984 let _query = #root::core::InsertQuery {
2985 model: <Self as #root::core::Model>::SCHEMA,
2986 columns: _columns,
2987 values: _values,
2988 returning: ::std::vec::Vec::new(),
2989 on_conflict: ::core::option::Option::Some(
2990 #root::core::ConflictClause::DoNothing,
2991 ),
2992 };
2993 let dialect = pool.dialect();
2994 let stmt = dialect.compile_insert(&_query)?;
2995 let rows = #root::sql::raw_execute_pool(
2996 pool, &stmt.sql, stmt.params,
2997 ).await?;
2998 ::core::result::Result::Ok(rows > 0)
2999 }
3000 }
3001 } else {
3002 quote! {
3003 /// Insert this row against either backend, populating any
3004 /// `Auto<T>` PK from the auto-assigned value.
3005 ///
3006 /// # Errors
3007 /// As [`Self::insert`].
3008 pub async fn insert_pool(
3009 &mut self,
3010 pool: &#root::sql::Pool,
3011 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3012 let mut _columns: ::std::vec::Vec<&'static str> =
3013 ::std::vec::Vec::new();
3014 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3015 ::std::vec::Vec::new();
3016 #( #pushes )*
3017 let _query = #root::core::InsertQuery {
3018 model: <Self as #root::core::Model>::SCHEMA,
3019 columns: _columns,
3020 values: _values,
3021 returning: ::std::vec![ #( #returning_cols ),* ],
3022 on_conflict: ::core::option::Option::None,
3023 };
3024 let _result = #root::sql::insert_returning_pool(
3025 pool, &_query,
3026 ).await?;
3027 #root::sql::apply_auto_pk(_result, self)
3028 }
3029
3030 /// Eloquent `Model::insertOrIgnore()` — INSERT this
3031 /// row or silently skip on unique-constraint
3032 /// violation. Returns `Ok(true)` when a row was
3033 /// inserted, `Ok(false)` when a conflict caused the
3034 /// INSERT to silently skip.
3035 ///
3036 /// **Caveat on auto-PK models**: when the row is
3037 /// skipped (conflict), this instance's `Auto<T>`
3038 /// fields stay `Unset` — no PK is back-populated
3039 /// because the server didn't auto-assign one. For
3040 /// "insert then read back the PK or the existing
3041 /// row's PK", use the `upsert` family or
3042 /// `get_or_create`.
3043 ///
3044 /// # Errors
3045 /// As [`Self::insert`].
3046 pub async fn insert_or_ignore(
3047 &mut self,
3048 pool: &#root::sql::Pool,
3049 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3050 let mut _columns: ::std::vec::Vec<&'static str> =
3051 ::std::vec::Vec::new();
3052 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3053 ::std::vec::Vec::new();
3054 #( #pushes )*
3055 let _query = #root::core::InsertQuery {
3056 model: <Self as #root::core::Model>::SCHEMA,
3057 columns: _columns,
3058 values: _values,
3059 returning: ::std::vec::Vec::new(),
3060 on_conflict: ::core::option::Option::Some(
3061 #root::core::ConflictClause::DoNothing,
3062 ),
3063 };
3064 let dialect = pool.dialect();
3065 let stmt = dialect.compile_insert(&_query)?;
3066 let rows = #root::sql::raw_execute_pool(
3067 pool, &stmt.sql, stmt.params,
3068 ).await?;
3069 ::core::result::Result::Ok(rows > 0)
3070 }
3071 }
3072 }
3073 } else {
3074 let insert_columns = &fields.insert_columns;
3075 let insert_values = &fields.insert_values;
3076 quote! {
3077 /// Insert this row into its table against either backend.
3078 /// Equivalent to [`Self::insert`] but takes
3079 /// [`#root::sql::Pool`].
3080 ///
3081 /// # Errors
3082 /// As [`Self::insert`].
3083 pub async fn insert_pool(
3084 &self,
3085 pool: &#root::sql::Pool,
3086 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3087 let _query = #root::core::InsertQuery {
3088 model: <Self as #root::core::Model>::SCHEMA,
3089 columns: ::std::vec![ #( #insert_columns ),* ],
3090 values: ::std::vec![ #( #insert_values ),* ],
3091 returning: ::std::vec::Vec::new(),
3092 on_conflict: ::core::option::Option::None,
3093 };
3094 #root::sql::insert_pool(pool, &_query).await
3095 }
3096
3097 /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3098 /// or silently skip on unique-constraint violation. Maps
3099 /// to per-dialect "INSERT ... DO NOTHING on conflict":
3100 /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3101 /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL
3102 /// `INSERT IGNORE INTO …`.
3103 ///
3104 /// Returns `Ok(true)` when a row was inserted,
3105 /// `Ok(false)` when a conflict caused the INSERT to
3106 /// silently skip.
3107 ///
3108 /// Use for "create-if-absent" patterns where you don't
3109 /// need the row back. For "find-or-create with the row
3110 /// returned", use the queryset-level
3111 /// `crate::sql::get_or_create` free function.
3112 ///
3113 /// # Errors
3114 /// As [`Self::insert`], plus any dialect-specific
3115 /// translation error from the ConflictClause writer.
3116 pub async fn insert_or_ignore(
3117 &self,
3118 pool: &#root::sql::Pool,
3119 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3120 let _query = #root::core::InsertQuery {
3121 model: <Self as #root::core::Model>::SCHEMA,
3122 columns: ::std::vec![ #( #insert_columns ),* ],
3123 values: ::std::vec![ #( #insert_values ),* ],
3124 returning: ::std::vec::Vec::new(),
3125 on_conflict: ::core::option::Option::Some(
3126 #root::core::ConflictClause::DoNothing,
3127 ),
3128 };
3129 let dialect = pool.dialect();
3130 let stmt = dialect.compile_insert(&_query)?;
3131 let rows = #root::sql::raw_execute_pool(pool, &stmt.sql, stmt.params).await?;
3132 ::core::result::Result::Ok(rows > 0)
3133 }
3134 }
3135 };
3136
3137 // pool_save_method moved to after audit_pair_tokens /
3138 // audit_pk_to_string (they live ~70 lines below) — needed for
3139 // the audited branch which builds an UpdateQuery + PendingEntry
3140 // and dispatches via audit::save_one_with_audit.
3141
3142 // pool_delete_method moved to after audit_pair_tokens / audit_pk_to_string
3143 // are computed (they live ~80 lines below).
3144
3145 // Build the (column, JSON value) pair list used by every
3146 // snapshot-style audit emission. Reused across delete_on,
3147 // soft_delete_on, restore_on, and (later) bulk paths. Empty
3148 // when the model isn't audited.
3149 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3150 .map(|tracked| {
3151 tracked
3152 .iter()
3153 .map(|c| {
3154 let column_lit = c.column.as_str();
3155 let ident = &c.ident;
3156 quote! {
3157 (
3158 #column_lit,
3159 #root::__serde_json::to_value(&self.#ident)
3160 .unwrap_or(#root::__serde_json::Value::Null),
3161 )
3162 }
3163 })
3164 .collect()
3165 })
3166 .unwrap_or_default();
3167 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3168 if fields.pk_is_auto {
3169 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3170 } else {
3171 quote!(::std::format!("{}", &self.#pk_ident))
3172 }
3173 } else {
3174 quote!(::std::string::String::new())
3175 };
3176 let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
3177 if audited_fields.is_some() {
3178 let pairs = audit_pair_tokens.iter();
3179 let pk_str = audit_pk_to_string.clone();
3180 quote! {
3181 let _audit_entry = #root::audit::PendingEntry {
3182 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3183 entity_pk: #pk_str,
3184 operation: #op_path,
3185 source: #root::audit::current_source(),
3186 changes: #root::audit::snapshot_changes(&[
3187 #( #pairs ),*
3188 ]),
3189 };
3190 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3191 }
3192 } else {
3193 quote!()
3194 }
3195 };
3196 let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create));
3197 let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete));
3198 let audit_softdelete_emit = make_op_emit(quote!(#root::audit::AuditOp::SoftDelete));
3199 let audit_restore_emit = make_op_emit(quote!(#root::audit::AuditOp::Restore));
3200
3201 // `save_pool(&Pool)` — emitted for every model with a PK.
3202 // Audited Auto-PK models are deferred (the Auto::Unset →
3203 // insert_pool path needs the audited-insert flow from a future
3204 // batch). Three body shapes:
3205 // - non-audited, plain PK: build UpdateQuery + dispatch through
3206 // sql::update_pool
3207 // - non-audited, Auto-PK: same, but Auto::Unset routes to
3208 // self.insert_pool which already handles RETURNING / LAST_INSERT_ID
3209 // - audited, plain PK: build UpdateQuery + PendingEntry, dispatch
3210 // through audit::save_one_with_audit (per-backend tx wraps
3211 // UPDATE + audit emit atomically). Snapshot-style audit (post-
3212 // write field values) — diff-style audit (with pre-UPDATE
3213 // SELECT for `before` values) needs per-tracked-column codegen
3214 // that doesn't fit the runtime-helper pattern; legacy &PgPool
3215 // `save` keeps the diff for now.
3216 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3217 let pk_column_lit = pk_col.as_str();
3218 let assignments = &fields.update_assignments;
3219 if audited_fields.is_some() {
3220 if fields.pk_is_auto {
3221 // Auto-PK + audited: defer. The Auto::Unset insert
3222 // path needs a transactional INSERT + LAST_INSERT_ID
3223 // + audit emit flow — that's a follow-up batch.
3224 quote!()
3225 } else {
3226 let pairs = audit_pair_tokens.iter();
3227 let pairs2 = audit_pair_tokens.iter();
3228 let pk_str = audit_pk_to_string.clone();
3229 let pk_str2 = audit_pk_to_string.clone();
3230 quote! {
3231 /// Save (UPDATE) this row against either backend
3232 /// with audit emission inside the same transaction.
3233 /// Bi-dialect counterpart of [`Self::save`] for
3234 /// audited models with non-`Auto<T>` PKs.
3235 ///
3236 /// Captures **post-write** field state (snapshot
3237 /// audit). The legacy &PgPool [`Self::save`]
3238 /// captures BEFORE+AFTER for true diff audit;
3239 /// porting that to the &Pool path needs runtime
3240 /// per-tracked-column decoding and is deferred.
3241 ///
3242 /// # Errors
3243 /// As [`Self::save`].
3244 pub async fn save_pool(
3245 &mut self,
3246 pool: &#root::sql::Pool,
3247 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3248 let _query = #root::core::UpdateQuery {
3249 model: <Self as #root::core::Model>::SCHEMA,
3250 set: ::std::vec![ #( #assignments ),* ],
3251 where_clause: #root::core::WhereExpr::Predicate(
3252 #root::core::Filter {
3253 column: #pk_column_lit,
3254 op: #root::core::Op::Eq,
3255 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3256 ::core::clone::Clone::clone(&self.#pk_ident)
3257 ),
3258 }
3259 ),
3260 };
3261 let _audit_entry = #root::audit::PendingEntry {
3262 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3263 entity_pk: #pk_str,
3264 operation: #root::audit::AuditOp::Update,
3265 source: #root::audit::current_source(),
3266 changes: #root::audit::snapshot_changes(&[
3267 #( #pairs ),*
3268 ]),
3269 };
3270 let _affected = #root::audit::save_one_with_audit(
3271 pool, &_query, &_audit_entry,
3272 ).await?;
3273 ::core::result::Result::Ok(_affected)
3274 }
3275
3276 /// `save_pool` narrowed to a Rust-field allowlist — issue #66
3277 /// (Django `Model.save(update_fields=[...])`).
3278 /// Audit emission shrinks to the same column set so
3279 /// the audit log reflects exactly what was written.
3280 ///
3281 /// # Errors
3282 /// As [`Self::save_pool`], plus
3283 /// [`#root::core::QueryError::UnknownField`] wrapped
3284 /// in `ExecError::Query` for unknown field names.
3285 pub async fn save_partial(
3286 &mut self,
3287 fields: &[&str],
3288 pool: &#root::sql::Pool,
3289 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3290 if fields.is_empty() {
3291 #root::__tracing::warn!(
3292 target: "rustango::save_partial",
3293 model = <Self as #root::core::Model>::SCHEMA.name,
3294 "save_partial called with empty field list — no-op"
3295 );
3296 return ::core::result::Result::Ok(0);
3297 }
3298 let _schema = <Self as #root::core::Model>::SCHEMA;
3299 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3300 ::std::collections::HashSet::with_capacity(fields.len());
3301 for f in fields {
3302 match _schema.field(f) {
3303 ::core::option::Option::Some(fs) => {
3304 _wanted_cols.insert(fs.column);
3305 }
3306 ::core::option::Option::None => {
3307 return ::core::result::Result::Err(
3308 #root::sql::ExecError::Query(
3309 #root::core::QueryError::UnknownField {
3310 model: _schema.name,
3311 field: (*f).to_owned(),
3312 }
3313 )
3314 );
3315 }
3316 }
3317 }
3318 let _full: ::std::vec::Vec<#root::core::Assignment> =
3319 ::std::vec![ #( #assignments ),* ];
3320 let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3321 .into_iter()
3322 .filter(|a| _wanted_cols.contains(a.column))
3323 .collect();
3324 if _filtered.is_empty() {
3325 #root::__tracing::warn!(
3326 target: "rustango::save_partial",
3327 model = _schema.name,
3328 "save_partial: every named field maps to a non-assignable column — no-op"
3329 );
3330 return ::core::result::Result::Ok(0);
3331 }
3332 let _query = #root::core::UpdateQuery {
3333 model: _schema,
3334 set: _filtered,
3335 where_clause: #root::core::WhereExpr::Predicate(
3336 #root::core::Filter {
3337 column: #pk_column_lit,
3338 op: #root::core::Op::Eq,
3339 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3340 ::core::clone::Clone::clone(&self.#pk_ident)
3341 ),
3342 }
3343 ),
3344 };
3345 // Narrow the audit snapshot to the same column set.
3346 let _all_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3347 ::std::vec![ #( #pairs2 ),* ];
3348 let _narrowed: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3349 _all_pairs
3350 .into_iter()
3351 .filter(|(col, _)| _wanted_cols.contains(col))
3352 .collect();
3353 let _audit_entry = #root::audit::PendingEntry {
3354 entity_table: _schema.table,
3355 entity_pk: #pk_str2,
3356 operation: #root::audit::AuditOp::Update,
3357 source: #root::audit::current_source(),
3358 changes: #root::audit::snapshot_changes(&_narrowed),
3359 };
3360 let _affected = #root::audit::save_one_with_audit(
3361 pool, &_query, &_audit_entry,
3362 ).await?;
3363 ::core::result::Result::Ok(_affected)
3364 }
3365
3366 /// Typed-column counterpart of [`Self::save_partial`] —
3367 /// issue #67. `fields` is a tuple of [`Column`]
3368 /// constants whose `Model` matches `Self`; typos and
3369 /// model mismatches surface at *compile time*
3370 /// (`Author::name` inside a `Post::save_partial_typed`
3371 /// call is a type error, no runtime check).
3372 ///
3373 /// ```ignore
3374 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3375 /// ```
3376 ///
3377 /// Lowers to [`Self::save_partial`] under the hood;
3378 /// audit narrowing + every other semantic is identical.
3379 ///
3380 /// [`Column`]: #root::core::Column
3381 ///
3382 /// # Errors
3383 /// As [`Self::save_partial`].
3384 pub async fn save_partial_typed<
3385 L: #root::core::TypedFieldList<Self>,
3386 >(
3387 &mut self,
3388 fields: L,
3389 pool: &#root::sql::Pool,
3390 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3391 let _names = fields.rust_field_names();
3392 let _refs: ::std::vec::Vec<&str> =
3393 _names.iter().copied().collect();
3394 self.save_partial(&_refs, pool).await
3395 }
3396 }
3397 }
3398 } else {
3399 let dispatch_unset = if fields.pk_is_auto {
3400 quote! {
3401 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3402 return self.insert_pool(pool).await.map(|()| 1u64);
3403 }
3404 }
3405 } else {
3406 quote!()
3407 };
3408 quote! {
3409 /// Save this row to its table against either backend.
3410 /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3411 /// `UPDATE` keyed on the PK.
3412 ///
3413 /// # Errors
3414 /// As [`Self::save`].
3415 pub async fn save_pool(
3416 &mut self,
3417 pool: &#root::sql::Pool,
3418 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3419 #dispatch_unset
3420 let _query = #root::core::UpdateQuery {
3421 model: <Self as #root::core::Model>::SCHEMA,
3422 set: ::std::vec![ #( #assignments ),* ],
3423 where_clause: #root::core::WhereExpr::Predicate(
3424 #root::core::Filter {
3425 column: #pk_column_lit,
3426 op: #root::core::Op::Eq,
3427 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3428 ::core::clone::Clone::clone(&self.#pk_ident)
3429 ),
3430 }
3431 ),
3432 };
3433 let _affected = #root::sql::update_pool(pool, &_query).await?;
3434 ::core::result::Result::Ok(_affected)
3435 }
3436
3437 /// Save (UPDATE) only the listed Rust-side fields,
3438 /// leaving every other column untouched. Issue #66 —
3439 /// Django's `Model.save(update_fields=[...])` shape.
3440 ///
3441 /// `fields` are Rust-side struct field names; the macro
3442 /// resolves each to its SQL column. Unknown field
3443 /// names return [`#root::core::QueryError::UnknownField`]
3444 /// wrapped in `ExecError::Query`. An empty list is a
3445 /// no-op (returns `Ok(())` and logs a `tracing::warn!`),
3446 /// matching Django's "nothing to do" semantic.
3447 ///
3448 /// Use this when:
3449 /// * you only mutated a couple of fields on a wide row
3450 /// (avoid re-writing every column on every save), or
3451 /// * two writers diverged after their initial read and
3452 /// you want to preserve the other writer's changes to
3453 /// columns you didn't touch.
3454 ///
3455 /// Auto-PK models with an unset PK return
3456 /// [`#root::core::QueryError::UnknownField`] with
3457 /// field name `<pk>` — `save_partial` is an
3458 /// UPDATE-only path. Call [`Self::insert_pool`]
3459 /// (or [`Self::save_pool`] which dispatches based on
3460 /// PK state) for the INSERT case.
3461 ///
3462 /// # Errors
3463 /// As [`Self::save_pool`], plus `UnknownField` for
3464 /// unknown / empty / Auto-Unset cases.
3465 pub async fn save_partial(
3466 &mut self,
3467 fields: &[&str],
3468 pool: &#root::sql::Pool,
3469 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3470 if fields.is_empty() {
3471 #root::__tracing::warn!(
3472 target: "rustango::save_partial",
3473 model = <Self as #root::core::Model>::SCHEMA.name,
3474 "save_partial called with empty field list — no-op"
3475 );
3476 return ::core::result::Result::Ok(0);
3477 }
3478 let _schema = <Self as #root::core::Model>::SCHEMA;
3479 // Validate field names against the schema.
3480 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3481 ::std::collections::HashSet::with_capacity(fields.len());
3482 for f in fields {
3483 match _schema.field(f) {
3484 ::core::option::Option::Some(fs) => {
3485 _wanted_cols.insert(fs.column);
3486 }
3487 ::core::option::Option::None => {
3488 return ::core::result::Result::Err(
3489 #root::sql::ExecError::Query(
3490 #root::core::QueryError::UnknownField {
3491 model: _schema.name,
3492 field: (*f).to_owned(),
3493 }
3494 )
3495 );
3496 }
3497 }
3498 }
3499 // Build the full assignment vec, then keep only the
3500 // assignments whose column is in `_wanted_cols`.
3501 let _full: ::std::vec::Vec<#root::core::Assignment> =
3502 ::std::vec![ #( #assignments ),* ];
3503 let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3504 .into_iter()
3505 .filter(|a| _wanted_cols.contains(a.column))
3506 .collect();
3507 if _filtered.is_empty() {
3508 // All field names valid, but they all map to
3509 // non-assignable slots (PK column, computed/
3510 // virtual fields, relations without an
3511 // assignment). Same no-op semantic as Django.
3512 #root::__tracing::warn!(
3513 target: "rustango::save_partial",
3514 model = _schema.name,
3515 "save_partial: every named field maps to a non-assignable column — no-op"
3516 );
3517 return ::core::result::Result::Ok(0);
3518 }
3519 let _query = #root::core::UpdateQuery {
3520 model: _schema,
3521 set: _filtered,
3522 where_clause: #root::core::WhereExpr::Predicate(
3523 #root::core::Filter {
3524 column: #pk_column_lit,
3525 op: #root::core::Op::Eq,
3526 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3527 ::core::clone::Clone::clone(&self.#pk_ident)
3528 ),
3529 }
3530 ),
3531 };
3532 let _affected = #root::sql::update_pool(pool, &_query).await?;
3533 ::core::result::Result::Ok(_affected)
3534 }
3535
3536 /// Typed-column counterpart of [`Self::save_partial`] —
3537 /// issue #67. `fields` is a tuple of [`Column`]
3538 /// constants whose `Model` matches `Self`; typos and
3539 /// model mismatches surface at *compile time*
3540 /// (`Author::name` inside a `Post::save_partial_typed`
3541 /// call is a type error, no runtime check).
3542 ///
3543 /// ```ignore
3544 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3545 /// ```
3546 ///
3547 /// Lowers to [`Self::save_partial`] under the hood — the
3548 /// tuple is reduced to a `&[&str]` slice of Rust-side
3549 /// field names and forwarded.
3550 ///
3551 /// [`Column`]: #root::core::Column
3552 ///
3553 /// # Errors
3554 /// As [`Self::save_partial`].
3555 pub async fn save_partial_typed<
3556 L: #root::core::TypedFieldList<Self>,
3557 >(
3558 &mut self,
3559 fields: L,
3560 pool: &#root::sql::Pool,
3561 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3562 let _names = fields.rust_field_names();
3563 let _refs: ::std::vec::Vec<&str> =
3564 _names.iter().copied().collect();
3565 self.save_partial(&_refs, pool).await
3566 }
3567 }
3568 }
3569 } else {
3570 quote!()
3571 };
3572
3573 // Audited `insert_pool` (overrides the placeholder set higher up
3574 // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3575 // audited models get insert_pool routing through
3576 // audit::insert_one_with_audit (per-backend tx wraps INSERT
3577 // + auto-PK readback + audit emit). Snapshot-style audit (the
3578 // PendingEntry's `changes` carries post-write field values).
3579 let pool_insert_method = if audited_fields.is_some() {
3580 if let Some(_) = primary_key {
3581 let pushes = if fields.has_auto {
3582 fields.insert_pushes.clone()
3583 } else {
3584 // For non-Auto-PK models, the macro normally builds
3585 // {columns, values} from fields.insert_columns +
3586 // fields.insert_values rather than insert_pushes.
3587 // Map those into the pushes shape.
3588 fields
3589 .insert_columns
3590 .iter()
3591 .zip(&fields.insert_values)
3592 .map(|(col, val)| {
3593 quote! {
3594 _columns.push(#col);
3595 _values.push(#val);
3596 }
3597 })
3598 .collect()
3599 };
3600 let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
3601 fields.returning_cols.clone()
3602 } else {
3603 // Non-Auto-PK: still need RETURNING something for the
3604 // audit helper's contract (it errors on empty
3605 // returning). Return the PK column so the audit row
3606 // can carry the assigned PK back. Some non-Auto PKs
3607 // are server-side-default (e.g. UUIDv4 default), so
3608 // RETURNING is genuinely useful.
3609 primary_key
3610 .map(|(_, col)| {
3611 let lit = col.as_str();
3612 vec![quote!(#lit)]
3613 })
3614 .unwrap_or_default()
3615 };
3616 let pairs = audit_pair_tokens.iter();
3617 let pk_str = audit_pk_to_string.clone();
3618 quote! {
3619 /// Insert this row against either backend with audit
3620 /// emission inside the same transaction. Bi-dialect
3621 /// counterpart of [`Self::insert`] for audited models.
3622 ///
3623 /// Snapshot-style audit (post-write field values).
3624 ///
3625 /// # Errors
3626 /// As [`Self::insert`].
3627 pub async fn insert_pool(
3628 &mut self,
3629 pool: &#root::sql::Pool,
3630 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3631 let mut _columns: ::std::vec::Vec<&'static str> =
3632 ::std::vec::Vec::new();
3633 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3634 ::std::vec::Vec::new();
3635 #( #pushes )*
3636 let _query = #root::core::InsertQuery {
3637 model: <Self as #root::core::Model>::SCHEMA,
3638 columns: _columns,
3639 values: _values,
3640 returning: ::std::vec![ #( #returning_cols ),* ],
3641 on_conflict: ::core::option::Option::None,
3642 };
3643 let _audit_entry = #root::audit::PendingEntry {
3644 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3645 entity_pk: #pk_str,
3646 operation: #root::audit::AuditOp::Create,
3647 source: #root::audit::current_source(),
3648 changes: #root::audit::snapshot_changes(&[
3649 #( #pairs ),*
3650 ]),
3651 };
3652 let _result = #root::audit::insert_one_with_audit(
3653 pool, &_query, &_audit_entry,
3654 ).await?;
3655 #root::sql::apply_auto_pk(_result, self)
3656 }
3657 }
3658 } else {
3659 quote!()
3660 }
3661 } else {
3662 // Keep the non-audited pool_insert_method we built earlier.
3663 pool_insert_method
3664 };
3665
3666 // Update audited save_pool: now that insert_pool is wired for
3667 // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3668 // insert_pool. Non-audited save_pool already does this.
3669 // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3670 // Replaces the snapshot-only emission with a per-backend transaction
3671 // body that:
3672 // 1. SELECTs the tracked columns by PK (typed Row::try_get per
3673 // column), capturing BEFORE values
3674 // 2. compiles the UPDATE via pool.dialect() and runs it on the tx
3675 // 3. builds AFTER pairs from &self
3676 // 4. diffs BEFORE/AFTER, emits one PendingEntry with
3677 // AuditOp::Update + diff_changes(...) on the same tx connection
3678 // 5. commits
3679 //
3680 // Per-backend arms inline the SQL string + placeholder shape, then
3681 // share the `audit_before_pair_tokens` decoder block (Row::try_get
3682 // is polymorphic over Row type — the same tokens work against
3683 // PgRow and MySqlRow as long as the field's Rust type implements
3684 // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3685 // primitives + chrono/uuid/serde_json::Value all do).
3686 let pool_save_method = if let Some(tracked) = audited_fields {
3687 if let Some((pk_ident, pk_col)) = primary_key {
3688 let pk_column_lit = pk_col.as_str();
3689 // Two iterators — quote!'s `#(#var)*` consumes the
3690 // iterator, and we need to splice the same after-pairs
3691 // sequence into both per-backend arms.
3692 let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
3693 let pk_str = audit_pk_to_string.clone();
3694 // Per-tracked-column BEFORE-pair token list. Each entry
3695 // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3696 // The Row alias resolves to PgRow / MySqlRow per call site,
3697 // so the same template generates both the PG and MySQL bodies.
3698 let mk_before_pairs =
3699 |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
3700 tracked
3701 .iter()
3702 .map(|c| {
3703 let column_lit = c.column.as_str();
3704 let value_ty = &c.value_ty;
3705 quote! {
3706 (
3707 #column_lit,
3708 match #getter::<#value_ty>(
3709 _audit_before_row, #column_lit,
3710 ) {
3711 ::core::result::Result::Ok(v) => {
3712 #root::__serde_json::to_value(&v)
3713 .unwrap_or(#root::__serde_json::Value::Null)
3714 }
3715 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3716 },
3717 )
3718 }
3719 })
3720 .collect()
3721 };
3722 let before_pairs_pg: Vec<proc_macro2::TokenStream> =
3723 mk_before_pairs(quote!(#root::sql::try_get_returning));
3724 let before_pairs_my: Vec<proc_macro2::TokenStream> =
3725 mk_before_pairs(quote!(#root::sql::try_get_returning_my));
3726 let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
3727 mk_before_pairs(quote!(#root::sql::try_get_returning_sqlite));
3728 let pg_select_cols: String = tracked
3729 .iter()
3730 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
3731 .collect::<Vec<_>>()
3732 .join(", ");
3733 let my_select_cols: String = tracked
3734 .iter()
3735 .map(|c| format!("`{}`", c.column.replace('`', "``")))
3736 .collect::<Vec<_>>()
3737 .join(", ");
3738 // SQLite uses double-quote identifier quoting (same as
3739 // Postgres in default config), so the column-list shape
3740 // matches PG.
3741 let sqlite_select_cols: String = pg_select_cols.clone();
3742 let pk_value_for_bind = if fields.pk_is_auto {
3743 quote!(self.#pk_ident.get().copied().unwrap_or_default())
3744 } else {
3745 quote!(::core::clone::Clone::clone(&self.#pk_ident))
3746 };
3747 let assignments = &fields.update_assignments;
3748 let unset_dispatch = if fields.has_auto {
3749 quote! {
3750 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3751 return self.insert_pool(pool).await.map(|()| 1u64);
3752 }
3753 }
3754 } else {
3755 quote!()
3756 };
3757 quote! {
3758 /// Save this row against either backend with audit
3759 /// emission (diff-style: BEFORE+AFTER) inside the
3760 /// same transaction. Auto::Unset PK routes to
3761 /// insert_pool. Bi-dialect counterpart of
3762 /// [`Self::save`] for audited models.
3763 ///
3764 /// The audit row's `changes` JSON contains one
3765 /// `{ "field": { "before": …, "after": … } }` entry
3766 /// per tracked column whose value actually changed
3767 /// — same shape as the existing &PgPool save() emits.
3768 ///
3769 /// # Errors
3770 /// As [`Self::save`].
3771 pub async fn save_pool(
3772 &mut self,
3773 pool: &#root::sql::Pool,
3774 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3775 #unset_dispatch
3776 let _query = #root::core::UpdateQuery {
3777 model: <Self as #root::core::Model>::SCHEMA,
3778 set: ::std::vec![ #( #assignments ),* ],
3779 where_clause: #root::core::WhereExpr::Predicate(
3780 #root::core::Filter {
3781 column: #pk_column_lit,
3782 op: #root::core::Op::Eq,
3783 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3784 ::core::clone::Clone::clone(&self.#pk_ident)
3785 ),
3786 }
3787 ),
3788 };
3789 let _after_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3790 ::std::vec![ #( #after_pairs_pg ),* ];
3791 #root::audit::save_one_with_diff(
3792 pool,
3793 &_query,
3794 #pk_column_lit,
3795 ::core::convert::Into::<#root::core::SqlValue>::into(
3796 #pk_value_for_bind,
3797 ),
3798 <Self as #root::core::Model>::SCHEMA.table,
3799 #pk_str,
3800 _after_pairs,
3801 #pg_select_cols,
3802 #my_select_cols,
3803 #sqlite_select_cols,
3804 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
3805 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
3806 |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
3807 ).await
3808 }
3809 }
3810 } else {
3811 quote!()
3812 }
3813 } else {
3814 pool_save_method
3815 };
3816
3817 // `delete_pool(&Pool)` — emitted for every model with a PK. Two
3818 // body shapes:
3819 // - non-audited: simple dispatch through `sql::delete_pool`
3820 // - audited: routes through `audit::delete_one_with_audit`,
3821 // which opens a per-backend transaction wrapping DELETE +
3822 // audit emit so the data write and audit row commit atomically.
3823 let pool_delete_method = {
3824 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
3825 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
3826 if let Some(pk_ident) = pk_ident_for_pool {
3827 if audited_fields.is_some() {
3828 let pairs = audit_pair_tokens.iter();
3829 let pk_str = audit_pk_to_string.clone();
3830 quote! {
3831 /// Delete this row against either backend with audit
3832 /// emission inside the same transaction. Bi-dialect
3833 /// counterpart of [`Self::delete`] for audited models.
3834 ///
3835 /// # Errors
3836 /// As [`Self::delete`].
3837 pub async fn delete_pool(
3838 &self,
3839 pool: &#root::sql::Pool,
3840 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3841 let _query = #root::core::DeleteQuery {
3842 model: <Self as #root::core::Model>::SCHEMA,
3843 where_clause: #root::core::WhereExpr::Predicate(
3844 #root::core::Filter {
3845 column: #pk_column_lit,
3846 op: #root::core::Op::Eq,
3847 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3848 ::core::clone::Clone::clone(&self.#pk_ident)
3849 ),
3850 }
3851 ),
3852 };
3853 let _audit_entry = #root::audit::PendingEntry {
3854 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3855 entity_pk: #pk_str,
3856 operation: #root::audit::AuditOp::Delete,
3857 source: #root::audit::current_source(),
3858 changes: #root::audit::snapshot_changes(&[
3859 #( #pairs ),*
3860 ]),
3861 };
3862 #root::audit::delete_one_with_audit(
3863 pool, &_query, &_audit_entry,
3864 ).await
3865 }
3866 }
3867 } else {
3868 quote! {
3869 /// Delete the row identified by this instance's primary key
3870 /// against either backend. Equivalent to [`Self::delete`] but
3871 /// takes [`#root::sql::Pool`] and dispatches per backend.
3872 ///
3873 /// # Errors
3874 /// As [`Self::delete`].
3875 pub async fn delete_pool(
3876 &self,
3877 pool: &#root::sql::Pool,
3878 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3879 let _query = #root::core::DeleteQuery {
3880 model: <Self as #root::core::Model>::SCHEMA,
3881 where_clause: #root::core::WhereExpr::Predicate(
3882 #root::core::Filter {
3883 column: #pk_column_lit,
3884 op: #root::core::Op::Eq,
3885 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3886 ::core::clone::Clone::clone(&self.#pk_ident)
3887 ),
3888 }
3889 ),
3890 };
3891 #root::sql::delete_pool(pool, &_query).await
3892 }
3893 }
3894 }
3895 } else {
3896 quote!()
3897 }
3898 };
3899
3900 // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
3901 // matching this instance's PK and overwrite the in-memory state
3902 // with the freshly-fetched columns. Django's `refresh_from_db`.
3903 // Issue #825. Only emitted when the model declares a PK; non-PK
3904 // models can't address a specific row.
3905 //
3906 // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
3907 // every field from `self`; resets the PK to `Auto::Unset` when
3908 // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
3909 // a fresh autoincrement value. Non-Auto PKs preserve the source
3910 // PK — the caller must overwrite before insert. Pure-Rust, no
3911 // I/O, no dialect surface.
3912 let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
3913 let other_field_clones: Vec<TokenStream2> = fields
3914 .column_entries
3915 .iter()
3916 .filter(|c| &c.ident != pk_ident)
3917 .map(|c| {
3918 let ident = &c.ident;
3919 quote! {
3920 #ident: ::core::clone::Clone::clone(&self.#ident)
3921 }
3922 })
3923 .collect();
3924 let pk_clone_token = if fields.pk_is_auto {
3925 quote! { #pk_ident: #root::sql::Auto::Unset }
3926 } else {
3927 quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
3928 };
3929 let replicate_doc = if fields.pk_is_auto {
3930 quote! {
3931 /// Eloquent-style `replicate()` — return a clone of this
3932 /// row with the primary key reset to [`Auto::Unset`] so
3933 /// the copy is ready for `insert_pool` / `save_pool` to
3934 /// allocate a fresh autoincrement value. Every other
3935 /// field is `Clone`d verbatim. Issue #825.
3936 ///
3937 /// `auto_now_add` / `auto_now` timestamp fields are
3938 /// **not** reset (Eloquent's `replicate` doesn't reset
3939 /// them either) — pass them through the normal insert
3940 /// path if you want fresh values, or assign them
3941 /// explicitly after the call.
3942 }
3943 } else {
3944 quote! {
3945 /// Eloquent-style `replicate()` — clone this row
3946 /// verbatim. Because the primary key is **not** an
3947 /// `Auto<T>`, the clone keeps the source PK; the
3948 /// caller must overwrite `copy.<pk>` before inserting
3949 /// to avoid a unique-key violation. Issue #825.
3950 }
3951 };
3952 // 2026-06-07 — field-name / shortcut collision guard.
3953 //
3954 // The macro emits both `pub const <field>: <field>_col = ...`
3955 // (per-field typed-column const, used by the typed-builder
3956 // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
3957 // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
3958 // model has a field named e.g. `count`, both items would land
3959 // in the same inherent impl with the same name, and the
3960 // compiler rejects the derive with "duplicate definitions".
3961 //
3962 // Drop the conflicting shortcut for that model. Callers can
3963 // still reach the same behavior via
3964 // `QuerySet::<Model>::default().count(&pool)`.
3965 let column_names: ::std::collections::HashSet<String> = fields
3966 .column_entries
3967 .iter()
3968 .map(|c| c.ident.to_string())
3969 .collect();
3970 let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
3971 if column_names.contains(name) {
3972 quote! {}
3973 } else {
3974 tokens
3975 }
3976 };
3977 let count_method = emit_if_no_field_collision(
3978 "count",
3979 quote! {
3980 /// Count rows of this model — `SELECT COUNT(*) FROM
3981 /// <table>`. Eloquent `Model::count()` parity.
3982 ///
3983 /// Skipped on models that already declare a field named
3984 /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
3985 /// in that case.
3986 ///
3987 /// # Errors
3988 /// As [`CounterPool::count`].
3989 ///
3990 /// [`CounterPool::count`]: rustango::sql::CounterPool::count
3991 pub async fn count(
3992 pool: &#root::sql::Pool,
3993 ) -> ::core::result::Result<i64, #root::sql::ExecError> {
3994 use #root::sql::CounterPool as _;
3995 #root::query::QuerySet::<Self>::default()
3996 .count(pool)
3997 .await
3998 }
3999 },
4000 );
4001 let value_method = emit_if_no_field_collision(
4002 "value",
4003 quote! {
4004 /// Pluck a single scalar from the first row.
4005 /// Eloquent `Model::query()->value($col)` parity.
4006 ///
4007 /// Skipped on models that already declare a field named
4008 /// `value`. Drop into
4009 /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4010 ///
4011 /// # Errors
4012 /// As `ValuesFlatQuerySet::first`.
4013 pub async fn value<U>(
4014 col: &str,
4015 pool: &#root::sql::Pool,
4016 ) -> ::core::result::Result<
4017 ::core::option::Option<U>,
4018 #root::sql::ExecError,
4019 >
4020 where
4021 U: #root::sql::MaybePgScalar
4022 + #root::sql::MaybeMyScalar
4023 + #root::sql::MaybeSqliteScalar
4024 + ::core::marker::Send
4025 + ::core::marker::Unpin,
4026 {
4027 let _col_static: &'static str = Self::__resolve_col(col)?;
4028 #root::query::QuerySet::<Self>::default()
4029 .values_list_flat(_col_static)
4030 .first::<U>(pool)
4031 .await
4032 }
4033 },
4034 );
4035 let sum_method = emit_if_no_field_collision(
4036 "sum",
4037 quote! {
4038 /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4039 /// Skipped on models that already declare a field named
4040 /// `sum`.
4041 ///
4042 /// # Errors
4043 /// As [`#root::sql::fetch_aggregate_pool`].
4044 pub async fn sum<U>(
4045 col: &str,
4046 pool: &#root::sql::Pool,
4047 ) -> ::core::result::Result<
4048 ::core::option::Option<U>,
4049 #root::sql::ExecError,
4050 >
4051 where
4052 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4053 + #root::sql::MaybeMyFromRow
4054 + #root::sql::MaybeSqliteFromRow
4055 + ::core::marker::Send
4056 + ::core::marker::Unpin,
4057 {
4058 Self::__aggregate_one_pool::<U>(
4059 col,
4060 |c| #root::core::AggregateExpr::Sum(c),
4061 pool,
4062 )
4063 .await
4064 }
4065 },
4066 );
4067 let avg_method = emit_if_no_field_collision(
4068 "avg",
4069 quote! {
4070 /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4071 /// models that already declare a field named `avg`.
4072 ///
4073 /// # Errors
4074 /// As [`#root::sql::fetch_aggregate_pool`].
4075 pub async fn avg<U>(
4076 col: &str,
4077 pool: &#root::sql::Pool,
4078 ) -> ::core::result::Result<
4079 ::core::option::Option<U>,
4080 #root::sql::ExecError,
4081 >
4082 where
4083 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4084 + #root::sql::MaybeMyFromRow
4085 + #root::sql::MaybeSqliteFromRow
4086 + ::core::marker::Send
4087 + ::core::marker::Unpin,
4088 {
4089 Self::__aggregate_one_pool::<U>(
4090 col,
4091 |c| #root::core::AggregateExpr::Avg(c),
4092 pool,
4093 )
4094 .await
4095 }
4096 },
4097 );
4098 let min_method = emit_if_no_field_collision(
4099 "min",
4100 quote! {
4101 /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4102 /// models that already declare a field named `min`.
4103 ///
4104 /// # Errors
4105 /// As [`#root::sql::fetch_aggregate_pool`].
4106 pub async fn min<U>(
4107 col: &str,
4108 pool: &#root::sql::Pool,
4109 ) -> ::core::result::Result<
4110 ::core::option::Option<U>,
4111 #root::sql::ExecError,
4112 >
4113 where
4114 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4115 + #root::sql::MaybeMyFromRow
4116 + #root::sql::MaybeSqliteFromRow
4117 + ::core::marker::Send
4118 + ::core::marker::Unpin,
4119 {
4120 Self::__aggregate_one_pool::<U>(
4121 col,
4122 |c| #root::core::AggregateExpr::Min(c),
4123 pool,
4124 )
4125 .await
4126 }
4127 },
4128 );
4129 let max_method = emit_if_no_field_collision(
4130 "max",
4131 quote! {
4132 /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4133 /// models that already declare a field named `max`.
4134 ///
4135 /// # Errors
4136 /// As [`#root::sql::fetch_aggregate_pool`].
4137 pub async fn max<U>(
4138 col: &str,
4139 pool: &#root::sql::Pool,
4140 ) -> ::core::result::Result<
4141 ::core::option::Option<U>,
4142 #root::sql::ExecError,
4143 >
4144 where
4145 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4146 + #root::sql::MaybeMyFromRow
4147 + #root::sql::MaybeSqliteFromRow
4148 + ::core::marker::Send
4149 + ::core::marker::Unpin,
4150 {
4151 Self::__aggregate_one_pool::<U>(
4152 col,
4153 |c| #root::core::AggregateExpr::Max(c),
4154 pool,
4155 )
4156 .await
4157 }
4158 },
4159 );
4160 let first_method = emit_if_no_field_collision(
4161 "first",
4162 quote! {
4163 /// First row of this model. Eloquent `Model::first()`.
4164 /// Skipped on models that already declare a field named
4165 /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4166 ///
4167 /// # Errors
4168 /// As `QuerySet::first`.
4169 pub async fn first(
4170 pool: &#root::sql::Pool,
4171 ) -> ::core::result::Result<
4172 ::core::option::Option<Self>,
4173 #root::sql::ExecError,
4174 > {
4175 #root::query::QuerySet::<Self>::default()
4176 .first(pool)
4177 .await
4178 }
4179 },
4180 );
4181 let last_method = emit_if_no_field_collision(
4182 "last",
4183 quote! {
4184 /// Last row of this model by primary-key DESC.
4185 /// Eloquent `Model::query()->latest('id')->first()`
4186 /// parity — fetches the highest-PK row without
4187 /// requiring the caller to spell the PK column.
4188 /// Returns `None` on an empty table.
4189 ///
4190 /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4191 /// Skipped on models that already declare a field
4192 /// named `last`.
4193 ///
4194 /// # Errors
4195 /// As `QuerySet::last`.
4196 pub async fn last(
4197 pool: &#root::sql::Pool,
4198 ) -> ::core::result::Result<
4199 ::core::option::Option<Self>,
4200 #root::sql::ExecError,
4201 > {
4202 #root::query::QuerySet::<Self>::default()
4203 .last(pool)
4204 .await
4205 }
4206 },
4207 );
4208 quote! {
4209 /// Re-SELECT this row by its primary key and overwrite
4210 /// every in-memory field with the freshly-fetched value.
4211 /// Django's [`Model.refresh_from_db`]. Issue #825.
4212 ///
4213 /// Use this when the row may have been modified by another
4214 /// process / connection / job since you read it — e.g. after
4215 /// a queued task callback, or to re-sync stale UI state
4216 /// before re-saving.
4217 ///
4218 /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4219 /// when the primary key no longer matches any row (e.g.
4220 /// the row was deleted concurrently).
4221 ///
4222 /// # Errors
4223 /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4224 /// the PK no longer exists.
4225 ///
4226 /// [`Model.refresh_from_db`]: https://docs.djangoproject.com/en/5.1/ref/models/instances/#django.db.models.Model.refresh_from_db
4227 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4228 pub async fn refresh_from_db(
4229 &mut self,
4230 pool: &#root::sql::Pool,
4231 ) -> ::core::result::Result<(), #root::sql::ExecError> {
4232 use #root::sql::FetcherPool as _;
4233 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4234 ::core::clone::Clone::clone(&self.#pk_ident),
4235 );
4236 let mut _rows: ::std::vec::Vec<Self> =
4237 #root::query::QuerySet::<Self>::default()
4238 .filter(::core::stringify!(#pk_ident), _pk_val)
4239 .limit(1)
4240 .fetch(pool)
4241 .await?;
4242 match _rows.into_iter().next() {
4243 ::core::option::Option::Some(_fresh) => {
4244 *self = _fresh;
4245 ::core::result::Result::Ok(())
4246 }
4247 ::core::option::Option::None => ::core::result::Result::Err(
4248 #root::sql::ExecError::Driver(
4249 #root::sql::sqlx::Error::RowNotFound,
4250 ),
4251 ),
4252 }
4253 }
4254
4255 /// Atomically increment the integer column `col` by
4256 /// `by` for this row. Equivalent to
4257 /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4258 /// Eloquent `Model::increment($col, $by)` / Django
4259 /// `Model.objects.filter(pk=…).update(col=F('col')+$by)`
4260 /// parity.
4261 ///
4262 /// **Doesn't mutate `self`** — the in-memory copy is now
4263 /// stale; call [`Self::refresh_from_db_pool`] /
4264 /// [`Self::fresh_pool`] to re-sync. Returns the rows-
4265 /// affected count (0 when the PK doesn't match any row,
4266 /// 1 on success).
4267 ///
4268 /// `col` is the Rust field name as a string; unknown
4269 /// fields surface as `UnknownField` at runtime. Negative
4270 /// `by` values atomically decrement (see also
4271 /// [`Self::decrement_pool`]).
4272 ///
4273 /// # Errors
4274 /// As [`UpdaterPool::execute_pool`].
4275 ///
4276 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4277 pub async fn increment(
4278 &self,
4279 col: &str,
4280 by: i64,
4281 pool: &#root::sql::Pool,
4282 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4283 Self::__increment_one(self, col, by, pool).await
4284 }
4285
4286 /// Sibling of [`Self::increment`] — atomically
4287 /// decrement this row's `col` by `by`. Eloquent
4288 /// `$model->decrement($col, $by)` parity. Equivalent to
4289 /// `self.increment(col, -by, &pool)`; the separate name
4290 /// keeps call sites readable.
4291 ///
4292 /// # Errors
4293 /// As [`Self::increment`].
4294 pub async fn decrement(
4295 &self,
4296 col: &str,
4297 by: i64,
4298 pool: &#root::sql::Pool,
4299 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4300 Self::__increment_one(self, col, -by, pool).await
4301 }
4302
4303 /// Bulk-increment: add `by` to `col` on every row of the
4304 /// table. Eloquent `Model::query()->increment($col, $by)`
4305 /// parity. Use for counters, score adjustments, view
4306 /// rollups.
4307 ///
4308 /// # Errors
4309 /// As [`UpdaterPool::execute_pool`].
4310 ///
4311 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4312 pub async fn increment_each(
4313 col: &str,
4314 by: i64,
4315 pool: &#root::sql::Pool,
4316 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4317 Self::__increment_all(col, by, pool).await
4318 }
4319
4320 /// Sibling of [`Self::increment_each`] — bulk-decrement.
4321 ///
4322 /// # Errors
4323 /// As [`Self::increment_each`].
4324 pub async fn decrement_each(
4325 col: &str,
4326 by: i64,
4327 pool: &#root::sql::Pool,
4328 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4329 Self::__increment_all(col, -by, pool).await
4330 }
4331
4332 /// Internal: forward to
4333 /// [`#root::sql::model_shortcuts::increment_one_pool`].
4334 /// One-line wrapper kept as a per-Model method so the
4335 /// macro's emitted `increment` / `decrement` instance
4336 /// calls don't have to thread `Self` through manually.
4337 #[doc(hidden)]
4338 pub async fn __increment_one(
4339 this: &Self,
4340 col: &str,
4341 by: i64,
4342 pool: &#root::sql::Pool,
4343 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4344 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4345 ::core::clone::Clone::clone(&this.#pk_ident),
4346 );
4347 #root::sql::model_shortcuts::increment_one_pool::<Self>(
4348 ::core::stringify!(#pk_ident),
4349 _pk_val,
4350 col,
4351 by,
4352 pool,
4353 )
4354 .await
4355 }
4356
4357 /// Internal: forward to
4358 /// [`#root::sql::model_shortcuts::increment_all_pool`].
4359 #[doc(hidden)]
4360 pub async fn __increment_all(
4361 col: &str,
4362 by: i64,
4363 pool: &#root::sql::Pool,
4364 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4365 #root::sql::model_shortcuts::increment_all_pool::<Self>(col, by, pool).await
4366 }
4367
4368 /// Internal: forward to
4369 /// [`#root::sql::model_shortcuts::resolve_col`].
4370 #[doc(hidden)]
4371 pub fn __resolve_col(
4372 col: &str,
4373 ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4374 #root::sql::model_shortcuts::resolve_col::<Self>(col)
4375 }
4376
4377 /// Internal: forward to
4378 /// [`#root::sql::model_shortcuts::add_signed_expr`].
4379 #[doc(hidden)]
4380 #[must_use]
4381 pub fn __add_signed_expr(
4382 col_static: &'static str,
4383 signed_by: i64,
4384 ) -> #root::core::Expr {
4385 #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4386 }
4387
4388 /// Re-SELECT this row by its primary key and return a
4389 /// **new** instance with the freshly-fetched fields.
4390 /// Eloquent `Model::fresh()` parity — non-mutating
4391 /// counterpart of [`Self::refresh_from_db_pool`].
4392 ///
4393 /// Returns `Ok(None)` when the row was deleted
4394 /// concurrently — vs [`Self::refresh_from_db_pool`]
4395 /// which surfaces that as `RowNotFound` because
4396 /// in-place mutation has nothing to write to.
4397 ///
4398 /// Useful when you want to compare the in-memory
4399 /// instance against the persisted state (audit-style
4400 /// diffs, conflict detection) without mutating the
4401 /// reference you already hold.
4402 ///
4403 /// # Errors
4404 /// As [`FetcherPool::fetch`].
4405 ///
4406 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4407 pub async fn fresh(
4408 &self,
4409 pool: &#root::sql::Pool,
4410 ) -> ::core::result::Result<
4411 ::core::option::Option<Self>,
4412 #root::sql::ExecError,
4413 > {
4414 use #root::sql::FetcherPool as _;
4415 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4416 ::core::clone::Clone::clone(&self.#pk_ident),
4417 );
4418 let _rows: ::std::vec::Vec<Self> =
4419 #root::query::QuerySet::<Self>::default()
4420 .filter(::core::stringify!(#pk_ident), _pk_val)
4421 .limit(1)
4422 .fetch(pool)
4423 .await?;
4424 ::core::result::Result::Ok(_rows.into_iter().next())
4425 }
4426
4427 #replicate_doc
4428 #[must_use]
4429 pub fn replicate(&self) -> Self {
4430 Self {
4431 #pk_clone_token,
4432 #( #other_field_clones, )*
4433 }
4434 }
4435
4436 #first_method
4437
4438 #last_method
4439
4440 /// Throwing counterpart of [`Self::first_pool`] —
4441 /// errors with `RowNotFound` when the table is empty.
4442 /// Eloquent `Model::firstOrFail()` parity.
4443 ///
4444 /// # Errors
4445 /// As [`Self::first_pool`]; additionally
4446 /// [`sqlx::Error::RowNotFound`] on empty tables.
4447 ///
4448 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4449 pub async fn first_or_fail(
4450 pool: &#root::sql::Pool,
4451 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4452 // Route through the queryset rather than `Self::first`,
4453 // which is suppressed on models with a field named
4454 // `first` (field/shortcut collision guard).
4455 match #root::query::QuerySet::<Self>::default().first(pool).await? {
4456 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4457 ::core::option::Option::None => ::core::result::Result::Err(
4458 #root::sql::ExecError::Driver(
4459 #root::sql::sqlx::Error::RowNotFound,
4460 ),
4461 ),
4462 }
4463 }
4464
4465 /// Single-column projection — `SELECT <col> FROM
4466 /// <table>`. Returns `Vec<U>` where each element is the
4467 /// decoded value of the column. Eloquent
4468 /// `Model::pluck($column)` / Django
4469 /// `Model.objects.values_list('col', flat=True)` parity.
4470 ///
4471 /// Thin wrapper over `QuerySet::<Self>::default()
4472 /// .values_list_flat(col).fetch::<U>(pool)`. `U` must
4473 /// be decodable from the column's SQL type on every
4474 /// dialect the binary targets (common picks: `i64` /
4475 /// `i32` / `String` / `bool` / `f64`).
4476 ///
4477 /// # Errors
4478 /// As `ValuesFlatQuerySet::fetch`.
4479 pub async fn pluck<U>(
4480 col: &'static str,
4481 pool: &#root::sql::Pool,
4482 ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4483 where
4484 U: #root::sql::MaybePgScalar
4485 + #root::sql::MaybeMyScalar
4486 + #root::sql::MaybeSqliteScalar
4487 + ::core::marker::Send
4488 + ::core::marker::Unpin,
4489 {
4490 #root::query::QuerySet::<Self>::default()
4491 .values_list_flat(col)
4492 .fetch::<U>(pool)
4493 .await
4494 }
4495
4496 /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4497 /// stream every row of this model in batches of `n`,
4498 /// invoking the callback once per batch. Stable PK-ASC
4499 /// ordering so the LIMIT/OFFSET pagination is
4500 /// deterministic across drivers.
4501 ///
4502 /// The callback is async — it can do further DB work
4503 /// (writes, related-row lookups, queue dispatch) per
4504 /// batch. Return `Err(...)` from the callback to abort
4505 /// the iteration early; the error bubbles up.
4506 ///
4507 /// **When to use**: bulk processing flows that can't
4508 /// fit the whole table in memory — sending newsletters,
4509 /// running data migrations, computing summary
4510 /// statistics. For small / known-bounded tables, plain
4511 /// `Self::all(&pool)` is simpler.
4512 ///
4513 /// **Caveat**: ascending OFFSET pagination is O(N²) on
4514 /// large tables. For multi-million-row scans prefer
4515 /// keyset-by-PK (the standard "WHERE id > last_seen"
4516 /// shape) over `chunk(...)`.
4517 ///
4518 /// Skipped on models without a primary key — chunking
4519 /// needs a stable order to avoid skipping / repeating
4520 /// rows across batches.
4521 pub async fn chunk<F, Fut>(
4522 n: i64,
4523 pool: &#root::sql::Pool,
4524 mut cb: F,
4525 ) -> ::core::result::Result<(), #root::sql::ExecError>
4526 where
4527 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4528 Fut: ::core::future::Future<
4529 Output = ::core::result::Result<(), #root::sql::ExecError>,
4530 >,
4531 {
4532 use #root::sql::FetcherPool as _;
4533 let pk_col = match Self::primary_key_column() {
4534 ::core::option::Option::Some(c) => c,
4535 ::core::option::Option::None => {
4536 return ::core::result::Result::Ok(());
4537 }
4538 };
4539 let mut offset: i64 = 0;
4540 loop {
4541 let rows: ::std::vec::Vec<Self> =
4542 #root::query::QuerySet::<Self>::default()
4543 .order_by(&[(pk_col, false)])
4544 .limit(n)
4545 .offset(offset)
4546 .fetch(pool)
4547 .await?;
4548 if rows.is_empty() {
4549 return ::core::result::Result::Ok(());
4550 }
4551 let len = rows.len() as i64;
4552 cb(rows).await?;
4553 if len < n {
4554 return ::core::result::Result::Ok(());
4555 }
4556 offset += n;
4557 }
4558 }
4559
4560 /// Eloquent `Model::chunkById($n, fn (...))` — same
4561 /// per-batch callback shape as [`Self::chunk`], but uses
4562 /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4563 /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4564 /// — the right choice for multi-million-row sweeps.
4565 ///
4566 /// Requires the primary key to be a signed integer type
4567 /// (`i64` / `i32`); the keyset comparison rides on
4568 /// `__rustango_pk_value()` lowering through
4569 /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4570 /// non-integer PKs (UUID, String) — those should use
4571 /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4572 /// keyset loop.
4573 ///
4574 /// Callback errors abort iteration; the error bubbles up
4575 /// unchanged. Empty table → callback invoked zero times.
4576 pub async fn chunk_by_id<F, Fut>(
4577 n: i64,
4578 pool: &#root::sql::Pool,
4579 mut cb: F,
4580 ) -> ::core::result::Result<(), #root::sql::ExecError>
4581 where
4582 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4583 Fut: ::core::future::Future<
4584 Output = ::core::result::Result<(), #root::sql::ExecError>,
4585 >,
4586 {
4587 use #root::sql::FetcherPool as _;
4588 let pk_col = match Self::primary_key_column() {
4589 ::core::option::Option::Some(c) => c,
4590 ::core::option::Option::None => {
4591 return ::core::result::Result::Ok(());
4592 }
4593 };
4594 // Track the largest PK seen so the next batch picks
4595 // up from there. `i64::MIN` as the sentinel — the
4596 // very first iteration's `> MIN` matches every row,
4597 // so the loop entry is uniform with subsequent
4598 // iterations.
4599 let mut last_seen: i64 = i64::MIN;
4600 loop {
4601 let key = ::std::format!("{}__gt", pk_col);
4602 let rows: ::std::vec::Vec<Self> =
4603 #root::query::QuerySet::<Self>::default()
4604 .filter(key.as_str(), last_seen)
4605 .order_by(&[(pk_col, false)])
4606 .limit(n)
4607 .fetch(pool)
4608 .await?;
4609 if rows.is_empty() {
4610 return ::core::result::Result::Ok(());
4611 }
4612 let len = rows.len() as i64;
4613 // Capture the last row's PK BEFORE moving rows
4614 // into the callback.
4615 let max_pk = match rows
4616 .last()
4617 .map(|r| r.__rustango_pk_value())
4618 {
4619 ::core::option::Option::Some(
4620 #root::core::SqlValue::I64(v),
4621 ) => v,
4622 ::core::option::Option::Some(
4623 #root::core::SqlValue::I32(v),
4624 ) => i64::from(v),
4625 _ => return ::core::result::Result::Ok(()),
4626 };
4627 cb(rows).await?;
4628 if len < n {
4629 return ::core::result::Result::Ok(());
4630 }
4631 last_seen = max_pk;
4632 }
4633 }
4634
4635 /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4636 /// per-row callback companion to [`Self::chunk`].
4637 /// Streams every row in keyset-paginated batches of
4638 /// `batch` size, calling `cb` once per row.
4639 ///
4640 /// Inherits the keyset-paginated scan of
4641 /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4642 /// non-integer PKs are silently a no-op).
4643 ///
4644 /// ```ignore
4645 /// Post::each(500, &pool, |p| async move {
4646 /// reindex(p).await?;
4647 /// Ok(())
4648 /// }).await?;
4649 /// ```
4650 ///
4651 /// Return `Err(...)` from the callback to abort
4652 /// iteration; the error bubbles up unchanged.
4653 pub async fn each<F, Fut>(
4654 batch: i64,
4655 pool: &#root::sql::Pool,
4656 mut cb: F,
4657 ) -> ::core::result::Result<(), #root::sql::ExecError>
4658 where
4659 F: ::core::ops::FnMut(Self) -> Fut,
4660 Fut: ::core::future::Future<
4661 Output = ::core::result::Result<(), #root::sql::ExecError>,
4662 >,
4663 {
4664 use #root::sql::FetcherPool as _;
4665 let pk_col = match Self::primary_key_column() {
4666 ::core::option::Option::Some(c) => c,
4667 ::core::option::Option::None => {
4668 return ::core::result::Result::Ok(());
4669 }
4670 };
4671 let mut last_seen: i64 = i64::MIN;
4672 loop {
4673 let key = ::std::format!("{}__gt", pk_col);
4674 let rows: ::std::vec::Vec<Self> =
4675 #root::query::QuerySet::<Self>::default()
4676 .filter(key.as_str(), last_seen)
4677 .order_by(&[(pk_col, false)])
4678 .limit(batch)
4679 .fetch(pool)
4680 .await?;
4681 if rows.is_empty() {
4682 return ::core::result::Result::Ok(());
4683 }
4684 let len = rows.len() as i64;
4685 let max_pk = match rows
4686 .last()
4687 .map(|r| r.__rustango_pk_value())
4688 {
4689 ::core::option::Option::Some(
4690 #root::core::SqlValue::I64(v),
4691 ) => v,
4692 ::core::option::Option::Some(
4693 #root::core::SqlValue::I32(v),
4694 ) => i64::from(v),
4695 _ => return ::core::result::Result::Ok(()),
4696 };
4697 for row in rows {
4698 cb(row).await?;
4699 }
4700 if len < batch {
4701 return ::core::result::Result::Ok(());
4702 }
4703 last_seen = max_pk;
4704 }
4705 }
4706
4707 /// Delete every row of this model — `TRUNCATE TABLE
4708 /// <table> RESTART IDENTITY CASCADE` on Postgres,
4709 /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4710 /// support `TRUNCATE` inside foreign-key constraints
4711 /// or — for SQLite — at all). Eloquent `Model::truncate()`
4712 /// / Django `Model.objects.all().delete()` parity.
4713 ///
4714 /// **Use only in tests / fixture-reset flows.** Production
4715 /// writes through this would silently bypass the
4716 /// `pre_delete` / `post_delete` signals (no per-row hooks
4717 /// fire on a TRUNCATE / bulk DELETE FROM) and lose every
4718 /// row's audit-log entry.
4719 ///
4720 /// # Errors
4721 /// As [`raw_execute_pool`].
4722 ///
4723 /// [`raw_execute_pool`]: rustango::sql::raw_execute_pool
4724 pub async fn truncate(
4725 pool: &#root::sql::Pool,
4726 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4727 let _table = <Self as #root::core::Model>::SCHEMA.table;
4728 let _dialect = pool.dialect();
4729 let _quoted = _dialect.quote_ident(_table);
4730 let _sql = if _dialect.name() == "postgres" {
4731 ::std::format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", _quoted)
4732 } else {
4733 ::std::format!("DELETE FROM {}", _quoted)
4734 };
4735 #root::sql::raw_execute_pool(pool, &_sql, ::std::vec::Vec::new()).await
4736 }
4737
4738 /// Bulk-delete every row whose primary key is in
4739 /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4740 /// Returns the affected row count.
4741 ///
4742 /// Eloquent `Model::destroy([1, 2, 3])` / Django
4743 /// `Model.objects.filter(pk__in=[...]).delete()` parity.
4744 /// Empty `pks` is a no-op (returns 0).
4745 ///
4746 /// Accepts any iterable whose elements are
4747 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4748 /// `[i64; N]`, etc.
4749 ///
4750 /// # Errors
4751 /// As `delete_pool`.
4752 pub async fn destroy<V>(
4753 pks: impl ::core::iter::IntoIterator<Item = V>,
4754 pool: &#root::sql::Pool,
4755 ) -> ::core::result::Result<u64, #root::sql::ExecError>
4756 where
4757 V: ::core::convert::Into<#root::core::SqlValue>,
4758 {
4759 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4760 pks.into_iter().map(::core::convert::Into::into).collect();
4761 if _values.is_empty() {
4762 return ::core::result::Result::Ok(0);
4763 }
4764 let _query = #root::core::DeleteQuery {
4765 model: <Self as #root::core::Model>::SCHEMA,
4766 where_clause: #root::core::WhereExpr::Predicate(
4767 #root::core::Filter {
4768 column: <Self as #root::core::Model>::SCHEMA
4769 .primary_key()
4770 .ok_or_else(|| {
4771 #root::sql::ExecError::Sql(
4772 #root::sql::SqlError::MissingPrimaryKey,
4773 )
4774 })?
4775 .column,
4776 op: #root::core::Op::In,
4777 value: #root::core::SqlValue::List(_values),
4778 },
4779 ),
4780 };
4781 #root::sql::delete_pool(pool, &_query).await
4782 }
4783
4784 /// Fetch every row where `<col> = <val>`. Eloquent
4785 /// `Model::where($col, $val)->get()` / Django
4786 /// `Model.objects.filter(col=val).all()` parity.
4787 ///
4788 /// Thin wrapper over `QuerySet::<Self>::default()
4789 /// .filter(col, val).fetch(pool)`. For one row,
4790 /// use [`Self::first_where_pool`]; for a chain that
4791 /// needs further `.filter()` / `.order_by()` /
4792 /// `.limit()`, drop down to `Self::query().filter(...)`
4793 /// directly.
4794 ///
4795 /// `val` accepts any value `Into<SqlValue>` so plain
4796 /// strings, ints, UUIDs, etc. all work.
4797 ///
4798 /// # Errors
4799 /// As [`FetcherPool::fetch`].
4800 ///
4801 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4802 pub async fn where_(
4803 col: &str,
4804 val: impl ::core::convert::Into<#root::core::SqlValue>,
4805 pool: &#root::sql::Pool,
4806 ) -> ::core::result::Result<
4807 ::std::vec::Vec<Self>,
4808 #root::sql::ExecError,
4809 > {
4810 use #root::sql::FetcherPool as _;
4811 #root::query::QuerySet::<Self>::default()
4812 .filter(col, val)
4813 .fetch(pool)
4814 .await
4815 }
4816
4817 /// Fetch every row where `<col> IN (vals)`. Eloquent
4818 /// `Model::whereIn($col, $vals)->get()` parity. Empty
4819 /// `vals` returns no rows (matches SQL's empty-IN
4820 /// semantics).
4821 ///
4822 /// `vals` accepts any iterable whose items are
4823 /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
4824 /// etc.
4825 ///
4826 /// # Errors
4827 /// As [`FetcherPool::fetch`].
4828 ///
4829 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4830 pub async fn where_in<V>(
4831 col: &str,
4832 vals: impl ::core::iter::IntoIterator<Item = V>,
4833 pool: &#root::sql::Pool,
4834 ) -> ::core::result::Result<
4835 ::std::vec::Vec<Self>,
4836 #root::sql::ExecError,
4837 >
4838 where
4839 V: ::core::convert::Into<#root::core::SqlValue>,
4840 {
4841 use #root::sql::FetcherPool as _;
4842 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4843 vals.into_iter().map(::core::convert::Into::into).collect();
4844 if _values.is_empty() {
4845 return ::core::result::Result::Ok(::std::vec::Vec::new());
4846 }
4847 let _key = ::std::format!("{}__in", col);
4848 #root::query::QuerySet::<Self>::default()
4849 .filter(&_key, #root::core::SqlValue::List(_values))
4850 .fetch(pool)
4851 .await
4852 }
4853
4854 /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
4855 /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
4856 /// `vals` returns every row (matches SQL's empty-NOT-IN
4857 /// semantics — vacuously true for every row).
4858 ///
4859 /// # Errors
4860 /// As [`FetcherPool::fetch`].
4861 ///
4862 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4863 pub async fn where_not_in<V>(
4864 col: &str,
4865 vals: impl ::core::iter::IntoIterator<Item = V>,
4866 pool: &#root::sql::Pool,
4867 ) -> ::core::result::Result<
4868 ::std::vec::Vec<Self>,
4869 #root::sql::ExecError,
4870 >
4871 where
4872 V: ::core::convert::Into<#root::core::SqlValue>,
4873 {
4874 use #root::sql::FetcherPool as _;
4875 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4876 vals.into_iter().map(::core::convert::Into::into).collect();
4877 if _values.is_empty() {
4878 return #root::query::QuerySet::<Self>::default()
4879 .fetch(pool)
4880 .await;
4881 }
4882 let _key = ::std::format!("{}__not_in", col);
4883 #root::query::QuerySet::<Self>::default()
4884 .filter(&_key, #root::core::SqlValue::List(_values))
4885 .fetch(pool)
4886 .await
4887 }
4888
4889 /// Fetch every row where `<col> IS NULL`. Eloquent
4890 /// `Model::whereNull($col)->get()` parity.
4891 ///
4892 /// # Errors
4893 /// As [`FetcherPool::fetch`].
4894 ///
4895 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4896 pub async fn where_null(
4897 col: &str,
4898 pool: &#root::sql::Pool,
4899 ) -> ::core::result::Result<
4900 ::std::vec::Vec<Self>,
4901 #root::sql::ExecError,
4902 > {
4903 use #root::sql::FetcherPool as _;
4904 let _key = ::std::format!("{}__isnull", col);
4905 #root::query::QuerySet::<Self>::default()
4906 .filter(&_key, #root::core::SqlValue::Bool(true))
4907 .fetch(pool)
4908 .await
4909 }
4910
4911 /// Fetch every row where `<col> IS NOT NULL`. Eloquent
4912 /// `Model::whereNotNull($col)->get()` parity.
4913 ///
4914 /// # Errors
4915 /// As [`FetcherPool::fetch`].
4916 ///
4917 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4918 pub async fn where_not_null(
4919 col: &str,
4920 pool: &#root::sql::Pool,
4921 ) -> ::core::result::Result<
4922 ::std::vec::Vec<Self>,
4923 #root::sql::ExecError,
4924 > {
4925 use #root::sql::FetcherPool as _;
4926 let _key = ::std::format!("{}__isnull", col);
4927 #root::query::QuerySet::<Self>::default()
4928 .filter(&_key, #root::core::SqlValue::Bool(false))
4929 .fetch(pool)
4930 .await
4931 }
4932
4933 /// Fetch up to `n` rows in random order. Eloquent
4934 /// `Model::inRandomOrder()->limit($n)->get()` /
4935 /// `Model::query()->inRandomOrder()->get()->take($n)`
4936 /// parity. **Performance caveat**: random ordering
4937 /// forces a full table scan + per-row random key sort;
4938 /// the optimizer cannot use an index. Prefer a
4939 /// `pk >= rand_offset LIMIT N` walk for huge tables.
4940 ///
4941 /// # Errors
4942 /// As [`FetcherPool::fetch`].
4943 ///
4944 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4945 pub async fn random_n(
4946 n: i64,
4947 pool: &#root::sql::Pool,
4948 ) -> ::core::result::Result<
4949 ::std::vec::Vec<Self>,
4950 #root::sql::ExecError,
4951 > {
4952 use #root::sql::FetcherPool as _;
4953 #root::query::QuerySet::<Self>::default()
4954 .order_random()
4955 .limit(n)
4956 .fetch(pool)
4957 .await
4958 }
4959
4960 /// Fetch one row in random order. Eloquent
4961 /// `Model::inRandomOrder()->first()` parity. Same
4962 /// performance caveat as [`Self::random_n_pool`].
4963 ///
4964 /// # Errors
4965 /// As [`FetcherPool::fetch`].
4966 ///
4967 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4968 pub async fn random(
4969 pool: &#root::sql::Pool,
4970 ) -> ::core::result::Result<
4971 ::core::option::Option<Self>,
4972 #root::sql::ExecError,
4973 > {
4974 ::core::result::Result::Ok(
4975 Self::random_n(1, pool).await?.into_iter().next(),
4976 )
4977 }
4978
4979 /// Fetch every row ordered ASC by `field`. Eloquent
4980 /// `Model::oldest($field)->get()` parity — the multi-row
4981 /// counterpart of [`Self::earliest_pool`].
4982 ///
4983 /// # Errors
4984 /// As [`FetcherPool::fetch`].
4985 ///
4986 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4987 pub async fn oldest(
4988 field: &str,
4989 pool: &#root::sql::Pool,
4990 ) -> ::core::result::Result<
4991 ::std::vec::Vec<Self>,
4992 #root::sql::ExecError,
4993 > {
4994 use #root::sql::FetcherPool as _;
4995 #root::query::QuerySet::<Self>::default()
4996 .order_by(&[(field, false)])
4997 .fetch(pool)
4998 .await
4999 }
5000
5001 /// Fetch every row ordered DESC by `field`. Eloquent
5002 /// `Model::latest($field)->get()` parity — the multi-row
5003 /// counterpart of [`Self::latest_pool`].
5004 ///
5005 /// # Errors
5006 /// As [`FetcherPool::fetch`].
5007 ///
5008 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5009 pub async fn newest(
5010 field: &str,
5011 pool: &#root::sql::Pool,
5012 ) -> ::core::result::Result<
5013 ::std::vec::Vec<Self>,
5014 #root::sql::ExecError,
5015 > {
5016 use #root::sql::FetcherPool as _;
5017 #root::query::QuerySet::<Self>::default()
5018 .order_by(&[(field, true)])
5019 .fetch(pool)
5020 .await
5021 }
5022
5023 /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5024 /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5025 /// Routes through the existing `__year` lookup suffix
5026 /// (issue #829).
5027 ///
5028 /// # Errors
5029 /// As [`FetcherPool::fetch`].
5030 ///
5031 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5032 pub async fn where_year(
5033 col: &str,
5034 year: i64,
5035 pool: &#root::sql::Pool,
5036 ) -> ::core::result::Result<
5037 ::std::vec::Vec<Self>,
5038 #root::sql::ExecError,
5039 > {
5040 use #root::sql::FetcherPool as _;
5041 let _key = ::std::format!("{}__year", col);
5042 #root::query::QuerySet::<Self>::default()
5043 .filter(&_key, #root::core::SqlValue::I64(year))
5044 .fetch(pool)
5045 .await
5046 }
5047
5048 /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5049 /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5050 /// `month` is 1–12.
5051 ///
5052 /// # Errors
5053 /// As [`FetcherPool::fetch`].
5054 ///
5055 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5056 pub async fn where_month(
5057 col: &str,
5058 month: i64,
5059 pool: &#root::sql::Pool,
5060 ) -> ::core::result::Result<
5061 ::std::vec::Vec<Self>,
5062 #root::sql::ExecError,
5063 > {
5064 use #root::sql::FetcherPool as _;
5065 let _key = ::std::format!("{}__month", col);
5066 #root::query::QuerySet::<Self>::default()
5067 .filter(&_key, #root::core::SqlValue::I64(month))
5068 .fetch(pool)
5069 .await
5070 }
5071
5072 /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5073 /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5074 /// `day` is 1–31.
5075 ///
5076 /// # Errors
5077 /// As [`FetcherPool::fetch`].
5078 ///
5079 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5080 pub async fn where_day(
5081 col: &str,
5082 day: i64,
5083 pool: &#root::sql::Pool,
5084 ) -> ::core::result::Result<
5085 ::std::vec::Vec<Self>,
5086 #root::sql::ExecError,
5087 > {
5088 use #root::sql::FetcherPool as _;
5089 let _key = ::std::format!("{}__day", col);
5090 #root::query::QuerySet::<Self>::default()
5091 .filter(&_key, #root::core::SqlValue::I64(day))
5092 .fetch(pool)
5093 .await
5094 }
5095
5096 /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5097 /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5098 /// `hour` is 0–23.
5099 ///
5100 /// # Errors
5101 /// As [`FetcherPool::fetch`].
5102 ///
5103 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5104 pub async fn where_hour(
5105 col: &str,
5106 hour: i64,
5107 pool: &#root::sql::Pool,
5108 ) -> ::core::result::Result<
5109 ::std::vec::Vec<Self>,
5110 #root::sql::ExecError,
5111 > {
5112 use #root::sql::FetcherPool as _;
5113 let _key = ::std::format!("{}__hour", col);
5114 #root::query::QuerySet::<Self>::default()
5115 .filter(&_key, #root::core::SqlValue::I64(hour))
5116 .fetch(pool)
5117 .await
5118 }
5119
5120 /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5121 /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5122 /// `minute` is 0–59.
5123 ///
5124 /// # Errors
5125 /// As [`FetcherPool::fetch`].
5126 ///
5127 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5128 pub async fn where_minute(
5129 col: &str,
5130 minute: i64,
5131 pool: &#root::sql::Pool,
5132 ) -> ::core::result::Result<
5133 ::std::vec::Vec<Self>,
5134 #root::sql::ExecError,
5135 > {
5136 use #root::sql::FetcherPool as _;
5137 let _key = ::std::format!("{}__minute", col);
5138 #root::query::QuerySet::<Self>::default()
5139 .filter(&_key, #root::core::SqlValue::I64(minute))
5140 .fetch(pool)
5141 .await
5142 }
5143
5144 /// Fetch every row where `<col> LIKE <pattern>` —
5145 /// caller-supplied pattern (must include `%` / `_`
5146 /// wildcards manually). Eloquent `Model::whereLike`
5147 /// parity.
5148 ///
5149 /// # Errors
5150 /// As [`FetcherPool::fetch`].
5151 ///
5152 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5153 pub async fn where_like(
5154 col: &str,
5155 pattern: impl ::core::convert::Into<::std::string::String>,
5156 pool: &#root::sql::Pool,
5157 ) -> ::core::result::Result<
5158 ::std::vec::Vec<Self>,
5159 #root::sql::ExecError,
5160 > {
5161 use #root::sql::FetcherPool as _;
5162 let _key = ::std::format!("{}__like", col);
5163 #root::query::QuerySet::<Self>::default()
5164 .filter(
5165 &_key,
5166 #root::core::SqlValue::String(pattern.into()),
5167 )
5168 .fetch(pool)
5169 .await
5170 }
5171
5172 /// Fetch every row where `<col> ILIKE <pattern>` —
5173 /// case-insensitive LIKE (PG native, MySQL/SQLite
5174 /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5175 ///
5176 /// # Errors
5177 /// As [`FetcherPool::fetch`].
5178 ///
5179 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5180 pub async fn where_ilike(
5181 col: &str,
5182 pattern: impl ::core::convert::Into<::std::string::String>,
5183 pool: &#root::sql::Pool,
5184 ) -> ::core::result::Result<
5185 ::std::vec::Vec<Self>,
5186 #root::sql::ExecError,
5187 > {
5188 use #root::sql::FetcherPool as _;
5189 let _key = ::std::format!("{}__ilike", col);
5190 #root::query::QuerySet::<Self>::default()
5191 .filter(
5192 &_key,
5193 #root::core::SqlValue::String(pattern.into()),
5194 )
5195 .fetch(pool)
5196 .await
5197 }
5198
5199 /// Fetch every row where `<col>` starts with `prefix`
5200 /// (auto-appends `%`). Django `__startswith` / Eloquent
5201 /// `whereLike("col", "$prefix%")` parity.
5202 ///
5203 /// # Errors
5204 /// As [`FetcherPool::fetch`].
5205 ///
5206 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5207 pub async fn where_starts_with(
5208 col: &str,
5209 prefix: impl ::core::convert::Into<::std::string::String>,
5210 pool: &#root::sql::Pool,
5211 ) -> ::core::result::Result<
5212 ::std::vec::Vec<Self>,
5213 #root::sql::ExecError,
5214 > {
5215 use #root::sql::FetcherPool as _;
5216 let _key = ::std::format!("{}__startswith", col);
5217 #root::query::QuerySet::<Self>::default()
5218 .filter(
5219 &_key,
5220 #root::core::SqlValue::String(prefix.into()),
5221 )
5222 .fetch(pool)
5223 .await
5224 }
5225
5226 /// Fetch every row where `<col>` ends with `suffix`
5227 /// (auto-prepends `%`). Django `__endswith` / Eloquent
5228 /// `whereLike("col", "%$suffix")` parity.
5229 ///
5230 /// # Errors
5231 /// As [`FetcherPool::fetch`].
5232 ///
5233 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5234 pub async fn where_ends_with(
5235 col: &str,
5236 suffix: impl ::core::convert::Into<::std::string::String>,
5237 pool: &#root::sql::Pool,
5238 ) -> ::core::result::Result<
5239 ::std::vec::Vec<Self>,
5240 #root::sql::ExecError,
5241 > {
5242 use #root::sql::FetcherPool as _;
5243 let _key = ::std::format!("{}__endswith", col);
5244 #root::query::QuerySet::<Self>::default()
5245 .filter(
5246 &_key,
5247 #root::core::SqlValue::String(suffix.into()),
5248 )
5249 .fetch(pool)
5250 .await
5251 }
5252
5253 /// Fetch every row where `<col>` contains `substr`
5254 /// (auto-wraps with `%`). Django `__contains` /
5255 /// Eloquent `whereLike("col", "%$substr%")` parity.
5256 ///
5257 /// # Errors
5258 /// As [`FetcherPool::fetch`].
5259 ///
5260 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5261 pub async fn where_contains(
5262 col: &str,
5263 substr: impl ::core::convert::Into<::std::string::String>,
5264 pool: &#root::sql::Pool,
5265 ) -> ::core::result::Result<
5266 ::std::vec::Vec<Self>,
5267 #root::sql::ExecError,
5268 > {
5269 use #root::sql::FetcherPool as _;
5270 let _key = ::std::format!("{}__contains", col);
5271 #root::query::QuerySet::<Self>::default()
5272 .filter(
5273 &_key,
5274 #root::core::SqlValue::String(substr.into()),
5275 )
5276 .fetch(pool)
5277 .await
5278 }
5279
5280 /// Fetch every row where `<col> > val`. Eloquent
5281 /// `Model::where($col, ">", $val)->get()` parity.
5282 ///
5283 /// # Errors
5284 /// As [`FetcherPool::fetch`].
5285 ///
5286 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5287 pub async fn where_gt(
5288 col: &str,
5289 val: impl ::core::convert::Into<#root::core::SqlValue>,
5290 pool: &#root::sql::Pool,
5291 ) -> ::core::result::Result<
5292 ::std::vec::Vec<Self>,
5293 #root::sql::ExecError,
5294 > {
5295 use #root::sql::FetcherPool as _;
5296 let _key = ::std::format!("{}__gt", col);
5297 #root::query::QuerySet::<Self>::default()
5298 .filter(&_key, val)
5299 .fetch(pool)
5300 .await
5301 }
5302
5303 /// Fetch every row where `<col> >= val`. Eloquent
5304 /// `Model::where($col, ">=", $val)->get()` parity.
5305 ///
5306 /// # Errors
5307 /// As [`FetcherPool::fetch`].
5308 ///
5309 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5310 pub async fn where_gte(
5311 col: &str,
5312 val: impl ::core::convert::Into<#root::core::SqlValue>,
5313 pool: &#root::sql::Pool,
5314 ) -> ::core::result::Result<
5315 ::std::vec::Vec<Self>,
5316 #root::sql::ExecError,
5317 > {
5318 use #root::sql::FetcherPool as _;
5319 let _key = ::std::format!("{}__gte", col);
5320 #root::query::QuerySet::<Self>::default()
5321 .filter(&_key, val)
5322 .fetch(pool)
5323 .await
5324 }
5325
5326 /// Fetch every row where `<col> < val`. Eloquent
5327 /// `Model::where($col, "<", $val)->get()` parity.
5328 ///
5329 /// # Errors
5330 /// As [`FetcherPool::fetch`].
5331 ///
5332 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5333 pub async fn where_lt(
5334 col: &str,
5335 val: impl ::core::convert::Into<#root::core::SqlValue>,
5336 pool: &#root::sql::Pool,
5337 ) -> ::core::result::Result<
5338 ::std::vec::Vec<Self>,
5339 #root::sql::ExecError,
5340 > {
5341 use #root::sql::FetcherPool as _;
5342 let _key = ::std::format!("{}__lt", col);
5343 #root::query::QuerySet::<Self>::default()
5344 .filter(&_key, val)
5345 .fetch(pool)
5346 .await
5347 }
5348
5349 /// Fetch every row where `<col> <= val`. Eloquent
5350 /// `Model::where($col, "<=", $val)->get()` parity.
5351 ///
5352 /// # Errors
5353 /// As [`FetcherPool::fetch`].
5354 ///
5355 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5356 pub async fn where_lte(
5357 col: &str,
5358 val: impl ::core::convert::Into<#root::core::SqlValue>,
5359 pool: &#root::sql::Pool,
5360 ) -> ::core::result::Result<
5361 ::std::vec::Vec<Self>,
5362 #root::sql::ExecError,
5363 > {
5364 use #root::sql::FetcherPool as _;
5365 let _key = ::std::format!("{}__lte", col);
5366 #root::query::QuerySet::<Self>::default()
5367 .filter(&_key, val)
5368 .fetch(pool)
5369 .await
5370 }
5371
5372 /// Fetch every row where `<col> <> val`. Eloquent
5373 /// `Model::where($col, "!=", $val)->get()` parity.
5374 ///
5375 /// # Errors
5376 /// As [`FetcherPool::fetch`].
5377 ///
5378 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5379 pub async fn where_ne(
5380 col: &str,
5381 val: impl ::core::convert::Into<#root::core::SqlValue>,
5382 pool: &#root::sql::Pool,
5383 ) -> ::core::result::Result<
5384 ::std::vec::Vec<Self>,
5385 #root::sql::ExecError,
5386 > {
5387 use #root::sql::FetcherPool as _;
5388 let _key = ::std::format!("{}__ne", col);
5389 #root::query::QuerySet::<Self>::default()
5390 .filter(&_key, val)
5391 .fetch(pool)
5392 .await
5393 }
5394
5395 /// Fetch every row matching `<col> = val` for ANY of the
5396 /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5397 /// parity. Empty `cols` returns no rows.
5398 ///
5399 /// Resolves each `&str` column to its SCHEMA-registered
5400 /// `&'static str` once and builds a single OR-composed
5401 /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5402 ///
5403 /// # Errors
5404 /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5405 /// when any column is not declared on the model.
5406 ///
5407 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5408 pub async fn where_any(
5409 cols: &[&str],
5410 val: impl ::core::convert::Into<#root::core::SqlValue>,
5411 pool: &#root::sql::Pool,
5412 ) -> ::core::result::Result<
5413 ::std::vec::Vec<Self>,
5414 #root::sql::ExecError,
5415 > {
5416 Self::__where_multi(cols, val, false, pool).await
5417 }
5418
5419 /// Fetch every row matching `<col> = val` for ALL listed
5420 /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5421 /// Empty `cols` returns every row (vacuous AND).
5422 ///
5423 /// # Errors
5424 /// As [`Self::where_any`].
5425 pub async fn where_all(
5426 cols: &[&str],
5427 val: impl ::core::convert::Into<#root::core::SqlValue>,
5428 pool: &#root::sql::Pool,
5429 ) -> ::core::result::Result<
5430 ::std::vec::Vec<Self>,
5431 #root::sql::ExecError,
5432 > {
5433 Self::__where_multi(cols, val, true, pool).await
5434 }
5435
5436 /// Internal: build a Q expression composing `cols` via
5437 /// AND (`all`) or OR (`!all`), then fetch. Backs
5438 /// `where_any` / `where_all`.
5439 #[doc(hidden)]
5440 pub async fn __where_multi(
5441 cols: &[&str],
5442 val: impl ::core::convert::Into<#root::core::SqlValue>,
5443 all: bool,
5444 pool: &#root::sql::Pool,
5445 ) -> ::core::result::Result<
5446 ::std::vec::Vec<Self>,
5447 #root::sql::ExecError,
5448 > {
5449 #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5450 .await
5451 }
5452
5453 /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5454 /// parity / Django `Model.objects.all()[:n]`. PK-ordered
5455 /// is NOT guaranteed without an explicit `order_by` —
5456 /// drop into `Self::query()` for that.
5457 ///
5458 /// # Errors
5459 /// As [`FetcherPool::fetch`].
5460 ///
5461 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5462 pub async fn take(
5463 n: i64,
5464 pool: &#root::sql::Pool,
5465 ) -> ::core::result::Result<
5466 ::std::vec::Vec<Self>,
5467 #root::sql::ExecError,
5468 > {
5469 use #root::sql::FetcherPool as _;
5470 #root::query::QuerySet::<Self>::default()
5471 .limit(n)
5472 .fetch(pool)
5473 .await
5474 }
5475
5476 /// Fetch the page-th window of `per_page` rows
5477 /// (1-indexed). Eloquent
5478 /// `Model::query()->forPage($page, $perPage)->get()`
5479 /// parity. The DB scans an `OFFSET (page - 1) * per_page
5480 /// LIMIT per_page`; for large offsets this is O(N) —
5481 /// prefer keyset pagination via PK on hot paths.
5482 ///
5483 /// # Errors
5484 /// As [`FetcherPool::fetch`].
5485 ///
5486 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5487 pub async fn for_page(
5488 page: i64,
5489 per_page: i64,
5490 pool: &#root::sql::Pool,
5491 ) -> ::core::result::Result<
5492 ::std::vec::Vec<Self>,
5493 #root::sql::ExecError,
5494 > {
5495 use #root::sql::FetcherPool as _;
5496 let _offset = if page > 1 { (page - 1) * per_page } else { 0 };
5497 #root::query::QuerySet::<Self>::default()
5498 .limit(per_page)
5499 .offset(_offset)
5500 .fetch(pool)
5501 .await
5502 }
5503
5504 /// Eloquent `Model::paginate($per_page, $page)` — fetch
5505 /// one page of rows AND the total row count in one
5506 /// call. Returns `(rows, total)`. Two queries: a
5507 /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5508 /// for the total.
5509 ///
5510 /// Useful for paginated UIs that need both the visible
5511 /// rows AND a "Page X of Y" / total-count widget. For
5512 /// large tables prefer keyset pagination + cached count;
5513 /// every call to `paginate` re-counts the full table.
5514 ///
5515 /// Same 1-indexed `page` convention as [`Self::for_page`].
5516 ///
5517 /// # Errors
5518 /// As [`Self::for_page`] and [`Self::count`].
5519 pub async fn paginate(
5520 page: i64,
5521 per_page: i64,
5522 pool: &#root::sql::Pool,
5523 ) -> ::core::result::Result<
5524 (::std::vec::Vec<Self>, i64),
5525 #root::sql::ExecError,
5526 > {
5527 // Route through the queryset rather than `Self::count`:
5528 // the `count` inherent method is suppressed on models
5529 // that declare a field named `count` (field/shortcut
5530 // collision guard), and `paginate` must still compile
5531 // for those models.
5532 let total = {
5533 use #root::sql::CounterPool as _;
5534 #root::query::QuerySet::<Self>::default()
5535 .count(pool)
5536 .await?
5537 };
5538 let rows = Self::for_page(page, per_page, pool).await?;
5539 ::core::result::Result::Ok((rows, total))
5540 }
5541
5542 /// Bulk-update — set `set_col = set_val` on every row
5543 /// matching `where_col = where_val`. Returns affected row
5544 /// count. Eloquent
5545 /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5546 /// parity.
5547 ///
5548 /// For multi-column updates drop into the queryset
5549 /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5550 ///
5551 /// # Errors
5552 /// As [`UpdaterPool::execute_pool`].
5553 ///
5554 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5555 pub async fn update_where(
5556 where_col: &str,
5557 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5558 set_col: &str,
5559 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5560 pool: &#root::sql::Pool,
5561 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5562 use #root::sql::UpdaterPool as _;
5563 #root::query::QuerySet::<Self>::default()
5564 .filter(where_col, where_val)
5565 .update()
5566 .set(set_col, set_val)
5567 .execute_pool(pool)
5568 .await
5569 }
5570
5571 /// Bulk-delete — remove every row matching
5572 /// `where_col = where_val`. Returns affected row count.
5573 /// Eloquent
5574 /// `Model::where($where_col, $where_val)->delete()` parity.
5575 ///
5576 /// For more complex filters drop into the queryset
5577 /// builder + `Self::query().filter(...).delete().execute_pool(&pool)`.
5578 ///
5579 /// # Errors
5580 /// As [`#root::sql::delete_pool`].
5581 pub async fn delete_where(
5582 where_col: &str,
5583 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5584 pool: &#root::sql::Pool,
5585 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5586 let _query = #root::core::DeleteQuery {
5587 model: <Self as #root::core::Model>::SCHEMA,
5588 where_clause: #root::core::WhereExpr::Predicate(
5589 #root::core::Filter {
5590 column: <Self as #root::core::Model>::SCHEMA
5591 .field(where_col)
5592 .ok_or_else(|| {
5593 #root::sql::ExecError::Query(
5594 #root::core::QueryError::UnknownField {
5595 model: <Self as #root::core::Model>::SCHEMA.name,
5596 field: ::std::string::ToString::to_string(where_col),
5597 },
5598 )
5599 })?
5600 .column,
5601 op: #root::core::Op::Eq,
5602 value: ::core::convert::Into::into(where_val),
5603 },
5604 ),
5605 };
5606 #root::sql::delete_pool(pool, &_query).await
5607 }
5608
5609 /// Bulk-update — set `set_col = set_val` on EVERY row of
5610 /// this model's table. **No WHERE clause** — use with
5611 /// care. Eloquent `Model::query()->update([$col => $val])`
5612 /// parity.
5613 ///
5614 /// Use for backfills, one-shot reset flows, etc. The
5615 /// returned count is rows affected.
5616 ///
5617 /// # Errors
5618 /// As [`UpdaterPool::execute_pool`].
5619 ///
5620 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5621 pub async fn update_all(
5622 set_col: &str,
5623 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5624 pool: &#root::sql::Pool,
5625 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5626 use #root::sql::UpdaterPool as _;
5627 #root::query::QuerySet::<Self>::default()
5628 .update()
5629 .set(set_col, set_val)
5630 .execute_pool(pool)
5631 .await
5632 }
5633
5634 /// Fetch every row where `<col> NOT LIKE <pattern>`.
5635 /// Eloquent `Model::whereNotLike` parity. Pattern is
5636 /// passed verbatim — caller controls `%` / `_`.
5637 ///
5638 /// # Errors
5639 /// As [`FetcherPool::fetch`].
5640 ///
5641 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5642 pub async fn where_not_like(
5643 col: &str,
5644 pattern: impl ::core::convert::Into<::std::string::String>,
5645 pool: &#root::sql::Pool,
5646 ) -> ::core::result::Result<
5647 ::std::vec::Vec<Self>,
5648 #root::sql::ExecError,
5649 > {
5650 use #root::sql::FetcherPool as _;
5651 let _key = ::std::format!("{}__not_like", col);
5652 #root::query::QuerySet::<Self>::default()
5653 .filter(
5654 &_key,
5655 #root::core::SqlValue::String(pattern.into()),
5656 )
5657 .fetch(pool)
5658 .await
5659 }
5660
5661 /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5662 /// case-insensitive `NOT LIKE` (PG native, MySQL /
5663 /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5664 ///
5665 /// # Errors
5666 /// As [`FetcherPool::fetch`].
5667 ///
5668 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5669 pub async fn where_not_ilike(
5670 col: &str,
5671 pattern: impl ::core::convert::Into<::std::string::String>,
5672 pool: &#root::sql::Pool,
5673 ) -> ::core::result::Result<
5674 ::std::vec::Vec<Self>,
5675 #root::sql::ExecError,
5676 > {
5677 use #root::sql::FetcherPool as _;
5678 let _key = ::std::format!("{}__not_ilike", col);
5679 #root::query::QuerySet::<Self>::default()
5680 .filter(
5681 &_key,
5682 #root::core::SqlValue::String(pattern.into()),
5683 )
5684 .fetch(pool)
5685 .await
5686 }
5687
5688 /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5689 /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5690 /// parity.
5691 ///
5692 /// # Errors
5693 /// As [`FetcherPool::fetch`].
5694 ///
5695 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5696 pub async fn where_not_between(
5697 col: &str,
5698 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5699 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5700 pool: &#root::sql::Pool,
5701 ) -> ::core::result::Result<
5702 ::std::vec::Vec<Self>,
5703 #root::sql::ExecError,
5704 > {
5705 use #root::sql::FetcherPool as _;
5706 let _key = ::std::format!("{}__not_between", col);
5707 let _vals = #root::core::SqlValue::List(::std::vec![
5708 ::core::convert::Into::into(lo),
5709 ::core::convert::Into::into(hi),
5710 ]);
5711 #root::query::QuerySet::<Self>::default()
5712 .filter(&_key, _vals)
5713 .fetch(pool)
5714 .await
5715 }
5716
5717 /// Returns the SQL table name for this model. Eloquent
5718 /// `$model->getTable()` parity.
5719 #[must_use]
5720 pub fn table_name() -> &'static str {
5721 <Self as #root::core::Model>::SCHEMA.table
5722 }
5723
5724 /// Returns the SQL column name of this model's primary
5725 /// key, or `None` when the model has no
5726 /// `#[rustango(primary_key)]`. Eloquent
5727 /// `$model->getKeyName()` parity.
5728 #[must_use]
5729 pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5730 <Self as #root::core::Model>::SCHEMA
5731 .primary_key()
5732 .map(|f| f.column)
5733 }
5734
5735 /// Fetch every row where `<col> BETWEEN lo AND hi`
5736 /// (inclusive on both ends — same as SQL). Eloquent
5737 /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5738 ///
5739 /// # Errors
5740 /// As [`FetcherPool::fetch`].
5741 ///
5742 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5743 pub async fn where_between(
5744 col: &str,
5745 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5746 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5747 pool: &#root::sql::Pool,
5748 ) -> ::core::result::Result<
5749 ::std::vec::Vec<Self>,
5750 #root::sql::ExecError,
5751 > {
5752 use #root::sql::FetcherPool as _;
5753 let _key = ::std::format!("{}__between", col);
5754 let _vals = #root::core::SqlValue::List(::std::vec![
5755 ::core::convert::Into::into(lo),
5756 ::core::convert::Into::into(hi),
5757 ]);
5758 #root::query::QuerySet::<Self>::default()
5759 .filter(&_key, _vals)
5760 .fetch(pool)
5761 .await
5762 }
5763
5764 /// Fetch the first row where `<col> = <val>`. Returns
5765 /// `Ok(None)` when no row matches. Eloquent
5766 /// `Model::firstWhere($col, $val)` / Django
5767 /// `Model.objects.filter(col=val).first()` parity.
5768 ///
5769 /// Thin wrapper over `QuerySet::<Self>::default()
5770 /// .filter(col, val).first(pool)`. Use this when you
5771 /// want one row identified by a non-PK column (e.g.
5772 /// `User::first_where_pool("email", "x@y.com", &pool)`).
5773 ///
5774 /// `val` accepts any value `Into<SqlValue>` so plain
5775 /// strings, ints, UUIDs, etc. all work.
5776 ///
5777 /// # Errors
5778 /// As `QuerySet::first`.
5779 pub async fn first_where(
5780 col: &str,
5781 val: impl ::core::convert::Into<#root::core::SqlValue>,
5782 pool: &#root::sql::Pool,
5783 ) -> ::core::result::Result<
5784 ::core::option::Option<Self>,
5785 #root::sql::ExecError,
5786 > {
5787 #root::query::QuerySet::<Self>::default()
5788 .filter(col, val)
5789 .first(pool)
5790 .await
5791 }
5792
5793 #value_method
5794
5795 /// Fetch the row with the largest `field` value —
5796 /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
5797 /// `Ok(None)` for an empty table. Eloquent
5798 /// `Model::latest($field)->first()` / Django
5799 /// `Model.objects.latest(field)` (non-throwing) parity.
5800 /// Thin wrapper over `QuerySet::<Self>::default()
5801 /// .latest(field, pool)`.
5802 ///
5803 /// **Field name** is the Rust field ident as a string
5804 /// (not the SQL column). Unknown fields surface as
5805 /// `ExecError::Query(QueryError::UnknownField)` at
5806 /// compile time.
5807 ///
5808 /// # Errors
5809 /// As `QuerySet::latest`.
5810 pub async fn latest(
5811 field: &str,
5812 pool: &#root::sql::Pool,
5813 ) -> ::core::result::Result<
5814 ::core::option::Option<Self>,
5815 #root::sql::ExecError,
5816 > {
5817 #root::query::QuerySet::<Self>::default()
5818 .latest(field, pool)
5819 .await
5820 }
5821
5822 /// Sibling of [`Self::latest_pool`] — fetches the row
5823 /// with the smallest `field` value (`ORDER BY <field>
5824 /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
5825 /// ->first()` / Django `Model.objects.earliest(field)`
5826 /// parity.
5827 ///
5828 /// # Errors
5829 /// As [`Self::latest_pool`].
5830 pub async fn earliest(
5831 field: &str,
5832 pool: &#root::sql::Pool,
5833 ) -> ::core::result::Result<
5834 ::core::option::Option<Self>,
5835 #root::sql::ExecError,
5836 > {
5837 #root::query::QuerySet::<Self>::default()
5838 .earliest(field, pool)
5839 .await
5840 }
5841
5842 #count_method
5843
5844 /// `true` when the table contains at least one row.
5845 /// Eloquent `Model::query()->exists()` / Django
5846 /// `Model.objects.exists()` parity. Thin wrapper over
5847 /// `QuerySet::<Self>::default().exists(pool)`.
5848 ///
5849 /// # Errors
5850 /// As [`ExistsPool::exists`].
5851 ///
5852 /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
5853 pub async fn exists(
5854 pool: &#root::sql::Pool,
5855 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5856 use #root::sql::ExistsPool as _;
5857 #root::query::QuerySet::<Self>::default()
5858 .exists(pool)
5859 .await
5860 }
5861
5862 /// Inverse of [`Self::exists`] — returns `true` when
5863 /// the table has zero rows. Eloquent
5864 /// `Model::doesntExist()` parity.
5865 ///
5866 /// # Errors
5867 /// As [`Self::exists`].
5868 pub async fn doesnt_exist(
5869 pool: &#root::sql::Pool,
5870 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5871 Self::exists(pool).await.map(|e| !e)
5872 }
5873
5874 /// Eloquent `Model::query()->whereKey($pk)->exists()` —
5875 /// `true` when a row with primary key `pk` exists in
5876 /// the table. Sugar over
5877 /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
5878 ///
5879 /// Differs from [`Self::exists`] (which checks "any row
5880 /// in the table") by checking a specific PK existence.
5881 /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
5882 /// because the row is never materialized — the SQL is
5883 /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
5884 ///
5885 /// # Errors
5886 /// As [`#root::sql::ExistsPool::contains_pk`].
5887 pub async fn contains_pk(
5888 pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
5889 pool: &#root::sql::Pool,
5890 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5891 use #root::sql::ExistsPool as _;
5892 #root::query::QuerySet::<Self>::default()
5893 .contains_pk(pool, pk)
5894 .await
5895 }
5896
5897 #sum_method
5898 #avg_method
5899 #min_method
5900 #max_method
5901
5902 /// Internal: forward to
5903 /// [`#root::sql::model_shortcuts::aggregate_one_pool`].
5904 /// Backs `sum` / `avg` / `min` / `max`.
5905 #[doc(hidden)]
5906 pub async fn __aggregate_one_pool<U>(
5907 col: &str,
5908 build: fn(&'static str) -> #root::core::AggregateExpr,
5909 pool: &#root::sql::Pool,
5910 ) -> ::core::result::Result<
5911 ::core::option::Option<U>,
5912 #root::sql::ExecError,
5913 >
5914 where
5915 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
5916 + #root::sql::MaybeMyFromRow
5917 + #root::sql::MaybeSqliteFromRow
5918 + ::core::marker::Send
5919 + ::core::marker::Unpin,
5920 {
5921 #root::sql::model_shortcuts::aggregate_one_pool::<Self, U>(col, build, pool)
5922 .await
5923 }
5924
5925 /// Fetch every row of this model from `pool`. Eloquent
5926 /// `Model::all()` parity — a thin wrapper over
5927 /// `QuerySet::<Self>::default().fetch(pool)`.
5928 ///
5929 /// **Use with care on large tables** — there's no
5930 /// pagination or limit; the entire table is materialized
5931 /// into memory. For anything beyond fixture / lookup
5932 /// tables, page through `QuerySet::<Self>::default()
5933 /// .order_by(...).limit(N).offset(M).fetch(pool)`
5934 /// or stream via `.iterator(chunk_size)`.
5935 ///
5936 /// # Errors
5937 /// As [`FetcherPool::fetch`].
5938 ///
5939 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5940 pub async fn all(
5941 pool: &#root::sql::Pool,
5942 ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
5943 {
5944 use #root::sql::FetcherPool as _;
5945 #root::query::QuerySet::<Self>::default()
5946 .fetch(pool)
5947 .await
5948 }
5949
5950 /// Look up every row whose primary key is in `pks`.
5951 /// Returns the matching rows in **inventory** order — NOT
5952 /// the order of `pks`. Empty `pks` returns an empty
5953 /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
5954 /// with a list) / Django `Model.objects.filter(pk__in=[...])`
5955 /// parity.
5956 ///
5957 /// Thin wrapper over `QuerySet::<Self>::default()
5958 /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
5959 /// Caller-supplied PKs that don't match a row are
5960 /// silently skipped (the returned vec is shorter than
5961 /// the input list). For an order-preserving / "fail
5962 /// when any missing" variant, build the queryset
5963 /// explicitly with `in_bulk` instead.
5964 ///
5965 /// Accepts any iterable whose elements are
5966 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
5967 /// `Vec<Uuid>`, etc.
5968 ///
5969 /// # Errors
5970 /// As [`FetcherPool::fetch`].
5971 ///
5972 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5973 pub async fn find_many<V>(
5974 pks: impl ::core::iter::IntoIterator<Item = V>,
5975 pool: &#root::sql::Pool,
5976 ) -> ::core::result::Result<
5977 ::std::vec::Vec<Self>,
5978 #root::sql::ExecError,
5979 >
5980 where
5981 V: ::core::convert::Into<#root::core::SqlValue>,
5982 {
5983 use #root::sql::FetcherPool as _;
5984 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5985 pks.into_iter().map(::core::convert::Into::into).collect();
5986 if _values.is_empty() {
5987 return ::core::result::Result::Ok(::std::vec::Vec::new());
5988 }
5989 let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
5990 #root::query::QuerySet::<Self>::default()
5991 .filter(&_key, #root::core::SqlValue::List(_values))
5992 .fetch(pool)
5993 .await
5994 }
5995
5996 /// Look up the row whose primary key equals `pk`. Returns
5997 /// `Ok(None)` when no row matches; this is the
5998 /// non-throwing counterpart of Django's `.get(pk=…)`
5999 /// (which raises `DoesNotExist`). Eloquent `Model::find`
6000 /// shape — accepts any value `Into<SqlValue>`.
6001 ///
6002 /// One-liner shortcut for the common
6003 /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
6004 /// .limit(1).fetch(pool).await?.into_iter().next()`
6005 /// dance.
6006 ///
6007 /// # Errors
6008 /// As [`FetcherPool::fetch`].
6009 ///
6010 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6011 pub async fn find(
6012 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6013 pool: &#root::sql::Pool,
6014 ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6015 {
6016 use #root::sql::FetcherPool as _;
6017 let _pk_val: #root::core::SqlValue = pk.into();
6018 let mut _rows: ::std::vec::Vec<Self> =
6019 #root::query::QuerySet::<Self>::default()
6020 .filter(::core::stringify!(#pk_ident), _pk_val)
6021 .limit(1)
6022 .fetch(pool)
6023 .await?;
6024 ::core::result::Result::Ok(_rows.into_iter().next())
6025 }
6026
6027 /// Look up the row whose primary key equals `pk`. Errors
6028 /// when no row matches — the throwing counterpart of
6029 /// [`Self::find_pool`]. Eloquent `Model::findOrFail` /
6030 /// Django `Model.objects.get(pk=…)` (which raises
6031 /// `DoesNotExist`) parity.
6032 ///
6033 /// Translates the miss into
6034 /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6035 /// so callers can `?`-bubble straight through the typical
6036 /// `ExecError` error chain.
6037 ///
6038 /// # Errors
6039 /// As [`Self::find_pool`]; additionally
6040 /// [`sqlx::Error::RowNotFound`] when no row matches.
6041 ///
6042 /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6043 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6044 pub async fn find_or_fail(
6045 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6046 pool: &#root::sql::Pool,
6047 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6048 match Self::find(pk, pool).await? {
6049 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6050 ::core::option::Option::None => ::core::result::Result::Err(
6051 #root::sql::ExecError::Driver(
6052 #root::sql::sqlx::Error::RowNotFound,
6053 ),
6054 ),
6055 }
6056 }
6057
6058 /// Look up multiple rows by primary key, **failing** if
6059 /// any requested PK is missing. Eloquent
6060 /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6061 /// inventory order (NOT request order — same as
6062 /// [`Self::find_many`]).
6063 ///
6064 /// Empty `pks` returns an empty `Vec` (no rows requested
6065 /// → nothing to fail on).
6066 ///
6067 /// Differs from [`Self::find_many`] in that this method
6068 /// requires every PK to resolve. If even one is missing,
6069 /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6070 /// in `ExecError::Driver`). Rows are deduped at the SQL
6071 /// layer, so passing the same PK twice in `pks` counts as
6072 /// one expected row.
6073 ///
6074 /// # Errors
6075 /// As [`Self::find_many`], plus `RowNotFound` when the
6076 /// returned row count is less than the count of distinct
6077 /// requested PKs.
6078 pub async fn find_many_or_fail<V>(
6079 pks: impl ::core::iter::IntoIterator<Item = V>,
6080 pool: &#root::sql::Pool,
6081 ) -> ::core::result::Result<
6082 ::std::vec::Vec<Self>,
6083 #root::sql::ExecError,
6084 >
6085 where
6086 V: ::core::convert::Into<#root::core::SqlValue>,
6087 {
6088 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6089 pks.into_iter().map(::core::convert::Into::into).collect();
6090 if _values.is_empty() {
6091 return ::core::result::Result::Ok(::std::vec::Vec::new());
6092 }
6093 // Dedup the requested PK list before counting so
6094 // duplicate-PK requests don't false-fail (the SQL
6095 // `IN (...)` clause naturally dedups too).
6096 let mut _seen: ::std::collections::HashSet<
6097 ::std::string::String,
6098 > = ::std::collections::HashSet::new();
6099 for v in &_values {
6100 _seen.insert(v.to_display_string());
6101 }
6102 let _expected = _seen.len();
6103 let _rows = Self::find_many(_values, pool).await?;
6104 if _rows.len() < _expected {
6105 return ::core::result::Result::Err(
6106 #root::sql::ExecError::Driver(
6107 #root::sql::sqlx::Error::RowNotFound,
6108 ),
6109 );
6110 }
6111 ::core::result::Result::Ok(_rows)
6112 }
6113
6114 /// Find by primary key or run `fallback` to produce a
6115 /// default row to return. Eloquent
6116 /// `Model::findOr($pk, fn() => …)` parity.
6117 ///
6118 /// Unlike [`Self::find_or_fail_pool`] (which raises on
6119 /// miss), this is the "give me something sensible"
6120 /// branch: typical use is "fetch the user's row, else
6121 /// fall back to an anonymous/guest stub".
6122 ///
6123 /// `fallback` runs only when no row matches — the DB
6124 /// round-trip happens unconditionally.
6125 ///
6126 /// # Errors
6127 /// As [`Self::find_pool`].
6128 pub async fn find_or<F>(
6129 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6130 pool: &#root::sql::Pool,
6131 fallback: F,
6132 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6133 where
6134 F: ::core::ops::FnOnce() -> Self,
6135 {
6136 ::core::result::Result::Ok(
6137 Self::find(pk, pool).await?.unwrap_or_else(fallback),
6138 )
6139 }
6140
6141 /// Same as [`Self::find_or`] but also returns a `bool`
6142 /// indicating whether the row was found in the DB
6143 /// (`true`) or freshly built from `fallback` (`false`).
6144 /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6145 /// in PHP the returned model also exposes `->exists`;
6146 /// here that's surfaced as the second tuple element.
6147 ///
6148 /// Useful for edit-or-create form handlers where the
6149 /// caller needs to know whether to PATCH or POST when
6150 /// the user submits the form.
6151 ///
6152 /// # Errors
6153 /// As [`Self::find`].
6154 pub async fn find_or_new<F>(
6155 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6156 pool: &#root::sql::Pool,
6157 fallback: F,
6158 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6159 where
6160 F: ::core::ops::FnOnce() -> Self,
6161 {
6162 match Self::find(pk, pool).await? {
6163 ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6164 ::core::option::Option::None => {
6165 ::core::result::Result::Ok((fallback(), false))
6166 }
6167 }
6168 }
6169
6170 /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6171 /// [`Self::find_or_new`] but **persists** the new row
6172 /// when the PK isn't found. Returns `(row, exists: bool)`
6173 /// — `exists=true` when the PK was found,
6174 /// `false` when a fresh row was inserted.
6175 ///
6176 /// ```ignore
6177 /// let (post, found) = Post::find_or_insert(
6178 /// pk,
6179 /// &pool,
6180 /// || Post { id: Auto::default(), title: "new".into() },
6181 /// ).await?;
6182 /// // `found` true → returned existing row;
6183 /// // `found` false → fallback was inserted, `post.id` populated.
6184 /// ```
6185 ///
6186 /// **Caveat**: two concurrent calls that both miss the
6187 /// find can both INSERT, violating uniqueness. Wrap in a
6188 /// transaction or rely on a UNIQUE constraint + handle
6189 /// the conflict error if you need race-free semantics.
6190 ///
6191 /// # Errors
6192 /// As [`Self::find`] and [`Self::save_pool`].
6193 pub async fn find_or_insert<F>(
6194 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6195 pool: &#root::sql::Pool,
6196 fallback: F,
6197 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6198 where
6199 F: ::core::ops::FnOnce() -> Self,
6200 {
6201 if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6202 return ::core::result::Result::Ok((_row, true));
6203 }
6204 let mut _new = fallback();
6205 _new.save_pool(pool).await?;
6206 ::core::result::Result::Ok((_new, false))
6207 }
6208
6209 /// Fetch the first row of the table, or run `fallback`
6210 /// when the table is empty. Eloquent
6211 /// `Model::firstOr(fn() => …)` parity.
6212 ///
6213 /// # Errors
6214 /// As [`Self::first_pool`].
6215 pub async fn first_or<F>(
6216 pool: &#root::sql::Pool,
6217 fallback: F,
6218 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6219 where
6220 F: ::core::ops::FnOnce() -> Self,
6221 {
6222 // Route through the queryset rather than `Self::first`,
6223 // which is suppressed on models with a field named
6224 // `first` (field/shortcut collision guard).
6225 ::core::result::Result::Ok(
6226 #root::query::QuerySet::<Self>::default()
6227 .first(pool)
6228 .await?
6229 .unwrap_or_else(fallback),
6230 )
6231 }
6232
6233 /// Fetch exactly one row matching `<col> = <val>`. Errors
6234 /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6235 /// or more than one matches
6236 /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6237 /// Eloquent `Model::sole($col, $val)` parity.
6238 ///
6239 /// # Errors
6240 /// As [`Self::where_pool`] plus the explicit
6241 /// `RowNotFound` / `MultipleRowsReturned` cases above.
6242 pub async fn sole(
6243 col: &str,
6244 val: impl ::core::convert::Into<#root::core::SqlValue>,
6245 pool: &#root::sql::Pool,
6246 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6247 let mut _rows = Self::where_(col, val, pool).await?;
6248 match _rows.len() {
6249 0 => ::core::result::Result::Err(
6250 #root::sql::ExecError::Driver(
6251 #root::sql::sqlx::Error::RowNotFound,
6252 ),
6253 ),
6254 1 => ::core::result::Result::Ok(_rows.remove(0)),
6255 n => ::core::result::Result::Err(
6256 #root::sql::ExecError::MultipleRowsReturned {
6257 op: "sole",
6258 table: <Self as #root::core::Model>::SCHEMA.name,
6259 count: n,
6260 },
6261 ),
6262 }
6263 }
6264 }
6265 } else {
6266 quote!()
6267 };
6268
6269 // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6270 // the non-audited `_pool` methods but execute against an open
6271 // `PoolTx` so the writes participate in the caller's transaction.
6272 // Auditing inside TX is deferred; these always use the plain
6273 // executor primitives regardless of whether the model is audited.
6274 let tx_insert_method = if fields.has_auto {
6275 let pushes = &fields.insert_pushes;
6276 let returning_cols = &fields.returning_cols;
6277 quote! {
6278 /// Insert this row inside an open transaction, populating
6279 /// any `Auto<T>` PK from the auto-assigned value. Works
6280 /// against any backend that `tx` wraps.
6281 ///
6282 /// # Errors
6283 /// As [`Self::insert_pool`].
6284 pub async fn insert_tx(
6285 &mut self,
6286 tx: &mut #root::sql::PoolTx<'_>,
6287 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6288 let mut _columns: ::std::vec::Vec<&'static str> =
6289 ::std::vec::Vec::new();
6290 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6291 ::std::vec::Vec::new();
6292 #( #pushes )*
6293 let _query = #root::core::InsertQuery {
6294 model: <Self as #root::core::Model>::SCHEMA,
6295 columns: _columns,
6296 values: _values,
6297 returning: ::std::vec![ #( #returning_cols ),* ],
6298 on_conflict: ::core::option::Option::None,
6299 };
6300 let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6301 #root::sql::apply_auto_pk(_result, self)
6302 }
6303 }
6304 } else {
6305 let insert_columns = &fields.insert_columns;
6306 let insert_values = &fields.insert_values;
6307 quote! {
6308 /// Insert this row inside an open transaction.
6309 ///
6310 /// # Errors
6311 /// As [`Self::insert_pool`].
6312 pub async fn insert_tx(
6313 &self,
6314 tx: &mut #root::sql::PoolTx<'_>,
6315 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6316 let _query = #root::core::InsertQuery {
6317 model: <Self as #root::core::Model>::SCHEMA,
6318 columns: ::std::vec![ #( #insert_columns ),* ],
6319 values: ::std::vec![ #( #insert_values ),* ],
6320 returning: ::std::vec::Vec::new(),
6321 on_conflict: ::core::option::Option::None,
6322 };
6323 #root::sql::insert_tx(tx, &_query).await
6324 }
6325 }
6326 };
6327
6328 let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6329 let pk_column_lit = pk_col.as_str();
6330 let assignments = &fields.update_assignments;
6331 let dispatch_unset = if fields.pk_is_auto {
6332 quote! {
6333 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6334 return self.insert_tx(tx).await.map(|()| 1u64);
6335 }
6336 }
6337 } else {
6338 quote!()
6339 };
6340 quote! {
6341 /// Save this row inside an open transaction. `INSERT` when
6342 /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6343 /// PK. Works against any backend that `tx` wraps.
6344 ///
6345 /// # Errors
6346 /// As [`Self::save_pool`].
6347 pub async fn save_tx(
6348 &mut self,
6349 tx: &mut #root::sql::PoolTx<'_>,
6350 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6351 #dispatch_unset
6352 let _query = #root::core::UpdateQuery {
6353 model: <Self as #root::core::Model>::SCHEMA,
6354 set: ::std::vec![ #( #assignments ),* ],
6355 where_clause: #root::core::WhereExpr::Predicate(
6356 #root::core::Filter {
6357 column: #pk_column_lit,
6358 op: #root::core::Op::Eq,
6359 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6360 ::core::clone::Clone::clone(&self.#pk_ident)
6361 ),
6362 }
6363 ),
6364 };
6365 let _affected = #root::sql::update_tx(tx, &_query).await?;
6366 ::core::result::Result::Ok(_affected)
6367 }
6368 }
6369 } else {
6370 quote!()
6371 };
6372
6373 let tx_delete_method = {
6374 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6375 let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6376 if let Some(pk_ident) = pk_ident_for_tx {
6377 quote! {
6378 /// Delete the row identified by this instance's PK
6379 /// inside an open transaction. Works against any backend
6380 /// that `tx` wraps.
6381 ///
6382 /// # Errors
6383 /// As [`Self::delete_pool`].
6384 pub async fn delete_tx(
6385 &self,
6386 tx: &mut #root::sql::PoolTx<'_>,
6387 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6388 let _query = #root::core::DeleteQuery {
6389 model: <Self as #root::core::Model>::SCHEMA,
6390 where_clause: #root::core::WhereExpr::Predicate(
6391 #root::core::Filter {
6392 column: #pk_column_lit,
6393 op: #root::core::Op::Eq,
6394 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6395 ::core::clone::Clone::clone(&self.#pk_ident)
6396 ),
6397 }
6398 ),
6399 };
6400 #root::sql::delete_tx(tx, &_query).await
6401 }
6402 }
6403 } else {
6404 quote!()
6405 }
6406 };
6407
6408 // Update emission captures both BEFORE and AFTER state — runs an
6409 // extra SELECT against `_executor` BEFORE the UPDATE, captures
6410 // each tracked field's prior value, then after the UPDATE diffs
6411 // against the in-memory `&self`. `diff_changes` drops unchanged
6412 // columns so the JSON only contains the actual delta.
6413 //
6414 // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6415 // and binds `_audit_before_pairs`; `audit_update_post` runs
6416 // after the UPDATE and emits the PendingEntry.
6417 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6418 audited_fields
6419 {
6420 if tracked.is_empty() {
6421 (quote!(), quote!())
6422 } else {
6423 let select_cols: String = tracked
6424 .iter()
6425 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
6426 .collect::<Vec<_>>()
6427 .join(", ");
6428 let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6429 let select_cols_lit = select_cols;
6430 let pk_column_lit_for_select = pk_column_for_select;
6431 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6432 if fields.pk_is_auto {
6433 quote!(self.#pk_ident.get().copied().unwrap_or_default())
6434 } else {
6435 quote!(::core::clone::Clone::clone(&self.#pk_ident))
6436 }
6437 } else {
6438 quote!(0_i64)
6439 };
6440 let before_pairs = tracked.iter().map(|c| {
6441 let column_lit = c.column.as_str();
6442 let value_ty = &c.value_ty;
6443 quote! {
6444 (
6445 #column_lit,
6446 match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6447 &_audit_before_row, #column_lit,
6448 ) {
6449 ::core::result::Result::Ok(v) => {
6450 #root::__serde_json::to_value(&v)
6451 .unwrap_or(#root::__serde_json::Value::Null)
6452 }
6453 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6454 },
6455 )
6456 }
6457 });
6458 let after_pairs = tracked.iter().map(|c| {
6459 let column_lit = c.column.as_str();
6460 let ident = &c.ident;
6461 quote! {
6462 (
6463 #column_lit,
6464 #root::__serde_json::to_value(&self.#ident)
6465 .unwrap_or(#root::__serde_json::Value::Null),
6466 )
6467 }
6468 });
6469 let pk_str = audit_pk_to_string.clone();
6470 let pre = quote! {
6471 let _audit_select_sql = ::std::format!(
6472 r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
6473 #select_cols_lit,
6474 <Self as #root::core::Model>::SCHEMA.table,
6475 #pk_column_lit_for_select,
6476 );
6477 let _audit_before_pairs:
6478 ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6479 match #root::sql::sqlx::query(&_audit_select_sql)
6480 .bind(#pk_value_for_bind)
6481 .fetch_optional(&mut *_executor)
6482 .await
6483 {
6484 ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
6485 ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
6486 }
6487 _ => ::core::option::Option::None,
6488 };
6489 };
6490 let post = quote! {
6491 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6492 let _audit_after:
6493 ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
6494 ::std::vec![ #( #after_pairs ),* ];
6495 let _audit_entry = #root::audit::PendingEntry {
6496 entity_table: <Self as #root::core::Model>::SCHEMA.table,
6497 entity_pk: #pk_str,
6498 operation: #root::audit::AuditOp::Update,
6499 source: #root::audit::current_source(),
6500 changes: #root::audit::diff_changes(
6501 &_audit_before,
6502 &_audit_after,
6503 ),
6504 };
6505 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6506 }
6507 };
6508 (pre, post)
6509 }
6510 } else {
6511 (quote!(), quote!())
6512 };
6513
6514 // Bulk-insert audit: capture every row's tracked fields after the
6515 // RETURNING populates each PK, then push one batched INSERT INTO
6516 // audit_log via `emit_many`. One round-trip regardless of N rows.
6517 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6518 let row_pk_str = if let Some((pk_ident, _)) = primary_key {
6519 if fields.pk_is_auto {
6520 quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
6521 } else {
6522 quote!(::std::format!("{}", &_row.#pk_ident))
6523 }
6524 } else {
6525 quote!(::std::string::String::new())
6526 };
6527 let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
6528 let column_lit = c.column.as_str();
6529 let ident = &c.ident;
6530 quote! {
6531 (
6532 #column_lit,
6533 #root::__serde_json::to_value(&_row.#ident)
6534 .unwrap_or(#root::__serde_json::Value::Null),
6535 )
6536 }
6537 });
6538 quote! {
6539 let _audit_source = #root::audit::current_source();
6540 let mut _audit_entries:
6541 ::std::vec::Vec<#root::audit::PendingEntry> =
6542 ::std::vec::Vec::with_capacity(rows.len());
6543 for _row in rows.iter() {
6544 _audit_entries.push(#root::audit::PendingEntry {
6545 entity_table: <Self as #root::core::Model>::SCHEMA.table,
6546 entity_pk: #row_pk_str,
6547 operation: #root::audit::AuditOp::Create,
6548 source: _audit_source.clone(),
6549 changes: #root::audit::snapshot_changes(&[
6550 #( #row_pairs ),*
6551 ]),
6552 });
6553 }
6554 #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6555 }
6556 } else {
6557 quote!()
6558 };
6559
6560 let save_method = if fields.pk_is_auto {
6561 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6562 let pk_column_lit = pk_column.as_str();
6563 let assignments = &fields.update_assignments;
6564 let upsert_cols = &fields.upsert_update_columns;
6565 let upsert_pushes = &fields.insert_pushes;
6566 let upsert_returning = &fields.returning_cols;
6567 let upsert_auto_assigns = &fields.auto_assigns;
6568 // Conflict target: prefer the first declared `unique_together`
6569 // when it exists. Plain `Auto<T>` PKs are server-assigned via
6570 // `BIGSERIAL` and never collide on insert, so a PK-only target
6571 // would silently turn `upsert()` into "always-insert" for
6572 // surrogate-PK models with composite UNIQUE constraints — see
6573 // `RolePermission` / `UserRole` / `UserPermission` in the
6574 // tenancy permission engine. When no `unique_together` is
6575 // declared we keep the PK target (the original behaviour).
6576 let upsert_target_columns: Vec<String> = indexes
6577 .iter()
6578 .find(|i| i.unique && !i.columns.is_empty())
6579 .map(|i| i.columns.clone())
6580 .unwrap_or_else(|| vec![pk_column.clone()]);
6581 let upsert_target_lits = upsert_target_columns
6582 .iter()
6583 .map(String::as_str)
6584 .collect::<Vec<_>>();
6585 let conflict_clause = if fields.upsert_update_columns.is_empty() {
6586 quote!(#root::core::ConflictClause::DoNothing)
6587 } else {
6588 quote!(#root::core::ConflictClause::DoUpdate {
6589 target: ::std::vec![ #( #upsert_target_lits ),* ],
6590 update_columns: ::std::vec![ #( #upsert_cols ),* ],
6591 })
6592 };
6593 Some(quote! {
6594 /// Insert this row if its `Auto<T>` primary key is
6595 /// `Unset`, otherwise update the existing row matching the
6596 /// PK. Mirrors Django's `save()` — caller doesn't need to
6597 /// pick `insert` vs the bulk-update path manually.
6598 ///
6599 /// On the insert branch, populates the PK from `RETURNING`
6600 /// (same behavior as `insert`). On the update branch,
6601 /// writes every non-PK column back; if no row matches the
6602 /// PK, returns `Ok(())` silently.
6603 ///
6604 /// Only generated when the primary key is declared as
6605 /// `Auto<T>`. Models with a manually-managed PK must use
6606 /// `insert` or the QuerySet update builder.
6607 ///
6608 /// **Postgres only — prefer [`Self::save_pool`].** This takes a
6609 /// driver-specific `PgPool`, so it does not exist at all on a
6610 /// build without the `postgres` feature: selecting `sqlite`
6611 /// makes the method vanish rather than fail with anything that
6612 /// names the cause.
6613 ///
6614 /// [`Self::save_pool`] takes `rustango::sql::Pool` and works on
6615 /// all three backends. The naming is inverted — the short name
6616 /// is the narrow one — and that is tracked in #1293.
6617 ///
6618 /// # Errors
6619 /// Returns [`#root::sql::ExecError`] for SQL-writing
6620 /// or driver failures.
6621 #[cfg(feature = "postgres")]
6622 pub async fn save(
6623 &mut self,
6624 pool: &#root::sql::sqlx::PgPool,
6625 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6626 #pool_to_save_on
6627 }
6628
6629 /// Like [`Self::save`] but accepts any sqlx executor —
6630 /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6631 /// escape hatch for tenant-scoped writes: schema-mode
6632 /// tenants share the registry pool but rely on a per-
6633 /// checkout `SET search_path`, so passing `&PgPool` would
6634 /// silently hit the wrong schema. Acquire a connection
6635 /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6636 ///
6637 /// # Errors
6638 /// As [`Self::save`].
6639 #[cfg(feature = "postgres")]
6640 pub async fn save_on #executor_generics (
6641 &mut self,
6642 #executor_param,
6643 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6644 #executor_where
6645 {
6646 // #1029 — INSERT writes exactly one row → 1; UPDATE returns
6647 // the rows-affected count (0 when the PK no longer exists,
6648 // the Django 6.0 `Model.NotUpdated` signal).
6649 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6650 return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
6651 }
6652 #audit_update_pre
6653 let _query = #root::core::UpdateQuery {
6654 model: <Self as #root::core::Model>::SCHEMA,
6655 set: ::std::vec![ #( #assignments ),* ],
6656 where_clause: #root::core::WhereExpr::Predicate(
6657 #root::core::Filter {
6658 column: #pk_column_lit,
6659 op: #root::core::Op::Eq,
6660 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6661 ::core::clone::Clone::clone(&self.#pk_ident)
6662 ),
6663 }
6664 ),
6665 };
6666 let _affected = #root::sql::__macro_internals::update_on(
6667 #executor_passes_to_data_write,
6668 &_query,
6669 ).await?;
6670 #audit_update_post
6671 ::core::result::Result::Ok(_affected)
6672 }
6673
6674 /// Per-call override for the audit source. Runs
6675 /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6676 /// scope so the resulting audit entry records `source`
6677 /// instead of the task-local default. Useful for seed
6678 /// scripts and one-off CLI tools that don't sit inside an
6679 /// admin handler. The override applies only to this call;
6680 /// no global state changes.
6681 ///
6682 /// # Errors
6683 /// As [`Self::save_on`].
6684 #[cfg(feature = "postgres")]
6685 pub async fn save_on_with #executor_generics (
6686 &mut self,
6687 #executor_param,
6688 source: #root::audit::AuditSource,
6689 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6690 #executor_where
6691 {
6692 #root::audit::with_source(source, self.save_on(_executor)).await
6693 }
6694
6695 /// Insert this row or update it in-place if the primary key already
6696 /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6697 ///
6698 /// With `Auto::Unset` PK the server assigns a new key and no conflict
6699 /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6700 /// inserted if absent or all non-PK columns are overwritten if present.
6701 ///
6702 /// # Errors
6703 /// As [`Self::insert_on`].
6704 #[cfg(feature = "postgres")]
6705 pub async fn upsert(
6706 &mut self,
6707 pool: &#root::sql::sqlx::PgPool,
6708 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6709 #pool_to_upsert_on
6710 }
6711
6712 /// Like [`Self::upsert`] but accepts any sqlx executor.
6713 /// See [`Self::save_on`] for tenancy-scoped rationale.
6714 ///
6715 /// # Errors
6716 /// As [`Self::upsert`].
6717 #[cfg(feature = "postgres")]
6718 pub async fn upsert_on #executor_generics (
6719 &mut self,
6720 #executor_param,
6721 ) -> ::core::result::Result<(), #root::sql::ExecError>
6722 #executor_where
6723 {
6724 let mut _columns: ::std::vec::Vec<&'static str> =
6725 ::std::vec::Vec::new();
6726 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6727 ::std::vec::Vec::new();
6728 #( #upsert_pushes )*
6729 let query = #root::core::InsertQuery {
6730 model: <Self as #root::core::Model>::SCHEMA,
6731 columns: _columns,
6732 values: _values,
6733 returning: ::std::vec![ #( #upsert_returning ),* ],
6734 on_conflict: ::core::option::Option::Some(#conflict_clause),
6735 };
6736 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
6737 #executor_passes_to_data_write,
6738 &query,
6739 ).await?;
6740 let _returning_row = &_returning_row_v;
6741 #( #upsert_auto_assigns )*
6742 ::core::result::Result::Ok(())
6743 }
6744 })
6745 } else {
6746 None
6747 };
6748
6749 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6750 let pk_column_lit = pk_column.as_str();
6751 // Optional `soft_delete_on` / `restore_on` companions when the
6752 // model has a `#[rustango(soft_delete)]` column. They land
6753 // alongside the regular `delete_on` so callers have both
6754 // options — a hard delete (audit-tracked as a real DELETE) and
6755 // a logical delete (audit-tracked as an UPDATE setting the
6756 // deleted_at column to NOW()).
6757 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6758 let col_lit = col;
6759 let sd_field_ident = fields
6760 .soft_delete_field_ident
6761 .clone()
6762 .expect("soft_delete_column without ident");
6763 quote! {
6764 /// Soft-delete this row by setting its
6765 /// `#[rustango(soft_delete)]` column to `NOW()`.
6766 /// Mirrors Django's `SoftDeleteModel.delete()` shape:
6767 /// the row stays in the table; query helpers can
6768 /// filter it out by checking the column for `IS NOT
6769 /// NULL`.
6770 ///
6771 /// # Errors
6772 /// As [`Self::delete`].
6773 #[cfg(feature = "postgres")]
6774 pub async fn soft_delete_on #executor_generics (
6775 &self,
6776 #executor_param,
6777 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6778 #executor_where
6779 {
6780 let _query = #root::core::UpdateQuery {
6781 model: <Self as #root::core::Model>::SCHEMA,
6782 set: ::std::vec![
6783 #root::core::Assignment {
6784 column: #col_lit,
6785 value: ::core::convert::Into::<#root::core::Expr>::into(
6786 ::core::convert::Into::<#root::core::SqlValue>::into(
6787 #root::__chrono::Utc::now()
6788 )
6789 ),
6790 },
6791 ],
6792 where_clause: #root::core::WhereExpr::Predicate(
6793 #root::core::Filter {
6794 column: #pk_column_lit,
6795 op: #root::core::Op::Eq,
6796 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6797 ::core::clone::Clone::clone(&self.#pk_ident)
6798 ),
6799 }
6800 ),
6801 };
6802 let _affected = #root::sql::__macro_internals::update_on(
6803 #executor_passes_to_data_write,
6804 &_query,
6805 ).await?;
6806 #audit_softdelete_emit
6807 ::core::result::Result::Ok(_affected)
6808 }
6809
6810 /// Inverse of [`Self::soft_delete_on`] — clears the
6811 /// soft-delete column back to NULL so the row is
6812 /// considered live again.
6813 ///
6814 /// # Errors
6815 /// As [`Self::delete`].
6816 #[cfg(feature = "postgres")]
6817 pub async fn restore_on #executor_generics (
6818 &self,
6819 #executor_param,
6820 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6821 #executor_where
6822 {
6823 let _query = #root::core::UpdateQuery {
6824 model: <Self as #root::core::Model>::SCHEMA,
6825 set: ::std::vec![
6826 #root::core::Assignment {
6827 column: #col_lit,
6828 value: ::core::convert::Into::<#root::core::Expr>::into(
6829 #root::core::SqlValue::Null
6830 ),
6831 },
6832 ],
6833 where_clause: #root::core::WhereExpr::Predicate(
6834 #root::core::Filter {
6835 column: #pk_column_lit,
6836 op: #root::core::Op::Eq,
6837 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6838 ::core::clone::Clone::clone(&self.#pk_ident)
6839 ),
6840 }
6841 ),
6842 };
6843 let _affected = #root::sql::__macro_internals::update_on(
6844 #executor_passes_to_data_write,
6845 &_query,
6846 ).await?;
6847 #audit_restore_emit
6848 ::core::result::Result::Ok(_affected)
6849 }
6850
6851 /// Tri-dialect counterpart of [`Self::soft_delete_on`]
6852 /// — takes [`#root::sql::Pool`] and dispatches per
6853 /// backend. Eloquent `Model::delete()` semantics on
6854 /// soft-delete-enabled models (closes #821 partial).
6855 ///
6856 /// Sets the `#[rustango(soft_delete)]` column to
6857 /// `NOW()` on every backend. Query helpers
6858 /// (`QuerySet::active()` / `only_trashed()`,
6859 /// `soft_delete::active_filter` /
6860 /// `compose_with_active`) filter trashed rows out by
6861 /// reading `IS NULL` on the same column.
6862 ///
6863 /// # Errors
6864 /// As [`#root::sql::update_pool`].
6865 pub async fn soft_delete(
6866 &self,
6867 pool: &#root::sql::Pool,
6868 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6869 let _query = #root::core::UpdateQuery {
6870 model: <Self as #root::core::Model>::SCHEMA,
6871 set: ::std::vec![
6872 #root::core::Assignment {
6873 column: #col_lit,
6874 value: ::core::convert::Into::<#root::core::Expr>::into(
6875 ::core::convert::Into::<#root::core::SqlValue>::into(
6876 #root::__chrono::Utc::now()
6877 )
6878 ),
6879 },
6880 ],
6881 where_clause: #root::core::WhereExpr::Predicate(
6882 #root::core::Filter {
6883 column: #pk_column_lit,
6884 op: #root::core::Op::Eq,
6885 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6886 ::core::clone::Clone::clone(&self.#pk_ident)
6887 ),
6888 }
6889 ),
6890 };
6891 #root::sql::update_pool(pool, &_query).await
6892 }
6893
6894 /// Tri-dialect counterpart of [`Self::restore_on`].
6895 /// Clears the `#[rustango(soft_delete)]` column back
6896 /// to `NULL`, marking the row live again. Eloquent
6897 /// `Model::restore()` parity.
6898 ///
6899 /// # Errors
6900 /// As [`#root::sql::update_pool`].
6901 pub async fn restore(
6902 &self,
6903 pool: &#root::sql::Pool,
6904 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6905 let _query = #root::core::UpdateQuery {
6906 model: <Self as #root::core::Model>::SCHEMA,
6907 set: ::std::vec![
6908 #root::core::Assignment {
6909 column: #col_lit,
6910 value: ::core::convert::Into::<#root::core::Expr>::into(
6911 #root::core::SqlValue::Null
6912 ),
6913 },
6914 ],
6915 where_clause: #root::core::WhereExpr::Predicate(
6916 #root::core::Filter {
6917 column: #pk_column_lit,
6918 op: #root::core::Op::Eq,
6919 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6920 ::core::clone::Clone::clone(&self.#pk_ident)
6921 ),
6922 }
6923 ),
6924 };
6925 #root::sql::update_pool(pool, &_query).await
6926 }
6927
6928 /// Hard-delete this row, ignoring the soft-delete
6929 /// column. Eloquent `Model::forceDelete()` parity —
6930 /// the escape hatch when you need to actually purge
6931 /// data (GDPR, fixture cleanup, etc.).
6932 ///
6933 /// Equivalent to [`Self::delete_pool`] (the framework's
6934 /// non-soft delete) — exposed under the Eloquent name
6935 /// for muscle-memory + so soft-delete-enabled models
6936 /// have all three operations (soft / restore / force)
6937 /// in one place.
6938 ///
6939 /// # Errors
6940 /// As [`Self::delete_pool`].
6941 pub async fn force_delete(
6942 &self,
6943 pool: &#root::sql::Pool,
6944 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6945 Self::delete_pool(self, pool).await
6946 }
6947
6948 /// Returns `true` when this row is soft-deleted (its
6949 /// `#[rustango(soft_delete)]` column is currently
6950 /// set — Eloquent `$model->trashed()` parity).
6951 ///
6952 /// Pure in-memory predicate over `&self`; does not
6953 /// hit the database. Useful in admin/template code
6954 /// like `{% if post.trashed() %}…{% endif %}` and in
6955 /// guard clauses on restore/force-delete flows.
6956 pub fn trashed(&self) -> bool {
6957 ::core::option::Option::is_some(&self.#sd_field_ident)
6958 }
6959
6960 /// Fetch every row whose `#[rustango(soft_delete)]`
6961 /// column is `NULL` (a.k.a. the live, non-trashed
6962 /// rows). Eloquent's default `Model::all()` behavior on
6963 /// a soft-delete model (Eloquent auto-scopes trashed
6964 /// rows out; rustango doesn't have auto-scoping yet —
6965 /// see issue #820 — so this is the explicit shortcut).
6966 ///
6967 /// One-liner over `QuerySet::<Self>::default()
6968 /// .active().fetch(pool)`. Closes #821 partial.
6969 ///
6970 /// # Errors
6971 /// As [`#root::sql::FetcherPool::fetch`].
6972 pub async fn active(
6973 pool: &#root::sql::Pool,
6974 ) -> ::core::result::Result<
6975 ::std::vec::Vec<Self>,
6976 #root::sql::ExecError,
6977 > {
6978 use #root::sql::FetcherPool as _;
6979 #root::query::QuerySet::<Self>::default()
6980 .active()
6981 .fetch(pool)
6982 .await
6983 }
6984
6985 /// Fetch ONLY soft-deleted rows. Eloquent
6986 /// `Model::onlyTrashed()->get()` parity — drives the
6987 /// admin "Trash" page, restore flows, GDPR purge
6988 /// scans, etc.
6989 ///
6990 /// One-liner over `QuerySet::<Self>::default()
6991 /// .only_trashed().fetch(pool)`. Closes #821
6992 /// partial.
6993 ///
6994 /// # Errors
6995 /// As [`#root::sql::FetcherPool::fetch`].
6996 pub async fn only_trashed(
6997 pool: &#root::sql::Pool,
6998 ) -> ::core::result::Result<
6999 ::std::vec::Vec<Self>,
7000 #root::sql::ExecError,
7001 > {
7002 use #root::sql::FetcherPool as _;
7003 #root::query::QuerySet::<Self>::default()
7004 .only_trashed()
7005 .fetch(pool)
7006 .await
7007 }
7008
7009 /// Fetch every row, both live and soft-deleted.
7010 /// Eloquent `Model::withTrashed()->get()` parity.
7011 ///
7012 /// Today every queryset already includes trashed rows
7013 /// (rustango has no global-scope tracking yet — issue
7014 /// #820), so this is functionally equivalent to
7015 /// [`Self::all_pool`]. Exposed as a named shortcut so
7016 /// soft-delete-aware code reads `Model::with_trashed_pool`
7017 /// rather than `Model::all_pool` — keeps intent visible
7018 /// in callers and stays correct when auto-scoping lands.
7019 ///
7020 /// Closes #821 partial.
7021 ///
7022 /// # Errors
7023 /// As [`#root::sql::FetcherPool::fetch`].
7024 pub async fn with_trashed(
7025 pool: &#root::sql::Pool,
7026 ) -> ::core::result::Result<
7027 ::std::vec::Vec<Self>,
7028 #root::sql::ExecError,
7029 > {
7030 use #root::sql::FetcherPool as _;
7031 #root::query::QuerySet::<Self>::default()
7032 .with_trashed()
7033 .fetch(pool)
7034 .await
7035 }
7036
7037 }
7038 } else {
7039 quote!()
7040 };
7041 quote! {
7042 /// Delete the row identified by this instance's primary key.
7043 ///
7044 /// Returns the number of rows affected (0 or 1).
7045 ///
7046 /// **Postgres only — prefer [`Self::delete_pool`].** This takes
7047 /// a driver-specific `PgPool`, so it does not exist on a build
7048 /// without the `postgres` feature. [`Self::delete_pool`] takes
7049 /// `rustango::sql::Pool` and works on all three. The naming is
7050 /// inverted; tracked in #1293.
7051 ///
7052 /// # Errors
7053 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7054 /// driver failures.
7055 #[cfg(feature = "postgres")]
7056 pub async fn delete(
7057 &self,
7058 pool: &#root::sql::sqlx::PgPool,
7059 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7060 #pool_to_delete_on
7061 }
7062
7063 /// Like [`Self::delete`] but accepts any sqlx executor —
7064 /// for tenant-scoped deletes against an explicitly-acquired
7065 /// connection. See [`Self::save_on`] for the rationale.
7066 ///
7067 /// # Errors
7068 /// As [`Self::delete`].
7069 #[cfg(feature = "postgres")]
7070 pub async fn delete_on #executor_generics (
7071 &self,
7072 #executor_param,
7073 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7074 #executor_where
7075 {
7076 let query = #root::core::DeleteQuery {
7077 model: <Self as #root::core::Model>::SCHEMA,
7078 where_clause: #root::core::WhereExpr::Predicate(
7079 #root::core::Filter {
7080 column: #pk_column_lit,
7081 op: #root::core::Op::Eq,
7082 value: ::core::convert::Into::<#root::core::SqlValue>::into(
7083 ::core::clone::Clone::clone(&self.#pk_ident)
7084 ),
7085 }
7086 ),
7087 };
7088 let _affected = #root::sql::__macro_internals::delete_on(
7089 #executor_passes_to_data_write,
7090 &query,
7091 ).await?;
7092 #audit_delete_emit
7093 ::core::result::Result::Ok(_affected)
7094 }
7095
7096 /// Per-call audit-source override for [`Self::delete_on`].
7097 /// See [`Self::save_on_with`] for shape rationale.
7098 ///
7099 /// # Errors
7100 /// As [`Self::delete_on`].
7101 #[cfg(feature = "postgres")]
7102 pub async fn delete_on_with #executor_generics (
7103 &self,
7104 #executor_param,
7105 source: #root::audit::AuditSource,
7106 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7107 #executor_where
7108 {
7109 #root::audit::with_source(source, self.delete_on(_executor)).await
7110 }
7111 #pool_delete_method
7112 #pool_insert_method
7113 #pool_save_method
7114 #refresh_replicate_methods
7115 #tx_delete_method
7116 #tx_insert_method
7117 #tx_save_method
7118 #soft_delete_methods
7119
7120 /// Returns `true` when `other` represents the same DB
7121 /// row as `self` — i.e. their primary keys compare
7122 /// equal. Eloquent `$model->is($other)` parity.
7123 ///
7124 /// Because both arguments are typed `&Self`, the
7125 /// model/table check is automatic — `Post::is` cannot
7126 /// be invoked against a `Comment` at compile time. Only
7127 /// the PK has to be compared at runtime.
7128 pub fn is(&self, other: &Self) -> bool {
7129 self.#pk_ident == other.#pk_ident
7130 }
7131
7132 /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7133 /// parity.
7134 pub fn is_not(&self, other: &Self) -> bool {
7135 self.#pk_ident != other.#pk_ident
7136 }
7137
7138 /// Returns this row's primary-key value as an
7139 /// [`#root::core::SqlValue`]. Eloquent
7140 /// `$model->getKey()` parity.
7141 ///
7142 /// Useful when you need to thread the PK through a
7143 /// generic `Into<SqlValue>`-bound API without knowing the
7144 /// concrete PK type (`i64` vs `Uuid` vs `String`).
7145 #[must_use]
7146 pub fn get_key(&self) -> #root::core::SqlValue {
7147 ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7148 }
7149
7150 }
7151 });
7152
7153 let insert_method = if fields.has_auto {
7154 let pushes = &fields.insert_pushes;
7155 let returning_cols = &fields.returning_cols;
7156 let auto_assigns = &fields.auto_assigns;
7157 quote! {
7158 /// Insert this row into its table. Skips columns whose
7159 /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7160 /// sequence fills them in, then reads each `Auto` column
7161 /// back via `RETURNING` and stores it on `self`.
7162 ///
7163 /// **Postgres only — prefer [`Self::insert_pool`].** This takes
7164 /// a driver-specific `PgPool`, so it does not exist on a build
7165 /// without the `postgres` feature. [`Self::insert_pool`] takes
7166 /// `rustango::sql::Pool` and works on all three. The naming is
7167 /// inverted; tracked in #1293.
7168 ///
7169 /// # Errors
7170 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7171 /// driver failures.
7172 #[cfg(feature = "postgres")]
7173 pub async fn insert(
7174 &mut self,
7175 pool: &#root::sql::sqlx::PgPool,
7176 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7177 #pool_to_insert_on
7178 }
7179
7180 /// Like [`Self::insert`] but accepts any sqlx executor.
7181 /// See [`Self::save_on`] for tenancy-scoped rationale.
7182 ///
7183 /// # Errors
7184 /// As [`Self::insert`].
7185 #[cfg(feature = "postgres")]
7186 pub async fn insert_on #executor_generics (
7187 &mut self,
7188 #executor_param,
7189 ) -> ::core::result::Result<(), #root::sql::ExecError>
7190 #executor_where
7191 {
7192 let mut _columns: ::std::vec::Vec<&'static str> =
7193 ::std::vec::Vec::new();
7194 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7195 ::std::vec::Vec::new();
7196 #( #pushes )*
7197 let query = #root::core::InsertQuery {
7198 model: <Self as #root::core::Model>::SCHEMA,
7199 columns: _columns,
7200 values: _values,
7201 returning: ::std::vec![ #( #returning_cols ),* ],
7202 on_conflict: ::core::option::Option::None,
7203 };
7204 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7205 #executor_passes_to_data_write,
7206 &query,
7207 ).await?;
7208 let _returning_row = &_returning_row_v;
7209 #( #auto_assigns )*
7210 #audit_insert_emit
7211 ::core::result::Result::Ok(())
7212 }
7213
7214 /// Per-call audit-source override for [`Self::insert_on`].
7215 /// See [`Self::save_on_with`] for shape rationale.
7216 ///
7217 /// # Errors
7218 /// As [`Self::insert_on`].
7219 #[cfg(feature = "postgres")]
7220 pub async fn insert_on_with #executor_generics (
7221 &mut self,
7222 #executor_param,
7223 source: #root::audit::AuditSource,
7224 ) -> ::core::result::Result<(), #root::sql::ExecError>
7225 #executor_where
7226 {
7227 #root::audit::with_source(source, self.insert_on(_executor)).await
7228 }
7229 }
7230 } else {
7231 let insert_columns = &fields.insert_columns;
7232 let insert_values = &fields.insert_values;
7233 quote! {
7234 /// Insert this row into its table.
7235 ///
7236 /// # Errors
7237 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7238 /// driver failures.
7239 #[cfg(feature = "postgres")]
7240 pub async fn insert(
7241 &self,
7242 pool: &#root::sql::sqlx::PgPool,
7243 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7244 self.insert_on(pool).await
7245 }
7246
7247 /// Like [`Self::insert`] but accepts any sqlx executor.
7248 /// See [`Self::save_on`] for tenancy-scoped rationale.
7249 ///
7250 /// # Errors
7251 /// As [`Self::insert`].
7252 #[cfg(feature = "postgres")]
7253 pub async fn insert_on<'_c, _E>(
7254 &self,
7255 _executor: _E,
7256 ) -> ::core::result::Result<(), #root::sql::ExecError>
7257 where
7258 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7259 {
7260 let query = #root::core::InsertQuery {
7261 model: <Self as #root::core::Model>::SCHEMA,
7262 columns: ::std::vec![ #( #insert_columns ),* ],
7263 values: ::std::vec![ #( #insert_values ),* ],
7264 returning: ::std::vec::Vec::new(),
7265 on_conflict: ::core::option::Option::None,
7266 };
7267 #root::sql::__macro_internals::insert_on(_executor, &query).await
7268 }
7269 }
7270 };
7271
7272 let bulk_insert_method = if fields.has_auto {
7273 let cols_no_auto = &fields.bulk_columns_no_auto;
7274 let cols_all = &fields.bulk_columns_all;
7275 let pushes_no_auto = &fields.bulk_pushes_no_auto;
7276 let pushes_all = &fields.bulk_pushes_all;
7277 let returning_cols = &fields.returning_cols;
7278 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7279 let uniformity = &fields.bulk_auto_uniformity;
7280 let first_auto_ident = fields
7281 .first_auto_ident
7282 .as_ref()
7283 .expect("has_auto implies first_auto_ident is Some");
7284 quote! {
7285 /// Bulk-insert `rows` in a single round-trip. Every row's
7286 /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7287 /// (sequence fills them in) or uniformly `Auto::Set(_)`
7288 /// (caller-supplied values). Mixed Set/Unset is rejected
7289 /// — call `insert` per row for that case.
7290 ///
7291 /// Empty slice is a no-op. Each row's `Auto` fields are
7292 /// populated from the `RETURNING` clause in input order
7293 /// before this returns.
7294 ///
7295 /// # Errors
7296 /// Returns [`#root::sql::ExecError`] for validation,
7297 /// SQL-writing, mixed-Auto rejection, or driver failures.
7298 #[cfg(feature = "postgres")]
7299 pub async fn bulk_insert(
7300 rows: &mut [Self],
7301 pool: &#root::sql::sqlx::PgPool,
7302 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7303 #pool_to_bulk_insert_on
7304 }
7305
7306 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7307 /// See [`Self::save_on`] for tenancy-scoped rationale.
7308 ///
7309 /// # Errors
7310 /// As [`Self::bulk_insert`].
7311 #[cfg(feature = "postgres")]
7312 pub async fn bulk_insert_on #executor_generics (
7313 rows: &mut [Self],
7314 #executor_param,
7315 ) -> ::core::result::Result<(), #root::sql::ExecError>
7316 #executor_where
7317 {
7318 if rows.is_empty() {
7319 return ::core::result::Result::Ok(());
7320 }
7321 let _first_unset = matches!(
7322 rows[0].#first_auto_ident,
7323 #root::sql::Auto::Unset
7324 );
7325 #( #uniformity )*
7326
7327 let mut _all_rows: ::std::vec::Vec<
7328 ::std::vec::Vec<#root::core::SqlValue>,
7329 > = ::std::vec::Vec::with_capacity(rows.len());
7330 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7331 for _row in rows.iter() {
7332 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7333 ::std::vec::Vec::new();
7334 #( #pushes_no_auto )*
7335 _all_rows.push(_row_vals);
7336 }
7337 ::std::vec![ #( #cols_no_auto ),* ]
7338 } else {
7339 for _row in rows.iter() {
7340 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7341 ::std::vec::Vec::new();
7342 #( #pushes_all )*
7343 _all_rows.push(_row_vals);
7344 }
7345 ::std::vec![ #( #cols_all ),* ]
7346 };
7347
7348 let _query = #root::core::BulkInsertQuery {
7349 model: <Self as #root::core::Model>::SCHEMA,
7350 columns: _columns,
7351 rows: _all_rows,
7352 returning: ::std::vec![ #( #returning_cols ),* ],
7353 on_conflict: ::core::option::Option::None,
7354 };
7355 let _returned = #root::sql::__macro_internals::bulk_insert_on(
7356 #executor_passes_to_data_write,
7357 &_query,
7358 ).await?;
7359 if _returned.len() != rows.len() {
7360 return ::core::result::Result::Err(
7361 #root::sql::ExecError::Sql(
7362 #root::sql::SqlError::BulkInsertReturningMismatch {
7363 expected: rows.len(),
7364 actual: _returned.len(),
7365 }
7366 )
7367 );
7368 }
7369 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7370 #auto_assigns_for_row
7371 }
7372 #audit_bulk_insert_emit
7373 ::core::result::Result::Ok(())
7374 }
7375 }
7376 } else {
7377 let cols_all = &fields.bulk_columns_all;
7378 let pushes_all = &fields.bulk_pushes_all;
7379 quote! {
7380 /// Bulk-insert `rows` in a single round-trip. Every row's
7381 /// fields are written verbatim — there are no `Auto<T>`
7382 /// fields on this model.
7383 ///
7384 /// Empty slice is a no-op.
7385 ///
7386 /// # Errors
7387 /// Returns [`#root::sql::ExecError`] for validation,
7388 /// SQL-writing, or driver failures.
7389 #[cfg(feature = "postgres")]
7390 pub async fn bulk_insert(
7391 rows: &[Self],
7392 pool: &#root::sql::sqlx::PgPool,
7393 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7394 Self::bulk_insert_on(rows, pool).await
7395 }
7396
7397 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7398 /// See [`Self::save_on`] for tenancy-scoped rationale.
7399 ///
7400 /// # Errors
7401 /// As [`Self::bulk_insert`].
7402 #[cfg(feature = "postgres")]
7403 pub async fn bulk_insert_on<'_c, _E>(
7404 rows: &[Self],
7405 _executor: _E,
7406 ) -> ::core::result::Result<(), #root::sql::ExecError>
7407 where
7408 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7409 {
7410 if rows.is_empty() {
7411 return ::core::result::Result::Ok(());
7412 }
7413 let mut _all_rows: ::std::vec::Vec<
7414 ::std::vec::Vec<#root::core::SqlValue>,
7415 > = ::std::vec::Vec::with_capacity(rows.len());
7416 for _row in rows.iter() {
7417 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7418 ::std::vec::Vec::new();
7419 #( #pushes_all )*
7420 _all_rows.push(_row_vals);
7421 }
7422 let _query = #root::core::BulkInsertQuery {
7423 model: <Self as #root::core::Model>::SCHEMA,
7424 columns: ::std::vec![ #( #cols_all ),* ],
7425 rows: _all_rows,
7426 returning: ::std::vec::Vec::new(),
7427 on_conflict: ::core::option::Option::None,
7428 };
7429 let _ = #root::sql::__macro_internals::bulk_insert_on(_executor, &_query).await?;
7430 ::core::result::Result::Ok(())
7431 }
7432 }
7433 };
7434
7435 // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7436 // (no postgres-feature gate); routes through the existing
7437 // `bulk_insert_pool` + per-dialect conflict writer.
7438 //
7439 // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7440 // sequence picks the PK for fresh inserts; the UPDATE branch never
7441 // touches the Auto column.
7442 let bulk_upsert_pool_method = {
7443 // Pick the "no Auto" columns when the model has Auto fields,
7444 // else every column.
7445 let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7446 (
7447 fields.bulk_columns_no_auto.clone(),
7448 fields.bulk_pushes_no_auto.clone(),
7449 )
7450 } else {
7451 (
7452 fields.bulk_columns_all.clone(),
7453 fields.bulk_pushes_all.clone(),
7454 )
7455 };
7456 quote! {
7457 /// Tri-dialect `bulk_create(update_conflicts=True)` — Django's
7458 /// canonical "import a batch idempotently" shape. Issue #267
7459 /// / T1.5.
7460 ///
7461 /// Per-row values are extracted and lowered into a
7462 /// [`#root::core::BulkInsertQuery`] with
7463 /// `on_conflict = DoUpdate { target, update_columns }`. The
7464 /// writer dispatches per-dialect:
7465 /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7466 /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)` (target ignored — MySQL matches every UNIQUE index)
7467 ///
7468 /// `target` names the column(s) whose unique constraint
7469 /// defines the conflict (typically a `unique` or
7470 /// `unique_together` natural-key column, NOT the `Auto<T>`
7471 /// PK). `update_cols` names the columns to overwrite on
7472 /// conflict — every other column is left untouched on the
7473 /// existing row.
7474 ///
7475 /// Auto-PK rows must all have `Auto::Unset` (the sequence
7476 /// picks the PK on insert; the update path never touches
7477 /// the Auto column). Auto-set rows trigger a hard error.
7478 /// Empty slice is a no-op.
7479 ///
7480 /// # Errors
7481 /// Returns [`#root::sql::ExecError`] for validation,
7482 /// SQL-writing, or driver failures.
7483 pub async fn bulk_upsert_pool(
7484 rows: &[Self],
7485 target: &[&'static str],
7486 update_cols: &[&'static str],
7487 pool: &#root::sql::Pool,
7488 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7489 if rows.is_empty() {
7490 return ::core::result::Result::Ok(());
7491 }
7492 let mut _all_rows: ::std::vec::Vec<
7493 ::std::vec::Vec<#root::core::SqlValue>,
7494 > = ::std::vec::Vec::with_capacity(rows.len());
7495 for _row in rows.iter() {
7496 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7497 ::std::vec::Vec::new();
7498 #( #upsert_pushes )*
7499 _all_rows.push(_row_vals);
7500 }
7501 let _query = #root::core::BulkInsertQuery {
7502 model: <Self as #root::core::Model>::SCHEMA,
7503 columns: ::std::vec![ #( #upsert_cols ),* ],
7504 rows: _all_rows,
7505 returning: ::std::vec::Vec::new(),
7506 on_conflict: ::core::option::Option::Some(
7507 #root::core::ConflictClause::DoUpdate {
7508 target: target.to_vec(),
7509 update_columns: update_cols.to_vec(),
7510 }
7511 ),
7512 };
7513 #root::sql::bulk_insert_pool(pool, &_query).await
7514 }
7515
7516 /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7517 /// skip rows that would violate a unique constraint. Issue
7518 /// #267 / T1.5. Same per-dialect dispatch as
7519 /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7520 /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7521 /// <pivot> = <pivot>` (MySQL no-op write).
7522 ///
7523 /// # Errors
7524 /// As [`Self::bulk_upsert_pool`].
7525 pub async fn bulk_insert_or_ignore_pool(
7526 rows: &[Self],
7527 pool: &#root::sql::Pool,
7528 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7529 if rows.is_empty() {
7530 return ::core::result::Result::Ok(());
7531 }
7532 let mut _all_rows: ::std::vec::Vec<
7533 ::std::vec::Vec<#root::core::SqlValue>,
7534 > = ::std::vec::Vec::with_capacity(rows.len());
7535 for _row in rows.iter() {
7536 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7537 ::std::vec::Vec::new();
7538 #( #upsert_pushes )*
7539 _all_rows.push(_row_vals);
7540 }
7541 let _query = #root::core::BulkInsertQuery {
7542 model: <Self as #root::core::Model>::SCHEMA,
7543 columns: ::std::vec![ #( #upsert_cols ),* ],
7544 rows: _all_rows,
7545 returning: ::std::vec::Vec::new(),
7546 on_conflict: ::core::option::Option::Some(
7547 #root::core::ConflictClause::DoNothing
7548 ),
7549 };
7550 #root::sql::bulk_insert_pool(pool, &_query).await
7551 }
7552 }
7553 };
7554
7555 // Ergonomic `Model::bulk_update(objs, fields)` — Django's
7556 // `QuerySet.bulk_update`. The SQL/IR/executor stack
7557 // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7558 // `write_bulk_update_*` writers) already existed; what was missing
7559 // was the per-model constructor that maps `&[Self]` + a runtime
7560 // column list into rows of `[pk, col_vals…]` so callers don't
7561 // hand-build the IR. Emitted only when the model has a primary key
7562 // (the PK is the join key and can't itself be updated).
7563 let bulk_update_method = match &fields.primary_key {
7564 None => quote! {},
7565 Some((pk_ident, pk_col)) => {
7566 // One pair of match arms per non-PK column: a validation arm
7567 // resolving the runtime name to its `&'static str` column,
7568 // and a value arm pushing that field off the row.
7569 let mut col_arms: Vec<TokenStream2> = Vec::new();
7570 let mut val_arms: Vec<TokenStream2> = Vec::new();
7571 for entry in &fields.column_entries {
7572 if &entry.column == pk_col {
7573 continue;
7574 }
7575 let col = &entry.column;
7576 let ident = &entry.ident;
7577 col_arms.push(quote! { #col => #col, });
7578 val_arms.push(quote! {
7579 #col => _row_vals.push(
7580 ::core::convert::Into::<#root::core::SqlValue>::into(
7581 ::core::clone::Clone::clone(&_o.#ident)
7582 )
7583 ),
7584 });
7585 }
7586 quote! {
7587 /// Django's `QuerySet.bulk_update(objs, fields)` — write
7588 /// per-row-different values for the named `fields` across
7589 /// every object in `objs` in a single statement, matched
7590 /// by primary key.
7591 ///
7592 /// `fields` names the **columns** to update. The primary
7593 /// key identifies each row and cannot itself be updated
7594 /// (pass it and you get
7595 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7596 /// Empty `objs` or `fields` is a no-op returning `0`.
7597 /// Objects whose PK matches no row are simply not updated.
7598 /// Returns the number of rows affected.
7599 ///
7600 /// Tri-dialect: lowers to one
7601 /// [`#root::core::BulkUpdateQuery`] and dispatches
7602 /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7603 /// a CTE + correlated subquery on SQLite, an inner
7604 /// `JOIN (VALUES …)` on MySQL.
7605 ///
7606 /// # Errors
7607 /// [`#root::core::QueryError::UnknownField`] for a name
7608 /// that isn't a column on this model,
7609 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7610 /// `fields` names the PK, or [`#root::sql::ExecError`] for
7611 /// SQL-writing / driver failures.
7612 pub async fn bulk_update(
7613 objs: &[Self],
7614 fields: &[&str],
7615 pool: &#root::sql::Pool,
7616 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7617 if objs.is_empty() || fields.is_empty() {
7618 return ::core::result::Result::Ok(0);
7619 }
7620 let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7621 let mut _update_columns: ::std::vec::Vec<&'static str> =
7622 ::std::vec::Vec::with_capacity(fields.len());
7623 for &_f in fields {
7624 let _col: &'static str = match _f {
7625 #pk_col => {
7626 return ::core::result::Result::Err(
7627 ::core::convert::Into::into(
7628 #root::core::QueryError::BulkUpdatePrimaryKey {
7629 model: _model_name,
7630 field: ::std::string::ToString::to_string(_f),
7631 }
7632 )
7633 );
7634 }
7635 #( #col_arms )*
7636 _ => {
7637 return ::core::result::Result::Err(
7638 ::core::convert::Into::into(
7639 #root::core::QueryError::UnknownField {
7640 model: _model_name,
7641 field: ::std::string::ToString::to_string(_f),
7642 }
7643 )
7644 );
7645 }
7646 };
7647 // A model whose only field is the primary key emits no
7648 // non-PK arms, so every arm above diverges and this push
7649 // is genuinely unreachable — correct, since such a model
7650 // has nothing to bulk-update. Scoped to the statement
7651 // rather than the fn so a real unreachability elsewhere
7652 // in this body still warns. Without it every PK-only
7653 // model warns in the *user's* crate, which they cannot
7654 // act on (#1290).
7655 #[allow(unreachable_code)]
7656 _update_columns.push(_col);
7657 }
7658 let mut _rows: ::std::vec::Vec<
7659 ::std::vec::Vec<#root::core::SqlValue>,
7660 > = ::std::vec::Vec::with_capacity(objs.len());
7661 for _o in objs.iter() {
7662 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7663 ::std::vec::Vec::with_capacity(fields.len() + 1);
7664 // PK first — the writers expect `[pk, …update cols]`.
7665 _row_vals.push(
7666 ::core::convert::Into::<#root::core::SqlValue>::into(
7667 ::core::clone::Clone::clone(&_o.#pk_ident)
7668 )
7669 );
7670 for &_f in fields {
7671 match _f {
7672 #( #val_arms )*
7673 // Unreachable: every name was validated
7674 // against the same arm set above.
7675 _ => {}
7676 }
7677 }
7678 _rows.push(_row_vals);
7679 }
7680 let _query = #root::core::BulkUpdateQuery {
7681 model: <Self as #root::core::Model>::SCHEMA,
7682 update_columns: _update_columns,
7683 rows: _rows,
7684 };
7685 #root::sql::bulk_update_pool(pool, &_query).await
7686 }
7687 }
7688 }
7689 };
7690
7691 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7692 quote! {
7693 /// Hidden runtime accessor for the primary-key value as a
7694 /// [`SqlValue`]. Used by reverse-relation helpers
7695 /// (`<parent>::<child>_set`) emitted from sibling models'
7696 /// FK fields. Not part of the public API.
7697 #[doc(hidden)]
7698 pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7699 ::core::convert::Into::<#root::core::SqlValue>::into(
7700 ::core::clone::Clone::clone(&self.#pk_ident)
7701 )
7702 }
7703 }
7704 });
7705
7706 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7707 quote! {
7708 impl #root::sql::HasPkValue for #struct_name {
7709 fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7710 ::core::convert::Into::<#root::core::SqlValue>::into(
7711 ::core::clone::Clone::clone(&self.#pk_ident)
7712 )
7713 }
7714 }
7715 }
7716 });
7717
7718 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7719
7720 // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7721 // dispatch to the right per-backend body without the macro emitting
7722 // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7723 // so audited models with non-Auto PKs (which still go through
7724 // `insert_one_with_audit` → `apply_auto_pk`) link.
7725 let assign_auto_pk_pool_impl = {
7726 let auto_assigns = &fields.auto_assigns;
7727 // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7728 // so the body is structurally identical to `auto_assigns` —
7729 // only the helper name swaps from `try_get_returning` to
7730 // `try_get_returning_sqlite` so the closure typechecks against
7731 // a `SqliteRow` instead of a `PgRow`.
7732 let auto_assigns_sqlite: Vec<TokenStream2> = fields
7733 .auto_field_idents
7734 .iter()
7735 .map(|(ident, column)| {
7736 quote! {
7737 self.#ident = #root::sql::try_get_returning_sqlite(
7738 _returning_row, #column
7739 )?;
7740 }
7741 })
7742 .collect();
7743 // #1028 — decode each `generated_as` column from the same
7744 // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7745 // own type drives the decode (no `Auto<T>` wrapper).
7746 let generated_assigns: Vec<TokenStream2> = fields
7747 .generated_field_idents
7748 .iter()
7749 .map(|(ident, column)| {
7750 quote! {
7751 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7752 }
7753 })
7754 .collect();
7755 let generated_assigns_sqlite: Vec<TokenStream2> = fields
7756 .generated_field_idents
7757 .iter()
7758 .map(|(ident, column)| {
7759 quote! {
7760 self.#ident = #root::sql::try_get_returning_sqlite(
7761 _returning_row, #column
7762 )?;
7763 }
7764 })
7765 .collect();
7766 let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
7767 // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7768 // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7769 // Auto<Uuid>/etc. fail to link against MySQL (intended —
7770 // those models can't use AUTO_INCREMENT). The trait is only
7771 // touched on the MySQL arm at runtime, so PG-only consumers
7772 // never see the bound failure.
7773 //
7774 // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7775 // Auto<i64> PK + auto_now_add timestamp) errored hard at
7776 // runtime with "multi-column RETURNING". MySQL has no
7777 // multi-column RETURNING semantic and a follow-up SELECT
7778 // would need cross-trait plumbing. Pragmatic shape: succeed
7779 // with the FIRST Auto field populated from LAST_INSERT_ID();
7780 // any other Auto fields stay `Auto::Unset`. Callers that
7781 // need the DB-defaulted timestamp / UUID can re-fetch the
7782 // row by PK after `save_pool`. Fixes the cookbook chapter
7783 // 12 dialect divergence.
7784 let value_ty = fields
7785 .first_auto_value_ty
7786 .as_ref()
7787 .expect("first_auto_value_ty set whenever first_auto_ident is");
7788 quote! {
7789 let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7790 ::rustango_from_mysql_auto_id(_id)?;
7791 self.#first = #root::sql::Auto::Set(_converted);
7792 ::core::result::Result::Ok(())
7793 }
7794 } else {
7795 quote! {
7796 let _ = _id;
7797 ::core::result::Result::Ok(())
7798 }
7799 };
7800 quote! {
7801 impl #root::sql::AssignAutoPkPool for #struct_name {
7802 fn __rustango_assign_from_pg_row(
7803 &mut self,
7804 _returning_row: &#root::sql::PgReturningRow,
7805 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7806 #( #auto_assigns )*
7807 #( #generated_assigns )*
7808 ::core::result::Result::Ok(())
7809 }
7810 fn __rustango_assign_from_mysql_id(
7811 &mut self,
7812 _id: i64,
7813 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7814 #mysql_body
7815 }
7816 fn __rustango_assign_from_sqlite_row(
7817 &mut self,
7818 _returning_row: &#root::sql::SqliteReturningRow,
7819 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7820 #( #auto_assigns_sqlite )*
7821 #( #generated_assigns_sqlite )*
7822 ::core::result::Result::Ok(())
7823 }
7824 }
7825 }
7826 };
7827
7828 let from_aliased_row_inits = &fields.from_aliased_row_inits;
7829 let aliased_row_helper = quote! {
7830 /// Decode a row's aliased target columns (produced by
7831 /// `select_related`'s LEFT JOIN) into a fresh instance of
7832 /// this model. Reads each column via
7833 /// `format!("{prefix}__{col}")`, matching the alias the
7834 /// SELECT writer emitted. Slice 9.0d.
7835 #[doc(hidden)]
7836 #[cfg(feature = "postgres")]
7837 pub fn __rustango_from_aliased_row(
7838 row: &#root::sql::sqlx::postgres::PgRow,
7839 prefix: &str,
7840 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7841 ::core::result::Result::Ok(Self {
7842 #( #from_aliased_row_inits ),*
7843 })
7844 }
7845 };
7846 // v0.23.0-batch8 — MySQL counterpart, gated through the
7847 // cfg-aware macro_rules so PG-only builds expand to nothing.
7848 let aliased_row_helper_my = quote! {
7849 #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7850 #( #from_aliased_row_inits ),*
7851 });
7852 };
7853
7854 // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7855 // pattern + cfg gate on the `sqlite` feature.
7856 let aliased_row_helper_sqlite = quote! {
7857 #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7858 #( #from_aliased_row_inits ),*
7859 });
7860 };
7861
7862 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7863 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7864 let load_related_impl_sqlite =
7865 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7866
7867 // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7868 // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7869 // default `Self::objects()`. Each accessor returns a fresh
7870 // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7871 // methods the user defined.
7872 let extra_manager_fns: Vec<TokenStream2> = manager_fns
7873 .iter()
7874 .map(|fn_ident| {
7875 let model_name_str = struct_name.to_string();
7876 let fn_name_str = fn_ident.to_string();
7877 let doc = format!(
7878 "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7879 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7880 equivalent to `Self::objects()`. Chains with any \
7881 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7882 extension methods."
7883 );
7884 quote! {
7885 #[doc = #doc]
7886 #[must_use]
7887 pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7888 #root::query::QuerySet::new()
7889 }
7890 }
7891 })
7892 .collect();
7893
7894 quote! {
7895 impl #struct_name {
7896 /// Start a new `QuerySet` over this model. Django shape.
7897 #[must_use]
7898 pub fn objects() -> #root::query::QuerySet<#struct_name> {
7899 #root::query::QuerySet::new()
7900 }
7901
7902 /// Eloquent-shape alias of [`Self::objects`]. Returns
7903 /// a fresh `QuerySet<Self>` ready for `.filter()` /
7904 /// `.where_()` / etc. Matches Laravel muscle-memory:
7905 ///
7906 /// ```ignore
7907 /// // Eloquent: Post::query()->where('published', true)
7908 /// // Django: Post.objects.filter(published=True)
7909 /// // rustango: Post::query().filter("published", true)
7910 /// // or: Post::objects().filter("published", true)
7911 /// ```
7912 ///
7913 /// Both names point at the same underlying constructor;
7914 /// neither is preferred.
7915 #[must_use]
7916 pub fn query() -> #root::query::QuerySet<#struct_name> {
7917 #root::query::QuerySet::new()
7918 }
7919
7920 #( #extra_manager_fns )*
7921
7922 #insert_method
7923
7924 #bulk_insert_method
7925
7926 #bulk_upsert_pool_method
7927
7928 #bulk_update_method
7929
7930 #save_method
7931
7932 #pk_methods
7933
7934 #pk_value_helper
7935
7936 #aliased_row_helper
7937
7938 #column_consts
7939 }
7940
7941 #aliased_row_helper_my
7942
7943 #aliased_row_helper_sqlite
7944
7945 #load_related_impl
7946
7947 #load_related_impl_my
7948
7949 #load_related_impl_sqlite
7950
7951 #has_pk_value_impl
7952
7953 #fk_pk_access_impl
7954
7955 #assign_auto_pk_pool_impl
7956 }
7957}
7958
7959/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
7960/// `auto_assigns` but reading from `_returning_row` and writing to
7961/// `_row_mut` instead of `self`.
7962fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
7963 let root = rustango_root();
7964 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
7965 let col_lit = column.as_str();
7966 quote! {
7967 _row_mut.#ident = #root::sql::sqlx::Row::try_get(
7968 _returning_row,
7969 #col_lit,
7970 )?;
7971 }
7972 });
7973 quote! { #( #lines )* }
7974}
7975
7976/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
7977fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
7978 let lines = entries.iter().map(|e| {
7979 let ident = &e.ident;
7980 let col_ty = column_type_ident(ident);
7981 quote! {
7982 #[allow(non_upper_case_globals)]
7983 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
7984 }
7985 });
7986 quote! { #(#lines)* }
7987}
7988
7989/// Emit a hidden per-model module carrying one zero-sized type per field,
7990/// each with a `Column` impl pointing back at the model.
7991fn column_module_tokens(
7992 module_ident: &syn::Ident,
7993 struct_name: &syn::Ident,
7994 entries: &[ColumnEntry],
7995) -> TokenStream2 {
7996 let root = rustango_root();
7997 let items = entries.iter().map(|e| {
7998 let col_ty = column_type_ident(&e.ident);
7999 let value_ty = &e.value_ty;
8000 let name = &e.name;
8001 let column = &e.column;
8002 let field_type_tokens = &e.field_type_tokens;
8003 quote! {
8004 #[derive(::core::clone::Clone, ::core::marker::Copy)]
8005 pub struct #col_ty;
8006
8007 impl #root::core::Column for #col_ty {
8008 type Model = super::#struct_name;
8009 type Value = #value_ty;
8010 const NAME: &'static str = #name;
8011 const COLUMN: &'static str = #column;
8012 const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
8013 }
8014 }
8015 });
8016 quote! {
8017 #[doc(hidden)]
8018 #[allow(non_camel_case_types, non_snake_case)]
8019 pub mod #module_ident {
8020 // Re-import the parent scope so field types referencing
8021 // sibling models (e.g. `ForeignKey<Author>`) resolve
8022 // inside this submodule. Without this we'd hit
8023 // `proc_macro_derive_resolution_fallback` warnings.
8024 #[allow(unused_imports)]
8025 use super::*;
8026 #(#items)*
8027 }
8028 }
8029}
8030
8031fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
8032 syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
8033}
8034
8035fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8036 syn::Ident::new(
8037 &format!("__rustango_cols_{struct_name}"),
8038 struct_name.span(),
8039 )
8040}
8041
8042fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
8043 let root = rustango_root();
8044 // The Postgres impl is always emitted — every rustango build pulls in
8045 // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8046 // routed through `#root::__impl_my_from_row!`, a cfg-gated
8047 // macro_rules whose body collapses to nothing when rustango is built
8048 // without the `mysql` feature. No user-facing feature shim required.
8049 //
8050 // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8051 // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8052 // back into a comma-separated list inside square brackets. Since each
8053 // entry is already in `field: expr` shape, the existing tokens slot in.
8054 quote! {
8055 #[cfg(feature = "postgres")]
8056 impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8057 for #struct_name
8058 {
8059 fn from_row(
8060 row: &'r #root::sql::sqlx::postgres::PgRow,
8061 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8062 ::core::result::Result::Ok(Self {
8063 #( #from_row_inits ),*
8064 })
8065 }
8066 }
8067
8068 #root::__impl_my_from_row!(#struct_name, |row| {
8069 #( #from_row_inits ),*
8070 });
8071
8072 #root::__impl_sqlite_from_row!(#struct_name, |row| {
8073 #( #from_row_inits ),*
8074 });
8075 }
8076}
8077
8078struct ContainerAttrs {
8079 table: Option<String>,
8080 display: Option<(String, proc_macro2::Span)>,
8081 /// Explicit Django-style app label from `#[rustango(app = "blog")]`.
8082 /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8083 /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8084 /// at runtime — this attribute is the override for cases where
8085 /// the inference is wrong (e.g. a model that conceptually belongs
8086 /// to one app but is physically in another module).
8087 app: Option<String>,
8088 /// Django ModelAdmin-shape per-model knobs from
8089 /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8090 /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8091 /// admin code falls back to `AdminConfig::DEFAULT`.
8092 admin: Option<AdminAttrs>,
8093 /// Per-model audit configuration from `#[rustango(audit(...))]`.
8094 /// `None` when the model isn't audited — write paths emit no
8095 /// audit entries. When present, single-row writes capture
8096 /// before/after for the listed fields and bulk writes batch
8097 /// snapshots into one INSERT into `rustango_audit_log`.
8098 audit: Option<AuditAttrs>,
8099 /// `true` when `#[rustango(permissions)]` is present. Signals that
8100 /// `auto_create_permissions` should seed the four CRUD codenames for
8101 /// this model.
8102 permissions: bool,
8103 /// Many-to-many relations declared via
8104 /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8105 /// src = "post_id", dst = "tag_id"))]`.
8106 m2m: Vec<M2MAttr>,
8107 /// Polymorphic M2M relations declared via
8108 /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8109 /// pk_column = "taggable_id", ct_column = "taggable_type",
8110 /// related_column = "tag_id"))]` (issue #818).
8111 generic_m2m: Vec<GenericM2MAttr>,
8112 /// Composite indexes declared via
8113 /// `#[rustango(index("col1, col2"))]` or
8114 /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8115 /// Single-column indexes from `#[rustango(index)]` on fields are
8116 /// accumulated here during field collection.
8117 indexes: Vec<IndexAttr>,
8118 /// Table-level CHECK constraints declared via
8119 /// `#[rustango(check(name = "…", expr = "…"))]`.
8120 checks: Vec<CheckAttr>,
8121 /// Table-level PG `EXCLUDE` constraints declared via
8122 /// `#[rustango(exclude(name = "…", using = "gist", elements =
8123 /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8124 /// migration writer renders them on Postgres and skips with a
8125 /// warning on MySQL/SQLite. Issue #319.
8126 excludes: Vec<ExcludeAttr>,
8127 /// Composite (multi-column) FKs declared via
8128 /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8129 /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8130 composite_fks: Vec<CompositeFkAttr>,
8131 /// Generic ("any model") FKs declared via
8132 /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8133 /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8134 generic_fks: Vec<GenericFkAttr>,
8135 /// Where this model lives in a tenancy deployment, declared via
8136 /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8137 /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8138 /// to partition diff output between registry-scoped and
8139 /// tenant-scoped migration files.
8140 scope: Option<String>,
8141 /// Custom-Manager extension-trait name from
8142 /// `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8143 /// When set, the macro emits an empty `pub trait <name>: Sized {}`
8144 /// adjacent to the model so users can write
8145 /// `impl FooManagerExt for QuerySet<Foo> { fn published(self) -> Self ... }`
8146 /// and discover the convention from the model definition.
8147 manager_ext: Option<syn::Ident>,
8148 /// Extra QuerySet accessor names from
8149 /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8150 /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8151 /// the default `Self::objects()`. Multiple attributes allowed.
8152 manager_fns: Vec<syn::Ident>,
8153 /// Default ordering declared via `#[rustango(default_order =
8154 /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8155 /// `(column_name, desc_flag, span_for_error_reporting)` — the
8156 /// `-` prefix means descending; the `+` prefix or no prefix means
8157 /// ascending.
8158 default_order: Vec<(String, bool, proc_macro2::Span)>,
8159 /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8160 /// Routes the emitted schema's `is_view = true` so the migration
8161 /// snapshot skips this model (its underlying SQL view is operator-
8162 /// managed, not rustango-managed).
8163 is_view: bool,
8164 /// Django-shape `Meta.managed` from `#[rustango(managed = false)]`.
8165 /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8166 /// the migration snapshot skips this model so `makemigrations` /
8167 /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8168 /// TABLE` against it (operator-managed schema).
8169 managed: bool,
8170 /// Django-shape `Meta.base_manager_name` from
8171 /// `#[rustango(base_manager_name = "...")]`. Threaded into
8172 /// `ModelSchema::base_manager_name`. Declarative-only today.
8173 base_manager_name: Option<String>,
8174 /// Django-shape `Meta.order_with_respect_to = "parent_fk"` from
8175 /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8176 /// field this model's instances are ordered relative to.
8177 /// Declarative-only today; threaded onto
8178 /// `ModelSchema::order_with_respect_to`.
8179 order_with_respect_to: Option<String>,
8180 /// Django-shape `Meta.proxy = True` from `#[rustango(proxy)]` /
8181 /// `#[rustango(proxy = true)]`. Marks the model as a proxy that
8182 /// shares its DB table with another struct. Threaded into
8183 /// `ModelSchema::proxy` so future codegen can skip table-owning
8184 /// behavior for proxies.
8185 proxy: bool,
8186 /// Django-shape `Meta.required_db_features` from
8187 /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8188 /// Each comma-separated capability token surfaces on
8189 /// `ModelSchema::required_db_features` so `manage check --deploy`
8190 /// can warn when the active dialect lacks one.
8191 required_db_features: Vec<String>,
8192 /// Django-shape `Meta.required_db_vendor` from
8193 /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8194 /// Normalized to the dialect name `manage check --deploy`
8195 /// compares against `Settings.database.backend`. Aliases
8196 /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8197 /// stored under the canonical name.
8198 required_db_vendor: Option<String>,
8199 /// Django-shape `Meta.default_related_name` from
8200 /// `#[rustango(default_related_name = "...")]`. Threaded into
8201 /// `ModelSchema::default_related_name`. Reverse-relation accessor
8202 /// name to use when an FK / M2M field doesn't override it.
8203 /// Today rustango doesn't auto-emit reverse managers; the
8204 /// metadata is the foundation for that work.
8205 default_related_name: Option<String>,
8206 /// Django-shape `Meta.db_table_comment` (4.2+) from
8207 /// `#[rustango(db_table_comment = "...")]`. Threaded into
8208 /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8209 /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8210 /// inline `COMMENT='...'`, SQLite: no-op).
8211 db_table_comment: Option<String>,
8212 /// Django-shape `Meta.get_latest_by` from
8213 /// `#[rustango(get_latest_by = "created_at")]` /
8214 /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8215 /// `(column, descending)` where `descending = true` when the
8216 /// attribute value starts with `-`. Threaded into
8217 /// `ModelSchema::get_latest_by`.
8218 get_latest_by: Option<(String, bool)>,
8219 /// Django-shape `Meta.permissions = [(codename, name), ...]`
8220 /// from `#[rustango(extra_permissions = "approve:Can approve,
8221 /// archive:Can archive")]`. Comma-separated `codename:label`
8222 /// pairs. Threaded into `ModelSchema::extra_permissions`.
8223 extra_permissions: Vec<(String, String)>,
8224 /// Django-shape `Meta.default_permissions` — which CRUD codenames
8225 /// (`"add"` / `"change"` / `"delete"` / `"view"`) the framework
8226 /// auto-creates. Empty `Vec` (default) means **all four** — matches
8227 /// Django's behavior when the operator omits the option. Set via
8228 /// `#[rustango(default_permissions = "view,change")]` to opt out.
8229 /// Validated at parse time; unknown actions fail with a span-pointing
8230 /// error.
8231 default_permissions: Vec<String>,
8232 /// `#[rustango(verbose_name = "blog post")]` — Django-shape
8233 /// human-readable singular label for the model. Threaded into
8234 /// `ModelSchema::verbose_name` so admin section headers /
8235 /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8236 /// over the Rust struct identifier.
8237 verbose_name: Option<String>,
8238 /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8239 /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8240 /// When unset, `display_label_plural()` auto-suffixes `s`.
8241 verbose_name_plural: Option<String>,
8242 /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8243 /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8244 /// a name (used by `QuerySet::without_global_scope`) with a
8245 /// `fn() -> WhereExpr` path that the macro emits into
8246 /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8247 global_scopes: Vec<GlobalScopeAttr>,
8248 /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8249 /// `#[rustango(through(name, far, far_fk_column, intermediate,
8250 /// intermediate_fk_column, intermediate_pk_column))]` — issue
8251 /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8252 /// entry emits an inherent `<name>_through(&self)` accessor that
8253 /// returns a `QuerySet<Far>` filtered via a correlated subquery
8254 /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8255 /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8256 through_relations: Vec<ThroughAttr>,
8257 /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8258 /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8259 /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8260 /// Each entry emits two associated functions on the parent —
8261 /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8262 /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8263 /// over a correlated subquery against the child table. Users
8264 /// drop the result into `QuerySet::where_raw(...)`.
8265 reverse_has_relations: Vec<ReverseHasAttr>,
8266 /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8267 /// issue #830. Each emits a `Model::generic_reverse_relations()`
8268 /// entry so the relation-existence family resolves polymorphic
8269 /// children by name.
8270 generic_has_relations: Vec<GenericHasAttr>,
8271}
8272
8273/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8274/// declaration. Each entry becomes one `core::GlobalScope` in the
8275/// emitted schema literal; `apply` resolves at macro-expand time
8276/// against the consumer's scope so the function must be in scope at
8277/// the use site. Issue #820.
8278struct GlobalScopeAttr {
8279 name: String,
8280 apply: syn::Path,
8281}
8282
8283/// Parsed `#[rustango(through(...))]` declaration. Issue
8284/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8285/// `hasManyThrough` / `hasOneThrough` parity.
8286///
8287/// `Country hasManyThrough Post via User` declares as:
8288///
8289/// ```ignore
8290/// #[rustango(through(
8291/// name = "posts",
8292/// far = "Post",
8293/// far_fk_column = "author_id",
8294/// intermediate = "User",
8295/// intermediate_fk_column = "country_id",
8296/// ))]
8297/// struct Country { ... }
8298/// ```
8299///
8300/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8301/// which returns a queryset filtered via a correlated subquery —
8302/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8303/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8304/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8305/// because the subquery lives inside a `WhereExpr::InSubquery` node
8306/// and the outer queryset's pending list stays empty.
8307///
8308/// All four required arguments use **SQL column / table names**
8309/// (not Rust field names) to sidestep the multi-hop-filter substrate
8310/// gap. Once that substrate lands, a higher-level Rust-field-name
8311/// shorthand can be added without breaking this surface.
8312struct ThroughAttr {
8313 /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8314 name: String,
8315 /// Far model type identifier. `far = "Post"` → returns
8316 /// `QuerySet<Post>`. Resolved verbatim against the scope where
8317 /// the derive expands.
8318 far: syn::Ident,
8319 /// SQL column on the far model's table that references the
8320 /// intermediate model's primary key. For `Post`'s
8321 /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8322 /// default `<field>_id` convention) or whatever the user
8323 /// declared via `#[rustango(db_column = "...")]`.
8324 far_fk_column: String,
8325 /// Intermediate model type identifier. Needed to look up its
8326 /// `SCHEMA` so the subquery's `FROM` clause points at the
8327 /// intermediate table. `intermediate = "User"`.
8328 intermediate: syn::Ident,
8329 /// SQL column on the intermediate's table that references the
8330 /// source (this) model's primary key. For `User`'s
8331 /// `country: ForeignKey<Country>` the column is `"country_id"`.
8332 intermediate_fk_column: String,
8333 /// SQL primary-key column on the intermediate's table — the
8334 /// column the subquery projects. Optional; defaults to `"id"`
8335 /// (rustango's default PK column name). Override when the
8336 /// intermediate declares a custom PK column.
8337 intermediate_pk_column: String,
8338}
8339
8340/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8341/// child_fk_column = "..."))]` declaration. Issue
8342/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8343/// `whereHas` / `whereDoesntHave` parity.
8344///
8345/// `Post hasMany Comment` declares as:
8346///
8347/// ```ignore
8348/// #[rustango(reverse_has(
8349/// name = "comments",
8350/// child = "Comment",
8351/// child_fk_column = "post_id",
8352/// ))]
8353/// struct Post { ... }
8354/// ```
8355///
8356/// The macro emits two associated functions on `Post`:
8357///
8358/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8359/// `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8360/// <outer>.<self_pk_column>)`.
8361/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8362/// but `NOT EXISTS`, the `whereDoesntHave` analog.
8363///
8364/// User code:
8365///
8366/// ```ignore
8367/// // Posts with at least one comment:
8368/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8369/// // Posts with no comments:
8370/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8371/// ```
8372///
8373/// As with #817, all column / table identifiers are **SQL names**
8374/// (not Rust field names) so the substrate is independent of the
8375/// outstanding multi-hop filter resolver gap. The emitted
8376/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8377/// SQL-emit time via the writer's scope stack.
8378struct ReverseHasAttr {
8379 /// Accessor name. `name = "comments"` → emits
8380 /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8381 name: String,
8382 /// Child model type identifier. `child = "Comment"` — needed to
8383 /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8384 /// points at the child table.
8385 child: syn::Ident,
8386 /// SQL column on the child's table that references this model's
8387 /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8388 /// column is `"post_id"`.
8389 child_fk_column: String,
8390 /// SQL primary-key column on **this** model's table — the column
8391 /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8392 /// (rustango's default PK column name).
8393 self_pk_column: String,
8394}
8395
8396/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8397/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8398/// relation-existence family (issue #830). The child is a polymorphic,
8399/// content-type-discriminated model; emits a `Model::
8400/// generic_reverse_relations()` entry the queryset resolves by name.
8401struct GenericHasAttr {
8402 /// Accessor name, e.g. `name = "tags"`.
8403 name: String,
8404 /// Child model type identifier (`child = "Tag"`) — looked up for its
8405 /// `SCHEMA` so the subquery's `FROM` points at the child table.
8406 child: syn::Ident,
8407 /// Column on the child table holding the parent's content-type id.
8408 /// Optional; defaults to `"content_type_id"`.
8409 ct_column: String,
8410 /// Column on the child table holding the parent's PK value.
8411 /// Optional; defaults to `"object_pk"`.
8412 pk_column: String,
8413 /// SQL primary-key column on **this** (parent) model's table.
8414 /// Optional; defaults to `"id"`.
8415 self_pk_column: String,
8416}
8417
8418/// Parsed form of one index declaration (field-level or container-level).
8419struct IndexAttr {
8420 /// Index name; auto-derived when `None` at parse time.
8421 name: Option<String>,
8422 /// Column names in the index.
8423 columns: Vec<String>,
8424 /// `true` for `CREATE UNIQUE INDEX`.
8425 unique: bool,
8426 /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8427 /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8428 /// `"btree"` when the attribute is absent — the DDL writer omits
8429 /// the `USING` clause and the backend uses its own default
8430 /// (btree on every supported dialect).
8431 method: String,
8432 /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8433 /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8434 /// condition = "...", name = "..."))]`. `None` for plain indexes.
8435 where_clause: Option<String>,
8436 /// Django `Index(fields=..., include=[...])` covering-index
8437 /// columns (PG 11+ `INCLUDE (...)` clause). Empty `Vec` (the
8438 /// default) means "no covering columns".
8439 include: Vec<String>,
8440}
8441
8442/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8443struct CheckAttr {
8444 name: String,
8445 expr: String,
8446}
8447
8448/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8449/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8450/// PG-only — surfaced on every backend in the macro emit; the migration
8451/// writer skips the op on MySQL/SQLite. Issue #319.
8452struct ExcludeAttr {
8453 /// Constraint name (free-form Rust identifier).
8454 name: String,
8455 /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8456 using: String,
8457 /// Comma-separated `(column, operator)` pairs, in declaration
8458 /// order. Parsed from `"col WITH op, col WITH op"`.
8459 elements: Vec<(String, String)>,
8460 /// Optional `WHERE` predicate (raw SQL).
8461 where_clause: Option<String>,
8462}
8463
8464/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8465/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8466/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8467/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8468/// the model, not the field.
8469struct CompositeFkAttr {
8470 /// Logical relation name (free-form Rust identifier).
8471 name: String,
8472 /// SQL table name of the target.
8473 to: String,
8474 /// Source-side column names, in declaration order.
8475 from: Vec<String>,
8476 /// Target-side column names, same length / order as `from`.
8477 on: Vec<String>,
8478}
8479
8480/// Parsed form of one `#[rustango(generic_fk(name = "target",
8481/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8482/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8483/// generic ("any model") FKs live on the model, not the field.
8484struct GenericFkAttr {
8485 /// Logical relation name (free-form Rust identifier).
8486 name: String,
8487 /// Source-side column carrying the `content_type_id` value.
8488 ct_column: String,
8489 /// Source-side column carrying the target row's primary key.
8490 pk_column: String,
8491}
8492
8493/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8494struct M2MAttr {
8495 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8496 name: String,
8497 /// Target table (e.g. `"app_tags"`).
8498 to: String,
8499 /// Junction table (e.g. `"post_tags"`).
8500 through: String,
8501 /// Source FK column in the junction table (e.g. `"post_id"`).
8502 src: String,
8503 /// Destination FK column in the junction table (e.g. `"tag_id"`).
8504 dst: String,
8505 /// Whether the migration writer should auto-create the junction
8506 /// table. Default `true`. Set `auto_create = false` (#324) when
8507 /// the operator declares the through table as its own
8508 /// `#[derive(Model)]` struct with extra columns.
8509 auto_create: bool,
8510}
8511
8512/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8513/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8514/// junction carries a ContentType discriminator so unrelated models
8515/// share one pivot + related set.
8516struct GenericM2MAttr {
8517 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8518 name: String,
8519 /// Polymorphic junction table (e.g. `"taggables"`).
8520 through: String,
8521 /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8522 pk_column: String,
8523 /// Junction column holding the owning model's `content_type_id`
8524 /// discriminator (e.g. `"taggable_type"`).
8525 ct_column: String,
8526 /// Junction column holding the related model PK (e.g. `"tag_id"`).
8527 related_column: String,
8528}
8529
8530/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8531/// "user"))]`. `track` is a comma-separated list of field names whose
8532/// before/after values land in the JSONB `changes` column. `source`
8533/// is informational only — it pins a default source when the model
8534/// is written outside any `audit::with_source(...)` scope (rare).
8535#[derive(Default)]
8536struct AuditAttrs {
8537 /// Field names to capture in the `changes` JSONB. Validated
8538 /// against declared scalar fields at compile time. Empty means
8539 /// "track every scalar field" — Django's audit-everything default.
8540 track: Option<(Vec<String>, proc_macro2::Span)>,
8541}
8542
8543/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8544/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8545/// comma-separated strings; we validate each ident against the model's
8546/// declared fields at compile time.
8547#[derive(Default)]
8548struct AdminAttrs {
8549 list_display: Option<(Vec<String>, proc_macro2::Span)>,
8550 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8551 list_per_page: Option<usize>,
8552 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8553 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8554 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8555 /// Bulk action names. No field-validation against model fields —
8556 /// these are action handlers, not column references.
8557 actions: Option<(Vec<String>, proc_macro2::Span)>,
8558 /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8559 /// sections, comma-separated fields per section, optional
8560 /// `Title:` prefix. Empty title omits the `<legend>`.
8561 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8562 /// `admin(list_display_links = "title")` — Django-shape. Names
8563 /// from `list_display` whose cells should link to detail/edit.
8564 /// Issue #350.
8565 list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8566 /// `admin(search_help_text = "...")` — Django-shape. Short
8567 /// caption rendered beside the admin list view's search box.
8568 /// Issue #353.
8569 search_help_text: Option<String>,
8570 /// `admin(actions_on_top = false)` — Django-shape. Hides the
8571 /// action-bar above the table. Default `true`. Issue #354.
8572 actions_on_top: Option<bool>,
8573 /// `admin(actions_on_bottom = true)` — Django-shape. Renders an
8574 /// additional action-bar below the table. Default `false`.
8575 /// Issue #354.
8576 actions_on_bottom: Option<bool>,
8577 /// `admin(date_hierarchy = "created_at")` — Django-shape. Name of
8578 /// a date / datetime field whose values render as a clickable
8579 /// year / month / day drill-down strip above the list table.
8580 /// Empty / unset disables the strip. Issue #355.
8581 date_hierarchy: Option<String>,
8582 /// `admin(prepopulated_fields = "slug:title")` — Django-shape.
8583 /// Each entry is `target:source[+source2]`; multiple entries are
8584 /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8585 /// The admin change-form emits JS that slugifies the source values
8586 /// into the target field on every keystroke. Issue #356.
8587 prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8588 /// `admin(raw_id_fields = "parent, owner")` — Django-shape. Names
8589 /// of FK fields whose change-form widget renders a lookup link
8590 /// next to the input. Issue #357.
8591 raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8592 /// `admin(autocomplete_fields = "author_id")` — Django-shape.
8593 /// Names of FK fields whose change-form widget renders an
8594 /// Ajax-driven typeahead populated from a `__autocomplete`
8595 /// endpoint on the target model. Issue #358.
8596 autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8597 /// `admin(list_select_related = "all" | "none" | "author, …")`
8598 /// — Django-shape. Tunes the admin list view's FK auto-JOIN
8599 /// policy. Default `"all"` matches rustango's join-everything
8600 /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8601 list_select_related: Option<String>,
8602 /// `admin(formfield_overrides = "field:widget, field2:widget2")` —
8603 /// Django-shape. Each entry is `field_name:widget_name`; multiple
8604 /// entries comma-separated. Empty / unset → no overrides. The
8605 /// list of widget names supported is documented on
8606 /// `AdminConfig::formfield_overrides`. Issue #359.
8607 formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8608}
8609
8610fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8611 let mut out = ContainerAttrs {
8612 table: None,
8613 display: None,
8614 app: None,
8615 admin: None,
8616 audit: None,
8617 // Default `permissions = true` so every `#[derive(Model)]`
8618 // gets the four CRUD codenames seeded by `auto_create_permissions`
8619 // and is visible to non-superusers in the tenant admin without
8620 // manual per-model annotation. Models that intentionally don't
8621 // want permission rows (registry-internal types, framework
8622 // tables operators shouldn't manage directly) opt out via
8623 // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8624 // out-of-the-box admin invisibility regression (#62).
8625 permissions: true,
8626 m2m: Vec::new(),
8627 generic_m2m: Vec::new(),
8628 indexes: Vec::new(),
8629 checks: Vec::new(),
8630 excludes: Vec::new(),
8631 composite_fks: Vec::new(),
8632 generic_fks: Vec::new(),
8633 scope: None,
8634 manager_ext: None,
8635 manager_fns: Vec::new(),
8636 default_order: Vec::new(),
8637 is_view: false,
8638 managed: true,
8639 verbose_name: None,
8640 verbose_name_plural: None,
8641 base_manager_name: None,
8642 order_with_respect_to: None,
8643 proxy: false,
8644 required_db_features: Vec::new(),
8645 required_db_vendor: None,
8646 default_related_name: None,
8647 db_table_comment: None,
8648 get_latest_by: None,
8649 extra_permissions: Vec::new(),
8650 default_permissions: Vec::new(),
8651 global_scopes: Vec::new(),
8652 through_relations: Vec::new(),
8653 reverse_has_relations: Vec::new(),
8654 generic_has_relations: Vec::new(),
8655 };
8656 for attr in &input.attrs {
8657 if !attr.path().is_ident("rustango") {
8658 continue;
8659 }
8660 attr.parse_nested_meta(|meta| {
8661 if meta.path.is_ident("table") {
8662 let s: LitStr = meta.value()?.parse()?;
8663 let name = s.value();
8664 // v0.27.3 (#65) — macro-time guard against table names
8665 // that compile but break SQL downstream. Hyphens are
8666 // the common footgun: PostgreSQL accepts a quoted
8667 // `"intermediate-region"` in CREATE TABLE, but the
8668 // FK / index name derivation in `migrate::ddl`
8669 // emits `intermediate-region_field_fkey` unquoted,
8670 // which then fails the SQL parser. Same shape rule
8671 // as Postgres regular identifiers so the safe path
8672 // is the only path.
8673 validate_table_name(&name, s.span())?;
8674 out.table = Some(name);
8675 return Ok(());
8676 }
8677 if meta.path.is_ident("display") {
8678 let s: LitStr = meta.value()?.parse()?;
8679 out.display = Some((s.value(), s.span()));
8680 return Ok(());
8681 }
8682 if meta.path.is_ident("app") {
8683 let s: LitStr = meta.value()?.parse()?;
8684 out.app = Some(s.value());
8685 return Ok(());
8686 }
8687 if meta.path.is_ident("scope") {
8688 let s: LitStr = meta.value()?.parse()?;
8689 let val = s.value();
8690 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8691 return Err(meta.error(format!(
8692 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8693 )));
8694 }
8695 out.scope = Some(val);
8696 return Ok(());
8697 }
8698 if meta.path.is_ident("admin") {
8699 let mut admin = AdminAttrs::default();
8700 meta.parse_nested_meta(|inner| {
8701 if inner.path.is_ident("list_display") {
8702 let s: LitStr = inner.value()?.parse()?;
8703 admin.list_display =
8704 Some((split_field_list(&s.value()), s.span()));
8705 return Ok(());
8706 }
8707 if inner.path.is_ident("search_fields") {
8708 let s: LitStr = inner.value()?.parse()?;
8709 admin.search_fields =
8710 Some((split_field_list(&s.value()), s.span()));
8711 return Ok(());
8712 }
8713 if inner.path.is_ident("readonly_fields") {
8714 let s: LitStr = inner.value()?.parse()?;
8715 admin.readonly_fields =
8716 Some((split_field_list(&s.value()), s.span()));
8717 return Ok(());
8718 }
8719 if inner.path.is_ident("list_per_page") {
8720 let lit: syn::LitInt = inner.value()?.parse()?;
8721 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8722 return Ok(());
8723 }
8724 if inner.path.is_ident("ordering") {
8725 let s: LitStr = inner.value()?.parse()?;
8726 admin.ordering = Some((
8727 parse_ordering_list(&s.value()),
8728 s.span(),
8729 ));
8730 return Ok(());
8731 }
8732 if inner.path.is_ident("list_filter") {
8733 let s: LitStr = inner.value()?.parse()?;
8734 admin.list_filter =
8735 Some((split_field_list(&s.value()), s.span()));
8736 return Ok(());
8737 }
8738 if inner.path.is_ident("actions") {
8739 let s: LitStr = inner.value()?.parse()?;
8740 admin.actions =
8741 Some((split_field_list(&s.value()), s.span()));
8742 return Ok(());
8743 }
8744 if inner.path.is_ident("fieldsets") {
8745 let s: LitStr = inner.value()?.parse()?;
8746 admin.fieldsets =
8747 Some((parse_fieldset_list(&s.value()), s.span()));
8748 return Ok(());
8749 }
8750 if inner.path.is_ident("list_display_links") {
8751 let s: LitStr = inner.value()?.parse()?;
8752 admin.list_display_links =
8753 Some((split_field_list(&s.value()), s.span()));
8754 return Ok(());
8755 }
8756 if inner.path.is_ident("search_help_text") {
8757 let s: LitStr = inner.value()?.parse()?;
8758 admin.search_help_text = Some(s.value());
8759 return Ok(());
8760 }
8761 if inner.path.is_ident("actions_on_top") {
8762 let lit: syn::LitBool = inner.value()?.parse()?;
8763 admin.actions_on_top = Some(lit.value);
8764 return Ok(());
8765 }
8766 if inner.path.is_ident("actions_on_bottom") {
8767 let lit: syn::LitBool = inner.value()?.parse()?;
8768 admin.actions_on_bottom = Some(lit.value);
8769 return Ok(());
8770 }
8771 if inner.path.is_ident("date_hierarchy") {
8772 let s: LitStr = inner.value()?.parse()?;
8773 admin.date_hierarchy = Some(s.value());
8774 return Ok(());
8775 }
8776 if inner.path.is_ident("prepopulated_fields") {
8777 let s: LitStr = inner.value()?.parse()?;
8778 admin.prepopulated_fields =
8779 Some((parse_prepopulated_list(&s.value()), s.span()));
8780 return Ok(());
8781 }
8782 if inner.path.is_ident("raw_id_fields") {
8783 let s: LitStr = inner.value()?.parse()?;
8784 admin.raw_id_fields =
8785 Some((split_field_list(&s.value()), s.span()));
8786 return Ok(());
8787 }
8788 if inner.path.is_ident("autocomplete_fields") {
8789 let s: LitStr = inner.value()?.parse()?;
8790 admin.autocomplete_fields =
8791 Some((split_field_list(&s.value()), s.span()));
8792 return Ok(());
8793 }
8794 if inner.path.is_ident("list_select_related") {
8795 let s: LitStr = inner.value()?.parse()?;
8796 admin.list_select_related = Some(s.value());
8797 return Ok(());
8798 }
8799 if inner.path.is_ident("formfield_overrides") {
8800 let s: LitStr = inner.value()?.parse()?;
8801 admin.formfield_overrides =
8802 Some((parse_formfield_overrides(&s.value()), s.span()));
8803 return Ok(());
8804 }
8805 Err(inner.error(
8806 "unknown admin attribute (supported: \
8807 `list_display`, `list_display_links`, \
8808 `search_fields`, `search_help_text`, \
8809 `readonly_fields`, \
8810 `list_filter`, `list_per_page`, `ordering`, `actions`, \
8811 `actions_on_top`, `actions_on_bottom`, \
8812 `date_hierarchy`, \
8813 `prepopulated_fields`, \
8814 `raw_id_fields`, \
8815 `autocomplete_fields`, \
8816 `list_select_related`, \
8817 `formfield_overrides`, \
8818 `fieldsets`)",
8819 ))
8820 })?;
8821 out.admin = Some(admin);
8822 return Ok(());
8823 }
8824 if meta.path.is_ident("manager") {
8825 // `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8826 // Stretch `from_queryset = "..."` (Django Manager.from_queryset
8827 // shape) is left as a follow-up — the issue's primary
8828 // acceptance is the `ext = ...` trait emission.
8829 meta.parse_nested_meta(|inner| {
8830 if inner.path.is_ident("ext") {
8831 let s: LitStr = inner.value()?.parse()?;
8832 let name = s.value();
8833 if name.is_empty() {
8834 return Err(inner.error("manager(ext = \"...\") cannot be empty"));
8835 }
8836 out.manager_ext =
8837 Some(syn::Ident::new(&name, s.span()));
8838 return Ok(());
8839 }
8840 Err(inner.error(
8841 "unknown manager attribute (supported: `ext = \"TraitName\"`)",
8842 ))
8843 })?;
8844 return Ok(());
8845 }
8846 if meta.path.is_ident("manager_fn") {
8847 // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8848 // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8849 // next to the default `Self::objects()`. Multiple
8850 // attributes accumulate.
8851 let s: LitStr = meta.value()?.parse()?;
8852 let name = s.value();
8853 if name.is_empty() {
8854 return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8855 }
8856 if name == "objects" {
8857 return Err(meta.error(
8858 "`manager_fn = \"objects\"` collides with the default \
8859 accessor — pick a different name",
8860 ));
8861 }
8862 let ident = syn::Ident::new(&name, s.span());
8863 if out.manager_fns.iter().any(|prev| *prev == ident) {
8864 return Err(meta.error(format!(
8865 "duplicate `manager_fn = \"{name}\"`"
8866 )));
8867 }
8868 out.manager_fns.push(ident);
8869 return Ok(());
8870 }
8871 if meta.path.is_ident("default_order") {
8872 // `#[rustango(default_order = "-created_at, status")]`
8873 // — issue #291 / T2.5. Comma-separated list; `-prefix`
8874 // means descending, `+prefix` or bare name means ascending.
8875 // Per-query opt-in via `QuerySet::with_default_order()`.
8876 let s: LitStr = meta.value()?.parse()?;
8877 let raw = s.value();
8878 let span = s.span();
8879 let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
8880 Vec::new();
8881 for entry in raw.split(',') {
8882 let trimmed = entry.trim();
8883 if trimmed.is_empty() {
8884 return Err(syn::Error::new(
8885 span,
8886 "`default_order = \"...\"` has an empty entry — \
8887 check for a stray comma",
8888 ));
8889 }
8890 let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
8891 (true, rest.trim().to_owned())
8892 } else if let Some(rest) = trimmed.strip_prefix('+') {
8893 (false, rest.trim().to_owned())
8894 } else {
8895 (false, trimmed.to_owned())
8896 };
8897 if name.is_empty() {
8898 return Err(syn::Error::new(
8899 span,
8900 "`default_order` entry has no column name after the prefix",
8901 ));
8902 }
8903 if parsed.iter().any(|(n, _, _)| *n == name) {
8904 return Err(syn::Error::new(
8905 span,
8906 format!("duplicate column `{name}` in `default_order`"),
8907 ));
8908 }
8909 parsed.push((name, desc, span));
8910 }
8911 if parsed.is_empty() {
8912 return Err(syn::Error::new(
8913 span,
8914 "`default_order = \"...\"` cannot be empty",
8915 ));
8916 }
8917 out.default_order = parsed;
8918 return Ok(());
8919 }
8920 if meta.path.is_ident("global_scope") {
8921 // `#[rustango(global_scope(name = "active", apply =
8922 // path::to::fn))]` — issue #820. The apply function
8923 // path resolves at macro-expand time in the consumer's
8924 // scope; the macro re-emits it verbatim into the
8925 // `ModelSchema::global_scopes` slice literal.
8926 let span = meta.path.span();
8927 let mut scope_name: Option<String> = None;
8928 let mut apply_path: Option<syn::Path> = None;
8929 meta.parse_nested_meta(|inner| {
8930 if inner.path.is_ident("name") {
8931 let s: LitStr = inner.value()?.parse()?;
8932 let raw = s.value();
8933 if raw.trim().is_empty() {
8934 return Err(syn::Error::new(
8935 s.span(),
8936 "`global_scope(name = \"...\")` must not be empty",
8937 ));
8938 }
8939 scope_name = Some(raw);
8940 return Ok(());
8941 }
8942 if inner.path.is_ident("apply") {
8943 let p: syn::Path = inner.value()?.parse()?;
8944 apply_path = Some(p);
8945 return Ok(());
8946 }
8947 Err(inner.error(
8948 "unknown `global_scope` attribute (supported: \
8949 `name`, `apply`)",
8950 ))
8951 })?;
8952 let Some(name) = scope_name else {
8953 return Err(syn::Error::new(
8954 span,
8955 "`global_scope` requires `name = \"...\"`",
8956 ));
8957 };
8958 let Some(apply) = apply_path else {
8959 return Err(syn::Error::new(
8960 span,
8961 "`global_scope` requires `apply = fn_path`",
8962 ));
8963 };
8964 if out.global_scopes.iter().any(|s| s.name == name) {
8965 return Err(syn::Error::new(
8966 span,
8967 format!(
8968 "duplicate global scope name `{name}` — \
8969 pick a unique identifier so \
8970 `QuerySet::without_global_scope(\"{name}\")` \
8971 is unambiguous"
8972 ),
8973 ));
8974 }
8975 out.global_scopes.push(GlobalScopeAttr { name, apply });
8976 return Ok(());
8977 }
8978 if meta.path.is_ident("through") {
8979 // `#[rustango(through(name = "posts", far = "Post",
8980 // far_fk_column = "author_id", intermediate = "User",
8981 // intermediate_fk_column = "country_id"
8982 // [, intermediate_pk_column = "id"]))]` — issue #817.
8983 let span = meta.path.span();
8984 let mut accessor_name: Option<String> = None;
8985 let mut far_ident: Option<syn::Ident> = None;
8986 let mut far_fk_column: Option<String> = None;
8987 let mut intermediate_ident: Option<syn::Ident> = None;
8988 let mut intermediate_fk_column: Option<String> = None;
8989 let mut intermediate_pk_column: Option<String> = None;
8990 fn parse_nonempty_string(
8991 inner: &syn::meta::ParseNestedMeta<'_>,
8992 field: &str,
8993 ) -> syn::Result<String> {
8994 let s: LitStr = inner.value()?.parse()?;
8995 let raw = s.value();
8996 let trimmed = raw.trim();
8997 if trimmed.is_empty() {
8998 return Err(syn::Error::new(
8999 s.span(),
9000 format!("`through({field} = \"...\")` must not be empty"),
9001 ));
9002 }
9003 Ok(trimmed.to_owned())
9004 }
9005 meta.parse_nested_meta(|inner| {
9006 if inner.path.is_ident("name") {
9007 accessor_name = Some(parse_nonempty_string(&inner, "name")?);
9008 return Ok(());
9009 }
9010 if inner.path.is_ident("far") {
9011 // Far model name accepted as a string literal
9012 // so the attribute fits inside the existing
9013 // `parse_nested_meta` shape; parsed back into a
9014 // `syn::Ident` so the emitted accessor can
9015 // reference the type directly.
9016 let s: LitStr = inner.value()?.parse()?;
9017 let raw = s.value();
9018 let trimmed = raw.trim();
9019 if trimmed.is_empty() {
9020 return Err(syn::Error::new(
9021 s.span(),
9022 "`through(far = \"...\")` must not be empty",
9023 ));
9024 }
9025 far_ident = Some(syn::Ident::new(trimmed, s.span()));
9026 return Ok(());
9027 }
9028 if inner.path.is_ident("far_fk_column") {
9029 far_fk_column =
9030 Some(parse_nonempty_string(&inner, "far_fk_column")?);
9031 return Ok(());
9032 }
9033 if inner.path.is_ident("intermediate") {
9034 let s: LitStr = inner.value()?.parse()?;
9035 let raw = s.value();
9036 let trimmed = raw.trim();
9037 if trimmed.is_empty() {
9038 return Err(syn::Error::new(
9039 s.span(),
9040 "`through(intermediate = \"...\")` must not be empty",
9041 ));
9042 }
9043 intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9044 return Ok(());
9045 }
9046 if inner.path.is_ident("intermediate_fk_column") {
9047 intermediate_fk_column =
9048 Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9049 return Ok(());
9050 }
9051 if inner.path.is_ident("intermediate_pk_column") {
9052 intermediate_pk_column =
9053 Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9054 return Ok(());
9055 }
9056 Err(inner.error(
9057 "unknown `through` attribute (supported: \
9058 `name`, `far`, `far_fk_column`, \
9059 `intermediate`, `intermediate_fk_column`, \
9060 `intermediate_pk_column`)",
9061 ))
9062 })?;
9063 let Some(name) = accessor_name else {
9064 return Err(syn::Error::new(
9065 span,
9066 "`through` requires `name = \"...\"`",
9067 ));
9068 };
9069 let Some(far) = far_ident else {
9070 return Err(syn::Error::new(
9071 span,
9072 "`through` requires `far = \"FarModelType\"`",
9073 ));
9074 };
9075 let Some(far_fk_column) = far_fk_column else {
9076 return Err(syn::Error::new(
9077 span,
9078 "`through` requires `far_fk_column = \"<column>\"`",
9079 ));
9080 };
9081 let Some(intermediate) = intermediate_ident else {
9082 return Err(syn::Error::new(
9083 span,
9084 "`through` requires `intermediate = \"IntermediateModelType\"`",
9085 ));
9086 };
9087 let Some(intermediate_fk_column) = intermediate_fk_column else {
9088 return Err(syn::Error::new(
9089 span,
9090 "`through` requires `intermediate_fk_column = \"<column>\"`",
9091 ));
9092 };
9093 let intermediate_pk_column =
9094 intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9095 if out.through_relations.iter().any(|t| t.name == name) {
9096 return Err(syn::Error::new(
9097 span,
9098 format!(
9099 "duplicate `through(name = \"{name}\")` — \
9100 pick a unique accessor name"
9101 ),
9102 ));
9103 }
9104 out.through_relations.push(ThroughAttr {
9105 name,
9106 far,
9107 far_fk_column,
9108 intermediate,
9109 intermediate_fk_column,
9110 intermediate_pk_column,
9111 });
9112 return Ok(());
9113 }
9114 if meta.path.is_ident("reverse_has") {
9115 // `#[rustango(reverse_has(name = "comments",
9116 // child = "Comment", child_fk_column = "post_id"
9117 // [, self_pk_column = "id"]))]` — issue #830.
9118 let span = meta.path.span();
9119 let mut accessor_name: Option<String> = None;
9120 let mut child_ident: Option<syn::Ident> = None;
9121 let mut child_fk_column: Option<String> = None;
9122 let mut self_pk_column: Option<String> = None;
9123 meta.parse_nested_meta(|inner| {
9124 if inner.path.is_ident("name") {
9125 let s: LitStr = inner.value()?.parse()?;
9126 let raw = s.value();
9127 if raw.trim().is_empty() {
9128 return Err(syn::Error::new(
9129 s.span(),
9130 "`reverse_has(name = \"...\")` must not be empty",
9131 ));
9132 }
9133 accessor_name = Some(raw);
9134 return Ok(());
9135 }
9136 if inner.path.is_ident("child") {
9137 let s: LitStr = inner.value()?.parse()?;
9138 let raw = s.value();
9139 let trimmed = raw.trim();
9140 if trimmed.is_empty() {
9141 return Err(syn::Error::new(
9142 s.span(),
9143 "`reverse_has(child = \"...\")` must not be empty",
9144 ));
9145 }
9146 child_ident = Some(syn::Ident::new(trimmed, s.span()));
9147 return Ok(());
9148 }
9149 if inner.path.is_ident("child_fk_column") {
9150 let s: LitStr = inner.value()?.parse()?;
9151 let raw = s.value();
9152 let trimmed = raw.trim();
9153 if trimmed.is_empty() {
9154 return Err(syn::Error::new(
9155 s.span(),
9156 "`reverse_has(child_fk_column = \"...\")` must not be empty",
9157 ));
9158 }
9159 child_fk_column = Some(trimmed.to_owned());
9160 return Ok(());
9161 }
9162 if inner.path.is_ident("self_pk_column") {
9163 let s: LitStr = inner.value()?.parse()?;
9164 let raw = s.value();
9165 let trimmed = raw.trim();
9166 if trimmed.is_empty() {
9167 return Err(syn::Error::new(
9168 s.span(),
9169 "`reverse_has(self_pk_column = \"...\")` must not be empty",
9170 ));
9171 }
9172 self_pk_column = Some(trimmed.to_owned());
9173 return Ok(());
9174 }
9175 Err(inner.error(
9176 "unknown `reverse_has` attribute (supported: \
9177 `name`, `child`, `child_fk_column`, \
9178 `self_pk_column`)",
9179 ))
9180 })?;
9181 let Some(name) = accessor_name else {
9182 return Err(syn::Error::new(
9183 span,
9184 "`reverse_has` requires `name = \"...\"`",
9185 ));
9186 };
9187 let Some(child) = child_ident else {
9188 return Err(syn::Error::new(
9189 span,
9190 "`reverse_has` requires `child = \"ChildModelType\"`",
9191 ));
9192 };
9193 let Some(child_fk_column) = child_fk_column else {
9194 return Err(syn::Error::new(
9195 span,
9196 "`reverse_has` requires `child_fk_column = \"<column>\"`",
9197 ));
9198 };
9199 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9200 if out.reverse_has_relations.iter().any(|r| r.name == name) {
9201 return Err(syn::Error::new(
9202 span,
9203 format!(
9204 "duplicate `reverse_has(name = \"{name}\")` — \
9205 pick a unique accessor name"
9206 ),
9207 ));
9208 }
9209 out.reverse_has_relations.push(ReverseHasAttr {
9210 name,
9211 child,
9212 child_fk_column,
9213 self_pk_column,
9214 });
9215 return Ok(());
9216 }
9217 if meta.path.is_ident("generic_has") {
9218 // `#[rustango(generic_has(name = "tags",
9219 // child = "Tag", ct_column = "content_type_id",
9220 // pk_column = "object_pk" [, self_pk_column = "id"]))]`
9221 // — issue #830, the reverse generic-FK (GFK) arm of the
9222 // relation-existence family.
9223 let span = meta.path.span();
9224 let mut accessor_name: Option<String> = None;
9225 let mut child_ident: Option<syn::Ident> = None;
9226 let mut ct_column: Option<String> = None;
9227 let mut pk_column: Option<String> = None;
9228 let mut self_pk_column: Option<String> = None;
9229 meta.parse_nested_meta(|inner| {
9230 if inner.path.is_ident("name") {
9231 let s: LitStr = inner.value()?.parse()?;
9232 let raw = s.value();
9233 if raw.trim().is_empty() {
9234 return Err(syn::Error::new(
9235 s.span(),
9236 "`generic_has(name = \"...\")` must not be empty",
9237 ));
9238 }
9239 accessor_name = Some(raw);
9240 return Ok(());
9241 }
9242 if inner.path.is_ident("child") {
9243 let s: LitStr = inner.value()?.parse()?;
9244 let trimmed = s.value().trim().to_owned();
9245 if trimmed.is_empty() {
9246 return Err(syn::Error::new(
9247 s.span(),
9248 "`generic_has(child = \"...\")` must not be empty",
9249 ));
9250 }
9251 child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9252 return Ok(());
9253 }
9254 if inner.path.is_ident("ct_column") {
9255 let s: LitStr = inner.value()?.parse()?;
9256 let trimmed = s.value().trim().to_owned();
9257 if trimmed.is_empty() {
9258 return Err(syn::Error::new(
9259 s.span(),
9260 "`generic_has(ct_column = \"...\")` must not be empty",
9261 ));
9262 }
9263 ct_column = Some(trimmed);
9264 return Ok(());
9265 }
9266 if inner.path.is_ident("pk_column") {
9267 let s: LitStr = inner.value()?.parse()?;
9268 let trimmed = s.value().trim().to_owned();
9269 if trimmed.is_empty() {
9270 return Err(syn::Error::new(
9271 s.span(),
9272 "`generic_has(pk_column = \"...\")` must not be empty",
9273 ));
9274 }
9275 pk_column = Some(trimmed);
9276 return Ok(());
9277 }
9278 if inner.path.is_ident("self_pk_column") {
9279 let s: LitStr = inner.value()?.parse()?;
9280 let trimmed = s.value().trim().to_owned();
9281 if trimmed.is_empty() {
9282 return Err(syn::Error::new(
9283 s.span(),
9284 "`generic_has(self_pk_column = \"...\")` must not be empty",
9285 ));
9286 }
9287 self_pk_column = Some(trimmed);
9288 return Ok(());
9289 }
9290 Err(inner.error(
9291 "unknown `generic_has` attribute (supported: \
9292 `name`, `child`, `ct_column`, `pk_column`, \
9293 `self_pk_column`)",
9294 ))
9295 })?;
9296 let Some(name) = accessor_name else {
9297 return Err(syn::Error::new(
9298 span,
9299 "`generic_has` requires `name = \"...\"`",
9300 ));
9301 };
9302 let Some(child) = child_ident else {
9303 return Err(syn::Error::new(
9304 span,
9305 "`generic_has` requires `child = \"ChildModelType\"`",
9306 ));
9307 };
9308 let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9309 let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9310 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9311 if out.generic_has_relations.iter().any(|r| r.name == name) {
9312 return Err(syn::Error::new(
9313 span,
9314 format!(
9315 "duplicate `generic_has(name = \"{name}\")` — \
9316 pick a unique accessor name"
9317 ),
9318 ));
9319 }
9320 out.generic_has_relations.push(GenericHasAttr {
9321 name,
9322 child,
9323 ct_column,
9324 pk_column,
9325 self_pk_column,
9326 });
9327 return Ok(());
9328 }
9329 if meta.path.is_ident("audit") {
9330 let mut audit = AuditAttrs::default();
9331 meta.parse_nested_meta(|inner| {
9332 if inner.path.is_ident("track") {
9333 let s: LitStr = inner.value()?.parse()?;
9334 audit.track =
9335 Some((split_field_list(&s.value()), s.span()));
9336 return Ok(());
9337 }
9338 Err(inner.error(
9339 "unknown audit attribute (supported: `track`)",
9340 ))
9341 })?;
9342 out.audit = Some(audit);
9343 return Ok(());
9344 }
9345 if meta.path.is_ident("permissions") {
9346 // Two forms accepted:
9347 // #[rustango(permissions)] — flag form, true
9348 // #[rustango(permissions = false)] — explicit opt-out
9349 // #[rustango(permissions = true)] — explicit opt-in
9350 if let Ok(v) = meta.value() {
9351 let lit: syn::LitBool = v.parse()?;
9352 out.permissions = lit.value;
9353 } else {
9354 out.permissions = true;
9355 }
9356 return Ok(());
9357 }
9358 if meta.path.is_ident("view") {
9359 // Issue #293 / T2.10. Two forms accepted, matching
9360 // the `permissions` flag pattern:
9361 // #[rustango(view)] — flag form, true
9362 // #[rustango(view = false)] — explicit opt-out
9363 // #[rustango(view = true)] — explicit opt-in
9364 if let Ok(v) = meta.value() {
9365 let lit: syn::LitBool = v.parse()?;
9366 out.is_view = lit.value;
9367 } else {
9368 out.is_view = true;
9369 }
9370 return Ok(());
9371 }
9372 if meta.path.is_ident("managed") {
9373 // Django-shape Meta.managed. Issue #321.
9374 // #[rustango(managed = false)] — operator-managed table
9375 // #[rustango(managed = true)] — rustango-managed (the default)
9376 // Bare-flag form is intentionally not accepted: writing
9377 // `#[rustango(managed)]` reads as "yes please manage it"
9378 // which is already the default. The opt-out is the only
9379 // useful state, so it must be explicit.
9380 let v = meta.value()?;
9381 let lit: syn::LitBool = v.parse()?;
9382 out.managed = lit.value;
9383 return Ok(());
9384 }
9385 if meta.path.is_ident("verbose_name") {
9386 let s: LitStr = meta.value()?.parse()?;
9387 out.verbose_name = Some(s.value());
9388 return Ok(());
9389 }
9390 if meta.path.is_ident("verbose_name_plural") {
9391 let s: LitStr = meta.value()?.parse()?;
9392 out.verbose_name_plural = Some(s.value());
9393 return Ok(());
9394 }
9395 if meta.path.is_ident("db_table_comment") {
9396 // Django-shape `Meta.db_table_comment` (4.2+) — free-form
9397 // table-level comment attached to the DB catalog.
9398 let s: LitStr = meta.value()?.parse()?;
9399 out.db_table_comment = Some(s.value());
9400 return Ok(());
9401 }
9402 if meta.path.is_ident("proxy") {
9403 // Django-shape `Meta.proxy = True` — declarative flag
9404 // marking the struct as a proxy of another model that
9405 // shares its DB table. Stored on `ModelSchema::proxy`
9406 // so future codegen can skip `CreateTable` emission
9407 // for proxies (parent owns the table) and route
9408 // per-instance method resolution to the proxy class.
9409 //
9410 // Accepts `proxy` (bare → true) and `proxy = true/false`.
9411 let value = if meta.input.peek(syn::Token![=]) {
9412 meta.value()?.parse::<syn::LitBool>()?.value
9413 } else {
9414 true
9415 };
9416 out.proxy = value;
9417 return Ok(());
9418 }
9419 if meta.path.is_ident("order_with_respect_to") {
9420 // Django-shape `Meta.order_with_respect_to = "parent_fk"` —
9421 // the model's instances are intrinsically ordered
9422 // relative to their parent FK. Django auto-generates
9423 // a `_order` integer column + admin reordering UI.
9424 //
9425 // rustango stores the FK field name on
9426 // `ModelSchema::order_with_respect_to`. Declarative-only
9427 // today: the migration writer + admin surfaces still
9428 // treat every model identically. Future codegen will
9429 // key off the metadata to auto-emit the `_order`
9430 // column and reorder helpers.
9431 //
9432 // Validated as a Rust-shape identifier so the macro
9433 // can reject typos at derive time.
9434 let s: LitStr = meta.value()?.parse()?;
9435 let raw = s.value();
9436 if raw.is_empty() {
9437 return Err(syn::Error::new(
9438 s.span(),
9439 "`order_with_respect_to` must be a non-empty FK field name",
9440 ));
9441 }
9442 let valid = raw
9443 .chars()
9444 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9445 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9446 if !valid {
9447 return Err(syn::Error::new(
9448 s.span(),
9449 format!(
9450 "`order_with_respect_to` must be a valid Rust \
9451 identifier (letters / digits / underscores, \
9452 not starting with a digit); got `{raw}`"
9453 ),
9454 ));
9455 }
9456 out.order_with_respect_to = Some(raw);
9457 return Ok(());
9458 }
9459 if meta.path.is_ident("required_db_features") {
9460 // Django-shape `Meta.required_db_features` — capability
9461 // tokens the model needs (e.g. `"json_extract"`,
9462 // `"window_functions"`, `"row_security"`). Comma-separated.
9463 // `manage check --deploy` walks every model and warns
9464 // when the active backend doesn't advertise the
9465 // capability.
9466 //
9467 // rustango ships a small registry of capability tokens
9468 // each dialect supports — see `Dialect::supports`.
9469 // Unknown tokens still parse (they end up on the
9470 // schema and show up in the warning) so projects can
9471 // declare aspirational capabilities and the check
9472 // verb will keep nagging until the dialect implements
9473 // them.
9474 let s: LitStr = meta.value()?.parse()?;
9475 let raw = s.value();
9476 let features: Vec<String> = raw
9477 .split(',')
9478 .map(str::trim)
9479 .filter(|s| !s.is_empty())
9480 .map(str::to_owned)
9481 .collect();
9482 if features.is_empty() {
9483 return Err(syn::Error::new(
9484 s.span(),
9485 "`required_db_features` must list at least one \
9486 comma-separated capability token",
9487 ));
9488 }
9489 out.required_db_features = features;
9490 return Ok(());
9491 }
9492 if meta.path.is_ident("required_db_vendor") {
9493 // Django-shape `Meta.required_db_vendor` — the model
9494 // is only meant to run against the named DB backend.
9495 // `manage check --deploy` flags a mismatch so
9496 // ops catches "I forgot to switch DATABASE_URL" at
9497 // deploy time rather than runtime.
9498 //
9499 // Django spells it as a free-form string; rustango
9500 // restricts to the three backends it ships dialects
9501 // for so the check verb can compare reliably.
9502 let s: LitStr = meta.value()?.parse()?;
9503 let raw = s.value().to_ascii_lowercase();
9504 match raw.as_str() {
9505 "postgresql" | "postgres" | "pg" => {
9506 out.required_db_vendor = Some("postgres".to_owned());
9507 }
9508 "mysql" | "mariadb" => {
9509 out.required_db_vendor = Some("mysql".to_owned());
9510 }
9511 "sqlite" | "sqlite3" => {
9512 out.required_db_vendor = Some("sqlite".to_owned());
9513 }
9514 _ => {
9515 return Err(syn::Error::new(
9516 s.span(),
9517 format!(
9518 "unknown required_db_vendor `{raw}` — \
9519 expected `postgres` (aliases: `postgresql`, `pg`), \
9520 `mysql` (alias: `mariadb`), or `sqlite` \
9521 (alias: `sqlite3`)"
9522 ),
9523 ));
9524 }
9525 }
9526 return Ok(());
9527 }
9528 if meta.path.is_ident("base_manager_name") {
9529 // Django-shape `Meta.base_manager_name` — name of the
9530 // Manager subclass that `<instance>.<relation>_set`
9531 // uses when resolving reverse-relation managers.
9532 // Distinct from `default_manager_name` (what
9533 // `Model.objects` returns at the class level).
9534 // Stored on `ModelSchema::base_manager_name`.
9535 //
9536 // Validated as a Rust identifier so it stays safe to
9537 // re-emit as code in future reverse-manager codegen.
9538 let s: LitStr = meta.value()?.parse()?;
9539 let raw = s.value();
9540 if raw.is_empty() {
9541 return Err(syn::Error::new(
9542 s.span(),
9543 "`base_manager_name` must be a non-empty string",
9544 ));
9545 }
9546 let valid = raw
9547 .chars()
9548 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9549 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9550 if !valid {
9551 return Err(syn::Error::new(
9552 s.span(),
9553 format!(
9554 "`base_manager_name` must be a valid Rust \
9555 identifier (letters / digits / underscores, \
9556 not starting with a digit); got `{raw}`"
9557 ),
9558 ));
9559 }
9560 out.base_manager_name = Some(raw);
9561 return Ok(());
9562 }
9563 if meta.path.is_ident("default_related_name") {
9564 // Django-shape `Meta.default_related_name` — the name
9565 // reverse-relation accessors use when callers don't
9566 // override `related_name=...` on the FK / M2M field.
9567 // Stored on `ModelSchema::default_related_name` so
9568 // future reverse-manager codegen / DRF schema emit /
9569 // admin templates can pick the right accessor name
9570 // (today rustango doesn't auto-emit reverse managers;
9571 // the metadata is the foundation for that work).
9572 //
9573 // Django requires snake_case + no `+` suffix; we
9574 // enforce non-empty + ASCII identifier-shape so the
9575 // string is safe to use as a Rust ident later.
9576 let s: LitStr = meta.value()?.parse()?;
9577 let raw = s.value();
9578 if raw.is_empty() {
9579 return Err(syn::Error::new(
9580 s.span(),
9581 "`default_related_name` must be a non-empty string",
9582 ));
9583 }
9584 let valid = raw
9585 .chars()
9586 .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9587 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9588 if !valid {
9589 return Err(syn::Error::new(
9590 s.span(),
9591 format!(
9592 "`default_related_name` must be snake_case ASCII \
9593 (lowercase letters / digits / underscores, not \
9594 starting with a digit); got `{raw}`"
9595 ),
9596 ));
9597 }
9598 out.default_related_name = Some(raw);
9599 return Ok(());
9600 }
9601 if meta.path.is_ident("extra_permissions") {
9602 // Django-shape `Meta.permissions = [(codename, name), ...]`.
9603 // Comma-separated `codename:label` pairs.
9604 let s: LitStr = meta.value()?.parse()?;
9605 let raw = s.value();
9606 let mut pairs = Vec::new();
9607 for entry in raw.split(',') {
9608 let entry = entry.trim();
9609 if entry.is_empty() {
9610 continue;
9611 }
9612 let (codename, label) = match entry.split_once(':') {
9613 Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9614 None => (entry.to_owned(), entry.to_owned()),
9615 };
9616 if codename.is_empty() {
9617 return Err(meta.error(
9618 "`extra_permissions` entries must be `codename:label` pairs",
9619 ));
9620 }
9621 pairs.push((codename, label));
9622 }
9623 if pairs.is_empty() {
9624 return Err(meta
9625 .error("`extra_permissions = \"…\"` must list at least one pair"));
9626 }
9627 out.extra_permissions = pairs;
9628 return Ok(());
9629 }
9630 if meta.path.is_ident("default_permissions") {
9631 // Django-shape `Meta.default_permissions = ('view',
9632 // 'change')`. Comma-separated subset of the CRUD
9633 // action set. Empty means all four (the framework
9634 // default — matches Django when the option is
9635 // omitted).
9636 let s: LitStr = meta.value()?.parse()?;
9637 let raw = s.value();
9638 let mut actions: Vec<String> = Vec::new();
9639 for entry in raw.split(',') {
9640 let action = entry.trim().to_ascii_lowercase();
9641 if action.is_empty() {
9642 continue;
9643 }
9644 match action.as_str() {
9645 "add" | "change" | "delete" | "view" => {}
9646 other => {
9647 return Err(syn::Error::new(
9648 s.span(),
9649 format!(
9650 "unknown default_permissions action `{other}` — \
9651 expected one of `add`, `change`, `delete`, `view`"
9652 ),
9653 ));
9654 }
9655 }
9656 if !actions.contains(&action) {
9657 actions.push(action);
9658 }
9659 }
9660 if actions.is_empty() {
9661 return Err(syn::Error::new(
9662 s.span(),
9663 "`default_permissions = \"…\"` must list at least one action; \
9664 use `permissions = false` on the container if you want NO \
9665 permissions seeded for this model.",
9666 ));
9667 }
9668 out.default_permissions = actions;
9669 return Ok(());
9670 }
9671 if meta.path.is_ident("get_latest_by") {
9672 // Django-shape `Meta.get_latest_by`. The `-` prefix
9673 // selects descending order (Django muscle memory).
9674 let s: LitStr = meta.value()?.parse()?;
9675 let raw = s.value();
9676 let trimmed = raw.trim();
9677 if trimmed.is_empty() {
9678 return Err(meta.error("`get_latest_by` must name a column"));
9679 }
9680 let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9681 (stripped.to_owned(), true)
9682 } else if let Some(stripped) = trimmed.strip_prefix('+') {
9683 (stripped.to_owned(), false)
9684 } else {
9685 (trimmed.to_owned(), false)
9686 };
9687 if col.is_empty() {
9688 return Err(meta.error("`get_latest_by` must name a column"));
9689 }
9690 out.get_latest_by = Some((col, desc));
9691 return Ok(());
9692 }
9693 if meta.path.is_ident("unique_together") {
9694 // Django-shape composite UNIQUE index. Two syntaxes:
9695 //
9696 // #[rustango(unique_together = "org_id, user_id")] — auto-derived name
9697 // #[rustango(unique_together(columns = "org_id, user_id", name = "x"))] — explicit name
9698 //
9699 // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9700 // (col1, col2)`, where <name> defaults to
9701 // `<table>_<col1>_<col2>_uq` when not supplied.
9702 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9703 out.indexes.push(IndexAttr {
9704 name,
9705 columns,
9706 unique: true,
9707 method: "btree".to_owned(),
9708 where_clause: None,
9709 include: Vec::new(),
9710 });
9711 return Ok(());
9712 }
9713 if meta.path.is_ident("index_together") {
9714 // Django-shape composite (non-unique) index. Two syntaxes
9715 // mirroring `unique_together`.
9716 //
9717 // #[rustango(index_together = "created_at, status")]
9718 // #[rustango(index_together(columns = "created_at, status", name = "x"))]
9719 let (columns, name) = parse_together_attr(&meta, "index_together")?;
9720 out.indexes.push(IndexAttr {
9721 name,
9722 columns,
9723 unique: false,
9724 method: "btree".to_owned(),
9725 where_clause: None,
9726 include: Vec::new(),
9727 });
9728 return Ok(());
9729 }
9730 if meta.path.is_ident("unique_when") {
9731 // Django 4.0+ `UniqueConstraint(condition=Q(...))` —
9732 // partial unique index. Issue #265 / T1.3.
9733 //
9734 // #[rustango(unique_when(
9735 // columns = "email",
9736 // condition = "deleted_at IS NULL",
9737 // name = "unique_active_email"
9738 // ))]
9739 //
9740 // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9741 // on PG / SQLite (both ship partial indexes natively).
9742 // MySQL falls back to a plain UNIQUE index — the
9743 // condition is lost; document the limitation in the
9744 // generated migration.
9745 let mut columns: Option<Vec<String>> = None;
9746 let mut condition: Option<String> = None;
9747 let mut name: Option<String> = None;
9748 let mut include: Vec<String> = Vec::new();
9749 meta.parse_nested_meta(|inner| {
9750 if inner.path.is_ident("columns") {
9751 let s: LitStr = inner.value()?.parse()?;
9752 columns = Some(split_field_list(&s.value()));
9753 return Ok(());
9754 }
9755 if inner.path.is_ident("condition") {
9756 let s: LitStr = inner.value()?.parse()?;
9757 condition = Some(s.value());
9758 return Ok(());
9759 }
9760 if inner.path.is_ident("name") {
9761 let s: LitStr = inner.value()?.parse()?;
9762 name = Some(s.value());
9763 return Ok(());
9764 }
9765 if inner.path.is_ident("include") {
9766 // Django `UniqueConstraint(include=[...])` — PG
9767 // 11+ covering-index columns. Non-key columns
9768 // travel with the index leaf for index-only
9769 // scans. Dropped on MySQL/SQLite by the writer.
9770 let s: LitStr = inner.value()?.parse()?;
9771 include = split_field_list(&s.value());
9772 return Ok(());
9773 }
9774 Err(inner.error(
9775 "unknown unique_when attribute (supported: \
9776 `columns = \"...\"`, `condition = \"...\"`, \
9777 `name = \"...\"`, `include = \"...\"`)",
9778 ))
9779 })?;
9780 let columns = columns.ok_or_else(|| {
9781 meta.error("`unique_when(...)` requires `columns = \"...\"`")
9782 })?;
9783 let condition = condition.ok_or_else(|| {
9784 meta.error("`unique_when(...)` requires `condition = \"...\"`")
9785 })?;
9786 if columns.is_empty() {
9787 return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9788 }
9789 out.indexes.push(IndexAttr {
9790 name,
9791 columns,
9792 unique: true,
9793 method: "btree".to_owned(),
9794 where_clause: Some(condition),
9795 include,
9796 });
9797 return Ok(());
9798 }
9799 if meta.path.is_ident("index_when") {
9800 // Django `Index(fields=..., condition=Q(...))` parity —
9801 // non-unique partial index. Sibling of `unique_when`
9802 // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9803 //
9804 // #[rustango(index_when(
9805 // columns = "status, created_at",
9806 // condition = "deleted_at IS NULL",
9807 // name = "active_status_created_idx"
9808 // ))]
9809 //
9810 // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9811 // on PG / SQLite (both ship partial indexes natively).
9812 // MySQL has no native partial-index support — the writer
9813 // emits a plain CREATE INDEX and the condition is lost;
9814 // operators wanting that selectivity on MySQL should
9815 // declare a covering index plus an application-level
9816 // filter.
9817 let mut columns: Option<Vec<String>> = None;
9818 let mut condition: Option<String> = None;
9819 let mut name: Option<String> = None;
9820 let mut method: String = "btree".to_owned();
9821 let mut include: Vec<String> = Vec::new();
9822 meta.parse_nested_meta(|inner| {
9823 if inner.path.is_ident("columns") {
9824 let s: LitStr = inner.value()?.parse()?;
9825 columns = Some(split_field_list(&s.value()));
9826 return Ok(());
9827 }
9828 if inner.path.is_ident("condition") {
9829 let s: LitStr = inner.value()?.parse()?;
9830 condition = Some(s.value());
9831 return Ok(());
9832 }
9833 if inner.path.is_ident("name") {
9834 let s: LitStr = inner.value()?.parse()?;
9835 name = Some(s.value());
9836 return Ok(());
9837 }
9838 if inner.path.is_ident("method") {
9839 let s: LitStr = inner.value()?.parse()?;
9840 method = s.value();
9841 return Ok(());
9842 }
9843 if inner.path.is_ident("include") {
9844 // Django `Index(include=[...])` — PG 11+
9845 // covering-index columns; non-key columns
9846 // travel with the index leaf. Dropped on
9847 // MySQL/SQLite.
9848 let s: LitStr = inner.value()?.parse()?;
9849 include = split_field_list(&s.value());
9850 return Ok(());
9851 }
9852 Err(inner.error(
9853 "unknown index_when attribute (supported: \
9854 `columns = \"...\"`, `condition = \"...\"`, \
9855 `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9856 `include = \"...\"`)",
9857 ))
9858 })?;
9859 let columns = columns
9860 .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9861 let condition = condition.ok_or_else(|| {
9862 meta.error("`index_when(...)` requires `condition = \"...\"`")
9863 })?;
9864 if columns.is_empty() {
9865 return Err(meta.error("`index_when(columns = \"\")` is empty"));
9866 }
9867 out.indexes.push(IndexAttr {
9868 name,
9869 columns,
9870 unique: false,
9871 method,
9872 where_clause: Some(condition),
9873 include,
9874 });
9875 return Ok(());
9876 }
9877 if meta.path.is_ident("index") {
9878 // Container-level composite index. Two syntaxes:
9879 // #[rustango(index = "col1, col2")] — bare, non-unique btree
9880 // #[rustango(index("col1, col2"))] — call form (same result)
9881 // #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
9882 // The call form takes the column list as a leading string
9883 // literal, then optional `unique` / `name = "..."` /
9884 // `method = "..."` flags (a leading literal can't compose
9885 // under `parse_nested_meta`, so the paren body is parsed by
9886 // hand). `unique_together` / `index_together` remain the
9887 // Django-shape aliases for the same feature.
9888 let cols_lit: LitStr;
9889 let mut unique = false;
9890 let mut name: Option<String> = None;
9891 let mut method = "btree".to_owned();
9892 if meta.input.peek(syn::token::Paren) {
9893 let content;
9894 syn::parenthesized!(content in meta.input);
9895 cols_lit = content.parse()?;
9896 while content.peek(syn::Token![,]) {
9897 content.parse::<syn::Token![,]>()?;
9898 if content.is_empty() {
9899 break;
9900 }
9901 let flag: syn::Ident = content.parse()?;
9902 if flag == "unique" {
9903 unique = true;
9904 } else if flag == "name" {
9905 content.parse::<syn::Token![=]>()?;
9906 let s: LitStr = content.parse()?;
9907 name = Some(s.value());
9908 } else if flag == "method" {
9909 content.parse::<syn::Token![=]>()?;
9910 let s: LitStr = content.parse()?;
9911 let v = s.value();
9912 match v.as_str() {
9913 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
9914 | "bloom" => method = v,
9915 other => {
9916 return Err(syn::Error::new(
9917 s.span(),
9918 format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
9919 ));
9920 }
9921 }
9922 } else {
9923 return Err(syn::Error::new(
9924 flag.span(),
9925 "unknown index flag (supported: `unique`, `name`, `method`)",
9926 ));
9927 }
9928 }
9929 } else {
9930 cols_lit = meta.value()?.parse()?;
9931 }
9932 let columns = split_field_list(&cols_lit.value());
9933 out.indexes.push(IndexAttr {
9934 name,
9935 columns,
9936 unique,
9937 method,
9938 where_clause: None,
9939 include: Vec::new(),
9940 });
9941 return Ok(());
9942 }
9943 if meta.path.is_ident("check") {
9944 // #[rustango(check(name = "…", expr = "…"))]
9945 let mut name: Option<String> = None;
9946 let mut expr: Option<String> = None;
9947 meta.parse_nested_meta(|inner| {
9948 if inner.path.is_ident("name") {
9949 let s: LitStr = inner.value()?.parse()?;
9950 name = Some(s.value());
9951 return Ok(());
9952 }
9953 if inner.path.is_ident("expr") {
9954 let s: LitStr = inner.value()?.parse()?;
9955 expr = Some(s.value());
9956 return Ok(());
9957 }
9958 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
9959 })?;
9960 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
9961 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
9962 out.checks.push(CheckAttr { name, expr });
9963 return Ok(());
9964 }
9965 if meta.path.is_ident("exclude") {
9966 // #[rustango(exclude(name = "…", using = "gist",
9967 // elements = "col WITH op, col WITH op",
9968 // where = "…"))]
9969 let mut name: Option<String> = None;
9970 let mut using: Option<String> = None;
9971 let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
9972 let mut where_clause: Option<String> = None;
9973 meta.parse_nested_meta(|inner| {
9974 if inner.path.is_ident("name") {
9975 let s: LitStr = inner.value()?.parse()?;
9976 name = Some(s.value());
9977 return Ok(());
9978 }
9979 if inner.path.is_ident("using") {
9980 let s: LitStr = inner.value()?.parse()?;
9981 using = Some(s.value());
9982 return Ok(());
9983 }
9984 if inner.path.is_ident("elements") {
9985 let s: LitStr = inner.value()?.parse()?;
9986 elements_raw = Some((s.value(), s.span()));
9987 return Ok(());
9988 }
9989 if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
9990 let s: LitStr = inner.value()?.parse()?;
9991 where_clause = Some(s.value());
9992 return Ok(());
9993 }
9994 Err(inner.error(
9995 "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
9996 ))
9997 })?;
9998 let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
9999 let using = using.unwrap_or_else(|| "gist".to_owned());
10000 let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
10001 meta.error(
10002 "exclude requires `elements = \"col WITH op, col WITH op\"`",
10003 )
10004 })?;
10005 // Parse `col WITH op` pairs separated by commas.
10006 let mut elements: Vec<(String, String)> = Vec::new();
10007 for pair in elements_str.split(',') {
10008 let pair = pair.trim();
10009 if pair.is_empty() {
10010 continue;
10011 }
10012 let mut split = pair.splitn(2, |c: char| c.is_whitespace());
10013 let col = split.next().unwrap_or("").trim();
10014 let rest = split.next().unwrap_or("").trim();
10015 // `WITH op` — case-insensitive on `WITH`, then op.
10016 let rest_lc = rest.to_ascii_lowercase();
10017 let op = rest_lc
10018 .strip_prefix("with")
10019 .map(|r| r.trim_start())
10020 .filter(|r| !r.is_empty())
10021 .map(|_| {
10022 // Pull the original-case op from `rest` after the
10023 // `WITH ` token (5 chars).
10024 rest[4..].trim_start().to_owned()
10025 });
10026 let Some(op) = op else {
10027 return Err(syn::Error::new(
10028 elements_span,
10029 format!(
10030 "exclude elements: `{pair}` must be `<col> WITH <op>` \
10031 (e.g. `room_id WITH =` or `during WITH &&`)"
10032 ),
10033 ));
10034 };
10035 if col.is_empty() || op.is_empty() {
10036 return Err(syn::Error::new(
10037 elements_span,
10038 format!(
10039 "exclude elements: `{pair}` must be `<col> WITH <op>` \
10040 (both sides non-empty)"
10041 ),
10042 ));
10043 }
10044 elements.push((col.to_owned(), op));
10045 }
10046 if elements.is_empty() {
10047 return Err(syn::Error::new(
10048 elements_span,
10049 "exclude requires at least one `col WITH op` element",
10050 ));
10051 }
10052 out.excludes.push(ExcludeAttr {
10053 name,
10054 using,
10055 elements,
10056 where_clause,
10057 });
10058 return Ok(());
10059 }
10060 if meta.path.is_ident("generic_fk") {
10061 let mut gfk = GenericFkAttr {
10062 name: String::new(),
10063 ct_column: String::new(),
10064 pk_column: String::new(),
10065 };
10066 meta.parse_nested_meta(|inner| {
10067 if inner.path.is_ident("name") {
10068 let s: LitStr = inner.value()?.parse()?;
10069 gfk.name = s.value();
10070 return Ok(());
10071 }
10072 if inner.path.is_ident("ct_column") {
10073 let s: LitStr = inner.value()?.parse()?;
10074 gfk.ct_column = s.value();
10075 return Ok(());
10076 }
10077 if inner.path.is_ident("pk_column") {
10078 let s: LitStr = inner.value()?.parse()?;
10079 gfk.pk_column = s.value();
10080 return Ok(());
10081 }
10082 Err(inner.error(
10083 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10084 ))
10085 })?;
10086 if gfk.name.is_empty() {
10087 return Err(meta.error("generic_fk requires `name = \"...\"`"));
10088 }
10089 if gfk.ct_column.is_empty() {
10090 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10091 }
10092 if gfk.pk_column.is_empty() {
10093 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10094 }
10095 out.generic_fks.push(gfk);
10096 return Ok(());
10097 }
10098 if meta.path.is_ident("fk_composite") {
10099 let mut fk = CompositeFkAttr {
10100 name: String::new(),
10101 to: String::new(),
10102 from: Vec::new(),
10103 on: Vec::new(),
10104 };
10105 meta.parse_nested_meta(|inner| {
10106 if inner.path.is_ident("name") {
10107 let s: LitStr = inner.value()?.parse()?;
10108 fk.name = s.value();
10109 return Ok(());
10110 }
10111 if inner.path.is_ident("to") {
10112 let s: LitStr = inner.value()?.parse()?;
10113 fk.to = s.value();
10114 return Ok(());
10115 }
10116 // `on = ("col1", "col2", ...)` — parse a parenthesised
10117 // comma-list of string literals.
10118 if inner.path.is_ident("on") || inner.path.is_ident("from") {
10119 let value = inner.value()?;
10120 let content;
10121 syn::parenthesized!(content in value);
10122 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10123 content.parse_terminated(
10124 |p| p.parse::<syn::LitStr>(),
10125 syn::Token![,],
10126 )?;
10127 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10128 if inner.path.is_ident("on") {
10129 fk.on = cols;
10130 } else {
10131 fk.from = cols;
10132 }
10133 return Ok(());
10134 }
10135 Err(inner.error(
10136 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10137 ))
10138 })?;
10139 if fk.name.is_empty() {
10140 return Err(meta.error("fk_composite requires `name = \"...\"`"));
10141 }
10142 if fk.to.is_empty() {
10143 return Err(meta.error("fk_composite requires `to = \"...\"`"));
10144 }
10145 if fk.from.is_empty() || fk.on.is_empty() {
10146 return Err(meta.error(
10147 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10148 ));
10149 }
10150 if fk.from.len() != fk.on.len() {
10151 return Err(meta.error(format!(
10152 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10153 fk.from.len(),
10154 fk.on.len(),
10155 )));
10156 }
10157 out.composite_fks.push(fk);
10158 return Ok(());
10159 }
10160 if meta.path.is_ident("m2m") {
10161 let mut m2m = M2MAttr {
10162 name: String::new(),
10163 to: String::new(),
10164 through: String::new(),
10165 src: String::new(),
10166 dst: String::new(),
10167 auto_create: true,
10168 };
10169 meta.parse_nested_meta(|inner| {
10170 if inner.path.is_ident("name") {
10171 let s: LitStr = inner.value()?.parse()?;
10172 m2m.name = s.value();
10173 return Ok(());
10174 }
10175 if inner.path.is_ident("to") {
10176 let s: LitStr = inner.value()?.parse()?;
10177 m2m.to = s.value();
10178 return Ok(());
10179 }
10180 if inner.path.is_ident("through") {
10181 let s: LitStr = inner.value()?.parse()?;
10182 m2m.through = s.value();
10183 return Ok(());
10184 }
10185 if inner.path.is_ident("src") {
10186 let s: LitStr = inner.value()?.parse()?;
10187 m2m.src = s.value();
10188 return Ok(());
10189 }
10190 if inner.path.is_ident("dst") {
10191 let s: LitStr = inner.value()?.parse()?;
10192 m2m.dst = s.value();
10193 return Ok(());
10194 }
10195 if inner.path.is_ident("auto_create") {
10196 let lit: syn::LitBool = inner.value()?.parse()?;
10197 m2m.auto_create = lit.value;
10198 return Ok(());
10199 }
10200 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10201 })?;
10202 if m2m.name.is_empty() {
10203 return Err(meta.error("m2m requires `name = \"...\"`"));
10204 }
10205 if m2m.to.is_empty() {
10206 return Err(meta.error("m2m requires `to = \"...\"`"));
10207 }
10208 if m2m.through.is_empty() {
10209 return Err(meta.error("m2m requires `through = \"...\"`"));
10210 }
10211 if m2m.src.is_empty() {
10212 return Err(meta.error("m2m requires `src = \"...\"`"));
10213 }
10214 if m2m.dst.is_empty() {
10215 return Err(meta.error("m2m requires `dst = \"...\"`"));
10216 }
10217 out.m2m.push(m2m);
10218 return Ok(());
10219 }
10220 if meta.path.is_ident("generic_m2m") {
10221 let mut gm = GenericM2MAttr {
10222 name: String::new(),
10223 through: String::new(),
10224 pk_column: String::new(),
10225 ct_column: String::new(),
10226 related_column: String::new(),
10227 };
10228 meta.parse_nested_meta(|inner| {
10229 let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10230 let s: LitStr = inner.value()?.parse()?;
10231 Ok(s.value())
10232 };
10233 if inner.path.is_ident("name") {
10234 gm.name = field(&inner)?;
10235 return Ok(());
10236 }
10237 if inner.path.is_ident("through") {
10238 gm.through = field(&inner)?;
10239 return Ok(());
10240 }
10241 if inner.path.is_ident("pk_column") {
10242 gm.pk_column = field(&inner)?;
10243 return Ok(());
10244 }
10245 if inner.path.is_ident("ct_column") {
10246 gm.ct_column = field(&inner)?;
10247 return Ok(());
10248 }
10249 if inner.path.is_ident("related_column") {
10250 gm.related_column = field(&inner)?;
10251 return Ok(());
10252 }
10253 Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10254 })?;
10255 for (val, label) in [
10256 (&gm.name, "name"),
10257 (&gm.through, "through"),
10258 (&gm.pk_column, "pk_column"),
10259 (&gm.ct_column, "ct_column"),
10260 (&gm.related_column, "related_column"),
10261 ] {
10262 if val.is_empty() {
10263 return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10264 }
10265 }
10266 out.generic_m2m.push(gm);
10267 return Ok(());
10268 }
10269 Err(meta.error("unknown rustango container attribute"))
10270 })?;
10271 }
10272 Ok(out)
10273}
10274
10275/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10276/// owned field names, trimming whitespace and skipping empty entries.
10277/// Field-name validation against the model is done by the caller.
10278fn split_field_list(raw: &str) -> Vec<String> {
10279 raw.split(',')
10280 .map(str::trim)
10281 .filter(|s| !s.is_empty())
10282 .map(str::to_owned)
10283 .collect()
10284}
10285
10286/// Shared parser for `unique_together` and `index_together` container
10287/// attrs. Accepts both shapes:
10288///
10289/// * `attr = "col1, col2"` — auto-derived index name.
10290/// * `attr(columns = "col1, col2", name = "...")` — explicit name.
10291///
10292/// Returns `(columns, name)`.
10293fn parse_together_attr(
10294 meta: &syn::meta::ParseNestedMeta<'_>,
10295 attr: &str,
10296) -> syn::Result<(Vec<String>, Option<String>)> {
10297 // Disambiguate by whether the next token is `=` (key-value) or
10298 // `(` (parenthesized).
10299 if meta.input.peek(syn::Token![=]) {
10300 let cols_lit: LitStr = meta.value()?.parse()?;
10301 let columns = split_field_list(&cols_lit.value());
10302 check_together_columns(meta, attr, &columns)?;
10303 return Ok((columns, None));
10304 }
10305 let mut columns: Option<Vec<String>> = None;
10306 let mut name: Option<String> = None;
10307 meta.parse_nested_meta(|inner| {
10308 if inner.path.is_ident("columns") {
10309 let s: LitStr = inner.value()?.parse()?;
10310 columns = Some(split_field_list(&s.value()));
10311 return Ok(());
10312 }
10313 if inner.path.is_ident("name") {
10314 let s: LitStr = inner.value()?.parse()?;
10315 name = Some(s.value());
10316 return Ok(());
10317 }
10318 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10319 })?;
10320 let columns = columns.ok_or_else(|| {
10321 meta.error(format!(
10322 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10323 ))
10324 })?;
10325 check_together_columns(meta, attr, &columns)?;
10326 Ok((columns, name))
10327}
10328
10329fn check_together_columns(
10330 meta: &syn::meta::ParseNestedMeta<'_>,
10331 attr: &str,
10332 columns: &[String],
10333) -> syn::Result<()> {
10334 if columns.len() < 2 {
10335 let single = if attr == "unique_together" {
10336 "#[rustango(unique)] on the field"
10337 } else {
10338 "#[rustango(index)] on the field"
10339 };
10340 return Err(meta.error(format!(
10341 "{attr} expects two or more columns; for a single-column equivalent use {single}",
10342 )));
10343 }
10344 Ok(())
10345}
10346
10347/// Parse the fieldsets DSL: pipe-separated sections, optional
10348/// `"Title:"` prefix on each, comma-separated field names after.
10349/// Examples:
10350/// * `"name, office"` → one untitled section with two fields
10351/// * `"Identity: name, office | Metadata: created_at"` → two titled
10352/// sections
10353///
10354/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10355fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10356 raw.split('|')
10357 .map(str::trim)
10358 .filter(|s| !s.is_empty())
10359 .map(|section| {
10360 // Split off an optional `Title:` prefix (first colon).
10361 let (title, rest) = match section.split_once(':') {
10362 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10363 _ => (String::new(), section),
10364 };
10365 let fields = split_field_list(rest);
10366 (title, fields)
10367 })
10368 .collect()
10369}
10370
10371/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10372/// comma-separated entry maps a target field to one or more source
10373/// fields joined with `+`. Whitespace around tokens is trimmed.
10374/// Entries missing `:` or with empty target/source lists are dropped.
10375fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10376 raw.split(',')
10377 .map(str::trim)
10378 .filter(|s| !s.is_empty())
10379 .filter_map(|entry| {
10380 let (target, sources_raw) = entry.split_once(':')?;
10381 let target = target.trim().to_owned();
10382 if target.is_empty() {
10383 return None;
10384 }
10385 let sources: Vec<String> = sources_raw
10386 .split('+')
10387 .map(|s| s.trim().to_owned())
10388 .filter(|s| !s.is_empty())
10389 .collect();
10390 if sources.is_empty() {
10391 return None;
10392 }
10393 Some((target, sources))
10394 })
10395 .collect()
10396}
10397
10398/// Parse Django-shape `formfield_overrides` — `"field:widget,field2:widget2"`
10399/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10400/// and empty halves drop silently — the macro layer only enforces shape,
10401/// not field-name vs. widget-name validity (those checks happen at
10402/// `AdminConfig` consumption time). Issue #359.
10403fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10404 raw.split(',')
10405 .map(str::trim)
10406 .filter(|s| !s.is_empty())
10407 .filter_map(|entry| {
10408 let (field, widget) = entry.split_once(':')?;
10409 let field = field.trim().to_owned();
10410 let widget = widget.trim().to_owned();
10411 if field.is_empty() || widget.is_empty() {
10412 return None;
10413 }
10414 Some((field, widget))
10415 })
10416 .collect()
10417}
10418
10419/// Parse Django-shape ordering — `"name"` is ASC, `"-name"` is DESC.
10420/// Returns `(field_name, desc)` pairs in the same order as the input.
10421fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10422 raw.split(',')
10423 .map(str::trim)
10424 .filter(|s| !s.is_empty())
10425 .map(|spec| {
10426 spec.strip_prefix('-')
10427 .map_or((spec.to_owned(), false), |rest| {
10428 (rest.trim().to_owned(), true)
10429 })
10430 })
10431 .collect()
10432}
10433
10434struct FieldAttrs {
10435 column: Option<String>,
10436 primary_key: bool,
10437 fk: Option<String>,
10438 o2o: Option<String>,
10439 on: Option<String>,
10440 /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10441 /// "set_default" | "no_action")]` — Django-shape
10442 /// `ForeignKey(on_delete=…)`. Only meaningful when `fk` / `o2o` is
10443 /// also set; the macro errors at compile time if applied to a
10444 /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10445 /// DDL writer renders `ON DELETE <action>` after the constraint
10446 /// clause when this is `Some`; `None` falls back to the database
10447 /// default (NO ACTION on every backend rustango supports).
10448 on_delete: Option<String>,
10449 /// `#[rustango(related_name = "...")]` — Django-shape per-FK
10450 /// reverse-accessor override. When set, the derive emits
10451 /// `Parent::<related_name>[_pool]` instead of the container-level
10452 /// `default_related_name` or the `<child_snake>_set[_pool]`
10453 /// fallback. Only meaningful when `fk` / `o2o` is also set;
10454 /// silently ignored on non-FK fields. Follow-up to #816.
10455 related_name: Option<String>,
10456 max_length: Option<u32>,
10457 /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10458 /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10459 /// unconstrained `vector` column.
10460 vector_dims: Option<u32>,
10461 /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10462 /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10463 /// unconstrained `geometry(Point)` column (SRID 0).
10464 geometry_srid: Option<u32>,
10465 min: Option<i64>,
10466 max: Option<i64>,
10467 default: Option<String>,
10468 /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10469 /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10470 /// "gen_random_uuid()"`. The Rust field type must be
10471 /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10472 /// INSERTs so the DB DEFAULT fires.
10473 auto_uuid: bool,
10474 /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10475 /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10476 /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10477 /// the field is `Auto::Unset`, then bound as a normal parameter
10478 /// rather than relying on a per-dialect DB function. Issue #823
10479 /// (Eloquent `HasUuids`).
10480 ///
10481 /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10482 /// Composes with every backend (PG / MySQL / SQLite) — no
10483 /// `gen_random_uuid()` requirement on the database.
10484 default_uuid_v7: bool,
10485 /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10486 /// Server-set on insert, immutable from app code afterwards.
10487 /// Implies `auto + default = "now()"`. Field type must be
10488 /// `DateTime<Utc>`.
10489 auto_now_add: bool,
10490 /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10491 /// every insert AND every update. Implies `auto + default =
10492 /// "now()"`; the macro additionally rewrites `update_on` /
10493 /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10494 /// field value.
10495 auto_now: bool,
10496 /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10497 /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10498 /// `soft_delete_on(executor)` and `restore_on(executor)`
10499 /// methods on the model.
10500 soft_delete: bool,
10501 /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10502 /// the column in the generated DDL.
10503 unique: bool,
10504 /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10505 /// generates a `CREATE INDEX` for this column. `unique` here means
10506 /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10507 index: bool,
10508 index_unique: bool,
10509 index_name: Option<String>,
10510 /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10511 /// `"btree"`. Issue #34.
10512 index_method: String,
10513 /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10514 /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10515 /// the macro skips this column from every INSERT and UPDATE
10516 /// path, so the database always recomputes the value from
10517 /// `EXPR`. Backlog item #35.
10518 generated_as: Option<String>,
10519 /// `#[rustango(help_text = "…")]` — Django-shape help text
10520 /// rendered below the admin form's input. Threaded into
10521 /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10522 /// layers can read it.
10523 help_text: Option<String>,
10524 /// `#[rustango(choices = "value:Label, value:Label")]` — Django-shape
10525 /// enumerated allowed values. Threaded into `FieldSchema::choices`
10526 /// as a `&'static [(&'static str, &'static str)]` slice. When
10527 /// present, the admin form renders a `<select>` instead of `<input>`
10528 /// and the validator rejects values not in the list. Only meaningful
10529 /// for `FieldType::String`; the macro errors at compile time if
10530 /// applied to a non-string field.
10531 choices: Option<Vec<(String, String)>>,
10532 /// `#[rustango(db_comment = "…")]` — Django-shape DB-side column
10533 /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10534 /// the comment in CREATE TABLE; Postgres emits a separate
10535 /// `COMMENT ON COLUMN` statement after the table is created;
10536 /// SQLite silently drops the value (no native column comments).
10537 db_comment: Option<String>,
10538 /// `#[rustango(verbose_name = "…")]` — Django-shape human-readable
10539 /// label for the field. Threaded into `FieldSchema::verbose_name`
10540 /// so admin column headers, form labels, and other display
10541 /// surfaces can prefer the friendly caption over the Rust
10542 /// identifier. `None` means renderers fall back to the field name.
10543 verbose_name: Option<String>,
10544 /// `#[rustango(editable = false)]` — Django-shape opt-out from
10545 /// auto-generated form rendering. Defaults to `true` so existing
10546 /// fields keep their current admin / form behavior; setting
10547 /// `false` removes the field from the admin change-form entirely
10548 /// (the value is still visible on detail / list views, just not
10549 /// editable).
10550 editable: bool,
10551 /// `#[rustango(blank)]` / `#[rustango(blank = true)]` — Django-shape
10552 /// "form may submit empty even when DB is NOT NULL". Threaded into
10553 /// `FieldSchema::blank`. Defaults to `false`.
10554 blank: bool,
10555 /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10556 /// Django-shape `CITextField`. Threaded into
10557 /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10558 /// fields; the macro errors at derive time if applied elsewhere.
10559 case_insensitive: bool,
10560 /// `#[rustango(validators = "email,url")]` — Django-shape
10561 /// model-level validator chain. Comma-separated names that
10562 /// dispatch to the `validators::*` family in `validate_value`.
10563 /// Empty by default; fires on every typed INSERT/UPDATE.
10564 validators: Vec<String>,
10565}
10566
10567fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10568 let mut out = FieldAttrs {
10569 column: None,
10570 primary_key: false,
10571 fk: None,
10572 o2o: None,
10573 on: None,
10574 on_delete: None,
10575 related_name: None,
10576 max_length: None,
10577 vector_dims: None,
10578 geometry_srid: None,
10579 min: None,
10580 max: None,
10581 default: None,
10582 auto_uuid: false,
10583 default_uuid_v7: false,
10584 auto_now_add: false,
10585 auto_now: false,
10586 soft_delete: false,
10587 unique: false,
10588 index: false,
10589 index_unique: false,
10590 index_name: None,
10591 index_method: "btree".to_owned(),
10592 generated_as: None,
10593 help_text: None,
10594 choices: None,
10595 db_comment: None,
10596 verbose_name: None,
10597 editable: true,
10598 blank: false,
10599 case_insensitive: false,
10600 validators: Vec::new(),
10601 };
10602 for attr in &field.attrs {
10603 if !attr.path().is_ident("rustango") {
10604 continue;
10605 }
10606 attr.parse_nested_meta(|meta| {
10607 if meta.path.is_ident("column") {
10608 let s: LitStr = meta.value()?.parse()?;
10609 let name = s.value();
10610 validate_sql_identifier(&name, "column", s.span())?;
10611 out.column = Some(name);
10612 return Ok(());
10613 }
10614 if meta.path.is_ident("primary_key") {
10615 out.primary_key = true;
10616 return Ok(());
10617 }
10618 if meta.path.is_ident("fk") {
10619 let s: LitStr = meta.value()?.parse()?;
10620 out.fk = Some(s.value());
10621 return Ok(());
10622 }
10623 if meta.path.is_ident("o2o") {
10624 let s: LitStr = meta.value()?.parse()?;
10625 out.o2o = Some(s.value());
10626 return Ok(());
10627 }
10628 if meta.path.is_ident("on") {
10629 let s: LitStr = meta.value()?.parse()?;
10630 out.on = Some(s.value());
10631 return Ok(());
10632 }
10633 if meta.path.is_ident("on_delete") {
10634 let s: LitStr = meta.value()?.parse()?;
10635 let raw = s.value();
10636 let normalized = raw.trim().to_ascii_lowercase();
10637 // Validate at parse time so the user gets a clear span
10638 // rather than a downstream compile error in the emit.
10639 match normalized.as_str() {
10640 "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10641 _ => {
10642 return Err(syn::Error::new(
10643 s.span(),
10644 format!(
10645 "unknown on_delete action `{raw}`; expected one of \
10646 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10647 ),
10648 ));
10649 }
10650 }
10651 out.on_delete = Some(normalized);
10652 return Ok(());
10653 }
10654 if meta.path.is_ident("related_name") {
10655 let s: LitStr = meta.value()?.parse()?;
10656 let raw = s.value();
10657 if raw.trim().is_empty() {
10658 return Err(syn::Error::new(
10659 s.span(),
10660 "`related_name` must be a non-empty identifier",
10661 ));
10662 }
10663 // Validate as a Rust-ident shape — the value becomes
10664 // a method name on the parent type. Same rule as
10665 // `default_related_name` so the two surfaces match.
10666 if !raw
10667 .chars()
10668 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10669 || raw.starts_with(char::is_numeric)
10670 {
10671 return Err(syn::Error::new(
10672 s.span(),
10673 "`related_name` must be snake_case ASCII (lowercase letters, \
10674 digits, underscores; no leading digit)",
10675 ));
10676 }
10677 out.related_name = Some(raw);
10678 return Ok(());
10679 }
10680 if meta.path.is_ident("max_length") {
10681 let lit: syn::LitInt = meta.value()?.parse()?;
10682 out.max_length = Some(lit.base10_parse::<u32>()?);
10683 return Ok(());
10684 }
10685 // `#[rustango(vector(dims = N))]` — pgvector column
10686 // dimension (#824). Nested-meta form so it reads like the
10687 // other typed-column attrs.
10688 if meta.path.is_ident("vector") {
10689 meta.parse_nested_meta(|inner| {
10690 if inner.path.is_ident("dims") {
10691 let lit: syn::LitInt = inner.value()?.parse()?;
10692 out.vector_dims = Some(lit.base10_parse::<u32>()?);
10693 return Ok(());
10694 }
10695 Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10696 })?;
10697 return Ok(());
10698 }
10699 // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10700 // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10701 if meta.path.is_ident("geometry") {
10702 meta.parse_nested_meta(|inner| {
10703 if inner.path.is_ident("srid") {
10704 let lit: syn::LitInt = inner.value()?.parse()?;
10705 out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10706 return Ok(());
10707 }
10708 Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10709 })?;
10710 return Ok(());
10711 }
10712 if meta.path.is_ident("min") {
10713 out.min = Some(parse_signed_i64(&meta)?);
10714 return Ok(());
10715 }
10716 if meta.path.is_ident("max") {
10717 out.max = Some(parse_signed_i64(&meta)?);
10718 return Ok(());
10719 }
10720 if meta.path.is_ident("default") {
10721 let s: LitStr = meta.value()?.parse()?;
10722 out.default = Some(s.value());
10723 return Ok(());
10724 }
10725 if meta.path.is_ident("generated_as") {
10726 let s: LitStr = meta.value()?.parse()?;
10727 out.generated_as = Some(s.value());
10728 return Ok(());
10729 }
10730 if meta.path.is_ident("help_text") {
10731 let s: LitStr = meta.value()?.parse()?;
10732 out.help_text = Some(s.value());
10733 return Ok(());
10734 }
10735 if meta.path.is_ident("choices") {
10736 let s: LitStr = meta.value()?.parse()?;
10737 let raw = s.value();
10738 let mut pairs: Vec<(String, String)> = Vec::new();
10739 for chunk in raw.split(',') {
10740 let chunk = chunk.trim();
10741 if chunk.is_empty() {
10742 continue;
10743 }
10744 let (value, label) = match chunk.split_once(':') {
10745 Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10746 None => (chunk.to_owned(), chunk.to_owned()),
10747 };
10748 if value.is_empty() {
10749 return Err(syn::Error::new(
10750 s.span(),
10751 "`choices` entry has empty value before `:`",
10752 ));
10753 }
10754 pairs.push((value, label));
10755 }
10756 if pairs.is_empty() {
10757 return Err(syn::Error::new(
10758 s.span(),
10759 "`choices = \"…\"` must contain at least one value",
10760 ));
10761 }
10762 out.choices = Some(pairs);
10763 return Ok(());
10764 }
10765 if meta.path.is_ident("db_comment") {
10766 let s: LitStr = meta.value()?.parse()?;
10767 out.db_comment = Some(s.value());
10768 return Ok(());
10769 }
10770 if meta.path.is_ident("verbose_name") {
10771 let s: LitStr = meta.value()?.parse()?;
10772 out.verbose_name = Some(s.value());
10773 return Ok(());
10774 }
10775 if meta.path.is_ident("editable") {
10776 // Two forms accepted:
10777 // #[rustango(editable = false)] / true — explicit
10778 // #[rustango(editable)] — flag form (= true, the
10779 // default, so harmless; included for symmetry)
10780 if let Ok(v) = meta.value() {
10781 let lit: syn::LitBool = v.parse()?;
10782 out.editable = lit.value;
10783 } else {
10784 out.editable = true;
10785 }
10786 return Ok(());
10787 }
10788 if meta.path.is_ident("blank") {
10789 // Two forms accepted:
10790 // #[rustango(blank)] — flag form, true
10791 // #[rustango(blank = false)] / true — explicit
10792 if let Ok(v) = meta.value() {
10793 let lit: syn::LitBool = v.parse()?;
10794 out.blank = lit.value;
10795 } else {
10796 out.blank = true;
10797 }
10798 return Ok(());
10799 }
10800 if meta.path.is_ident("citext") {
10801 // Django-parity CITextField (#344). Two forms:
10802 // #[rustango(citext)] — flag form, true
10803 // #[rustango(citext = true)] — explicit
10804 // #[rustango(citext = false)] — explicit opt-out
10805 // String-only validation lives in the field-type
10806 // emitter (the FieldType discriminant is computed in
10807 // `detect_type`); the macro records the flag and the
10808 // DDL writer emits dialect-specific COLLATE / CITEXT.
10809 if let Ok(v) = meta.value() {
10810 let lit: syn::LitBool = v.parse()?;
10811 out.case_insensitive = lit.value;
10812 } else {
10813 out.case_insensitive = true;
10814 }
10815 return Ok(());
10816 }
10817 if meta.path.is_ident("validators") {
10818 let s: LitStr = meta.value()?.parse()?;
10819 let raw = s.value();
10820 out.validators = raw
10821 .split(',')
10822 .map(str::trim)
10823 .filter(|s| !s.is_empty())
10824 .map(str::to_owned)
10825 .collect();
10826 if out.validators.is_empty() {
10827 return Err(syn::Error::new(
10828 s.span(),
10829 "`validators = \"…\"` must list at least one name",
10830 ));
10831 }
10832 return Ok(());
10833 }
10834 if meta.path.is_ident("auto_uuid") {
10835 out.auto_uuid = true;
10836 // Implied: PK + auto + DEFAULT gen_random_uuid().
10837 // Each is also explicitly settable; the explicit
10838 // value wins if conflicting.
10839 out.primary_key = true;
10840 if out.default.is_none() {
10841 out.default = Some("gen_random_uuid()".into());
10842 }
10843 return Ok(());
10844 }
10845 if meta.path.is_ident("default_uuid_v7") {
10846 // Backend-neutral counterpart of `auto_uuid` — issue #823.
10847 // No SQL DEFAULT (the macro fills the value Rust-side
10848 // before binding); just mark the field as PK + Auto
10849 // so the insert path is the `Auto::Unset → generate`
10850 // branch.
10851 out.default_uuid_v7 = true;
10852 out.primary_key = true;
10853 return Ok(());
10854 }
10855 if meta.path.is_ident("auto_now_add") {
10856 out.auto_now_add = true;
10857 if out.default.is_none() {
10858 out.default = Some("now()".into());
10859 }
10860 return Ok(());
10861 }
10862 if meta.path.is_ident("auto_now") {
10863 out.auto_now = true;
10864 if out.default.is_none() {
10865 out.default = Some("now()".into());
10866 }
10867 return Ok(());
10868 }
10869 if meta.path.is_ident("soft_delete") {
10870 out.soft_delete = true;
10871 return Ok(());
10872 }
10873 if meta.path.is_ident("unique") {
10874 out.unique = true;
10875 return Ok(());
10876 }
10877 if meta.path.is_ident("index") {
10878 out.index = true;
10879 // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
10880 if meta.input.peek(syn::token::Paren) {
10881 meta.parse_nested_meta(|inner| {
10882 if inner.path.is_ident("unique") {
10883 out.index_unique = true;
10884 return Ok(());
10885 }
10886 if inner.path.is_ident("name") {
10887 let s: LitStr = inner.value()?.parse()?;
10888 out.index_name = Some(s.value());
10889 return Ok(());
10890 }
10891 if inner.path.is_ident("method") {
10892 let s: LitStr = inner.value()?.parse()?;
10893 let v = s.value();
10894 match v.as_str() {
10895 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
10896 out.index_method = v;
10897 }
10898 other => {
10899 return Err(inner.error(format!(
10900 "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
10901 )));
10902 }
10903 }
10904 return Ok(());
10905 }
10906 Err(inner.error(
10907 "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
10908 ))
10909 })?;
10910 }
10911 return Ok(());
10912 }
10913 Err(meta.error("unknown rustango field attribute"))
10914 })?;
10915 }
10916 Ok(out)
10917}
10918
10919/// Parse a signed integer literal, accepting optional leading `-`.
10920fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
10921 let expr: syn::Expr = meta.value()?.parse()?;
10922 match expr {
10923 syn::Expr::Lit(syn::ExprLit {
10924 lit: syn::Lit::Int(lit),
10925 ..
10926 }) => lit.base10_parse::<i64>(),
10927 syn::Expr::Unary(syn::ExprUnary {
10928 op: syn::UnOp::Neg(_),
10929 expr,
10930 ..
10931 }) => {
10932 if let syn::Expr::Lit(syn::ExprLit {
10933 lit: syn::Lit::Int(lit),
10934 ..
10935 }) = *expr
10936 {
10937 let v: i64 = lit.base10_parse()?;
10938 Ok(-v)
10939 } else {
10940 Err(syn::Error::new_spanned(expr, "expected integer literal"))
10941 }
10942 }
10943 other => Err(syn::Error::new_spanned(
10944 other,
10945 "expected integer literal (signed)",
10946 )),
10947 }
10948}
10949
10950struct FieldInfo<'a> {
10951 ident: &'a syn::Ident,
10952 column: String,
10953 primary_key: bool,
10954 /// `true` when the Rust type was `Auto<T>` — the INSERT path will
10955 /// skip this column when `Auto::Unset` and emit it under
10956 /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
10957 auto: bool,
10958 /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
10959 /// the `Column::Value` associated type for typed-column tokens.
10960 value_ty: &'a Type,
10961 /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
10962 field_type_tokens: TokenStream2,
10963 schema: TokenStream2,
10964 from_row_init: TokenStream2,
10965 /// Variant of [`Self::from_row_init`] that reads the column via
10966 /// `format!("{prefix}__{col}")` so a model can be decoded out of
10967 /// the aliased columns of a JOINed row. Drives slice 9.0d's
10968 /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
10969 /// helper that `select_related` calls when stitching loaded FKs.
10970 from_aliased_row_init: TokenStream2,
10971 /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
10972 /// relation helper emit (`Author::<child>_set`) needs to know `T`
10973 /// to point the generated method at the right child model.
10974 fk_inner: Option<Type>,
10975 /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
10976 /// `kind` (since ForeignKey detection sets `kind` to K's
10977 /// underlying type) but stored separately for clarity at the
10978 /// `FkRelation` construction site, which only sees the FK's
10979 /// surface fields.
10980 fk_pk_kind: DetectedKind,
10981 /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
10982 /// the bare `ForeignKey<T, K>`. Routes the load_related and
10983 /// fk_pk_access emitters to wrap assignments / accessors in
10984 /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
10985 /// FK column compiles end-to-end. The DDL writer reads this off
10986 /// the field schema (`nullable` flag); the macro just needs to
10987 /// keep the Rust-side codegen consistent.
10988 nullable: bool,
10989 /// `true` when this column was marked `#[rustango(auto_now)]` —
10990 /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
10991 /// column instead of the user-supplied value, so `updated_at`
10992 /// always reflects the latest write without the caller having
10993 /// to remember to set it.
10994 auto_now: bool,
10995 /// `true` when this column was marked `#[rustango(auto_now_add)]`
10996 /// — the column is server-set on INSERT (DB DEFAULT) and
10997 /// **immutable** afterwards. `update_on` / `save_on` skip the
10998 /// column entirely so a stale `created_at` value in memory never
10999 /// rewrites the persisted timestamp.
11000 auto_now_add: bool,
11001 /// `true` when this column was marked `#[rustango(soft_delete)]`.
11002 /// Triggers emission of `soft_delete_on(executor)` and
11003 /// `restore_on(executor)` on the model's inherent impl. There is
11004 /// at most one such column per model — emission asserts this.
11005 soft_delete: bool,
11006 /// `Some` when this column was marked
11007 /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
11008 /// every INSERT and UPDATE path; the database recomputes the
11009 /// value from `EXPR`. Backlog item #35.
11010 generated_as: Option<String>,
11011 /// `true` when this column was marked
11012 /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
11013 /// emit an `insert_push` that auto-fills an `Auto::Unset` value
11014 /// with `Uuid::now_v7()` before binding, so the PK is generated
11015 /// Rust-side and the column is always present in the INSERT
11016 /// statement (no DB DEFAULT requirement). Issue #823.
11017 default_uuid_v7: bool,
11018 /// `Some` when this FK field carried `#[rustango(related_name =
11019 /// "...")]`. Threaded into [`FkRelation::related_name`] and
11020 /// consumed by [`reverse_helper_tokens`] to override the default
11021 /// `<child_snake>_set` accessor name. Follow-up to #816.
11022 related_name: Option<String>,
11023}
11024
11025/// Reject table names that won't survive SQL identifier
11026/// derivation downstream. Postgres' regular-identifier rule
11027/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
11028/// through the framework's unquoted FK / index / constraint name
11029/// emission without surprises. We also disallow leading-digit and
11030/// the empty string for clarity.
11031///
11032/// Reserved-word collisions (`select`, `from`, …) aren't flagged
11033/// here — those produce a runtime error from the SQL parser,
11034/// which is loud enough; statically enumerating reserved words
11035/// across the three supported dialects is more friction than help.
11036///
11037/// Backlog item #65.
11038fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11039 validate_sql_identifier(name, "table", span)
11040}
11041
11042/// Reject SQL identifiers that compile but break downstream SQL
11043/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11044/// `kind` is "table" / "column" — used for the error message so users
11045/// see which attribute caused the failure.
11046fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11047 if name.is_empty() {
11048 return Err(syn::Error::new(
11049 span,
11050 format!("`{kind} = \"\"` is not a valid SQL identifier"),
11051 ));
11052 }
11053 let mut chars = name.chars();
11054 let first = chars.next().unwrap();
11055 if !(first.is_ascii_alphabetic() || first == '_') {
11056 return Err(syn::Error::new(
11057 span,
11058 format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11059 ));
11060 }
11061 for c in chars {
11062 if !(c.is_ascii_alphanumeric() || c == '_') {
11063 return Err(syn::Error::new(
11064 span,
11065 format!(
11066 "{kind} name `{name}` contains invalid character {c:?} — \
11067 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11068 Hyphens in particular break FK / index name derivation \
11069 downstream; use underscores instead (e.g. `{}`)",
11070 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11071 ),
11072 ));
11073 }
11074 }
11075 Ok(())
11076}
11077
11078fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11079 let root = rustango_root();
11080 let attrs = parse_field_attrs(field)?;
11081 let ident = field
11082 .ident
11083 .as_ref()
11084 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11085 let name = ident.to_string();
11086 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11087 let primary_key = attrs.primary_key;
11088 let DetectedType {
11089 kind,
11090 nullable,
11091 auto: detected_auto,
11092 fk_inner,
11093 } = detect_type(&field.ty)?;
11094 check_bound_compatibility(field, &attrs, kind)?;
11095 let auto = detected_auto;
11096 // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11097 // INSERT path: the user must wrap the field in `Auto<T>`, which
11098 // marks the column as DB-default-supplied. The mixin attrs then
11099 // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11100 // for `auto_now`, force the value on UPDATE too.
11101 if attrs.auto_uuid {
11102 if kind != DetectedKind::Uuid {
11103 return Err(syn::Error::new_spanned(
11104 field,
11105 "`#[rustango(auto_uuid)]` requires the field type to be \
11106 `Auto<uuid::Uuid>`",
11107 ));
11108 }
11109 if !detected_auto {
11110 return Err(syn::Error::new_spanned(
11111 field,
11112 "`#[rustango(auto_uuid)]` requires the field type to be \
11113 wrapped in `Auto<...>` so the macro skips the column on \
11114 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11115 ));
11116 }
11117 }
11118 if attrs.default_uuid_v7 {
11119 if kind != DetectedKind::Uuid {
11120 return Err(syn::Error::new_spanned(
11121 field,
11122 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11123 `Auto<uuid::Uuid>`",
11124 ));
11125 }
11126 if !detected_auto {
11127 return Err(syn::Error::new_spanned(
11128 field,
11129 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11130 wrapped in `Auto<...>` so the macro can detect the \
11131 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11132 ));
11133 }
11134 if attrs.auto_uuid {
11135 return Err(syn::Error::new_spanned(
11136 field,
11137 "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11138 `#[rustango(auto_uuid)]` — the former generates the UUID \
11139 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11140 Pick one.",
11141 ));
11142 }
11143 }
11144 if attrs.auto_now_add || attrs.auto_now {
11145 if kind != DetectedKind::DateTime {
11146 return Err(syn::Error::new_spanned(
11147 field,
11148 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11149 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11150 ));
11151 }
11152 if !detected_auto {
11153 return Err(syn::Error::new_spanned(
11154 field,
11155 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11156 the field type to be wrapped in `Auto<...>` so the macro skips \
11157 the column on INSERT and the DB DEFAULT (`now()`) fires",
11158 ));
11159 }
11160 }
11161 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11162 return Err(syn::Error::new_spanned(
11163 field,
11164 "`#[rustango(soft_delete)]` requires the field type to be \
11165 `Option<chrono::DateTime<chrono::Utc>>`",
11166 ));
11167 }
11168 let is_mixin_auto =
11169 attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11170 if detected_auto && !primary_key && !is_mixin_auto {
11171 return Err(syn::Error::new_spanned(
11172 field,
11173 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11174 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11175 `auto_now`",
11176 ));
11177 }
11178 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11179 return Err(syn::Error::new_spanned(
11180 field,
11181 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11182 SERIAL / BIGSERIAL already supplies a default sequence.",
11183 ));
11184 }
11185 if fk_inner.is_some() && primary_key {
11186 return Err(syn::Error::new_spanned(
11187 field,
11188 "`ForeignKey<T>` is not allowed on a primary-key field — \
11189 a row's PK is its own identity, not a reference to a parent.",
11190 ));
11191 }
11192 if attrs.generated_as.is_some() {
11193 if primary_key {
11194 return Err(syn::Error::new_spanned(
11195 field,
11196 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11197 primary-key field — a PK must be writable so the row \
11198 has an identity at INSERT time.",
11199 ));
11200 }
11201 if attrs.default.is_some() {
11202 return Err(syn::Error::new_spanned(
11203 field,
11204 "`#[rustango(generated_as = \"…\")]` cannot combine with \
11205 `default = \"…\"` — Postgres rejects DEFAULT on \
11206 generated columns. The expression IS the default.",
11207 ));
11208 }
11209 if detected_auto {
11210 return Err(syn::Error::new_spanned(
11211 field,
11212 "`#[rustango(generated_as = \"…\")]` is not allowed on \
11213 an `Auto<T>` field — generated columns are computed \
11214 by the DB, not server-assigned via a sequence. Use a \
11215 plain Rust type (e.g. `f64`).",
11216 ));
11217 }
11218 if fk_inner.is_some() {
11219 return Err(syn::Error::new_spanned(
11220 field,
11221 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11222 ForeignKey field.",
11223 ));
11224 }
11225 }
11226 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11227 let column_lit = column.as_str();
11228 // pgvector (#824): the `vector(dims = N)` attribute supplies the
11229 // dimension that the bare `Vector` Rust type can't carry, so emit
11230 // `FieldType::Vector(N)` here rather than the `variant_tokens`
11231 // fallback of `Vector(0)`.
11232 let field_type_tokens = if kind == DetectedKind::Vector {
11233 let root = rustango_root();
11234 let dims = attrs.vector_dims.unwrap_or(0);
11235 quote!(#root::core::FieldType::Vector(#dims))
11236 } else if kind == DetectedKind::Geometry {
11237 // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11238 // SRID that the bare `Point` Rust type can't carry, so emit
11239 // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11240 let root = rustango_root();
11241 let srid = attrs.geometry_srid.unwrap_or(0);
11242 quote!(#root::core::FieldType::Geometry(#srid))
11243 } else {
11244 kind.variant_tokens()
11245 };
11246 let max_length = optional_u32(attrs.max_length);
11247 let min = optional_i64(attrs.min);
11248 let max = optional_i64(attrs.max);
11249 let default = optional_str(attrs.default.as_deref());
11250
11251 let unique = attrs.unique;
11252 let generated_as = optional_str(attrs.generated_as.as_deref());
11253 let help_text = optional_str(attrs.help_text.as_deref());
11254 let choices = optional_choices(attrs.choices.as_deref());
11255 let db_comment = optional_str(attrs.db_comment.as_deref());
11256 let verbose_name = optional_str(attrs.verbose_name.as_deref());
11257 let editable = attrs.editable;
11258 let blank = attrs.blank;
11259 let case_insensitive = attrs.case_insensitive;
11260 let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11261 if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() {
11262 return Err(syn::Error::new_spanned(
11263 field,
11264 "`#[rustango(on_delete = \"…\")]` requires either `fk = \"<table>\"` \
11265 or `o2o = \"<table>\"` on the same field — it has no meaning on a \
11266 non-FK column.",
11267 ));
11268 }
11269 let fk_on_delete = match attrs.on_delete.as_deref() {
11270 None => quote!(::core::option::Option::None),
11271 Some(action) => {
11272 let variant = match action {
11273 "cascade" => quote!(Cascade),
11274 "restrict" => quote!(Restrict),
11275 "set_null" => quote!(SetNull),
11276 "set_default" => quote!(SetDefault),
11277 "no_action" => quote!(NoAction),
11278 // parse_field_attrs already validated this — guard against future drift.
11279 other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11280 };
11281 quote!(::core::option::Option::Some(
11282 #root::core::OnDeleteAction::#variant
11283 ))
11284 }
11285 };
11286 let schema = quote! {
11287 #root::core::FieldSchema {
11288 name: #name,
11289 column: #column_lit,
11290 ty: #field_type_tokens,
11291 nullable: #nullable,
11292 primary_key: #primary_key,
11293 relation: #relation,
11294 max_length: #max_length,
11295 min: #min,
11296 max: #max,
11297 default: #default,
11298 auto: #auto,
11299 unique: #unique,
11300 generated_as: #generated_as,
11301 help_text: #help_text,
11302 choices: #choices,
11303 db_comment: #db_comment,
11304 verbose_name: #verbose_name,
11305 editable: #editable,
11306 blank: #blank,
11307 case_insensitive: #case_insensitive,
11308 fk_on_delete: #fk_on_delete,
11309 validators: &[ #(#validators_lits),* ],
11310 }
11311 };
11312
11313 let from_row_init = quote! {
11314 #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11315 };
11316 let from_aliased_row_init = quote! {
11317 #ident: #root::sql::sqlx::Row::try_get(
11318 row,
11319 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11320 )?
11321 };
11322
11323 Ok(FieldInfo {
11324 ident,
11325 column,
11326 primary_key,
11327 auto,
11328 value_ty: &field.ty,
11329 field_type_tokens,
11330 schema,
11331 from_row_init,
11332 from_aliased_row_init,
11333 fk_inner: fk_inner.cloned(),
11334 fk_pk_kind: kind,
11335 nullable,
11336 auto_now: attrs.auto_now,
11337 auto_now_add: attrs.auto_now_add,
11338 soft_delete: attrs.soft_delete,
11339 generated_as: attrs.generated_as.clone(),
11340 default_uuid_v7: attrs.default_uuid_v7,
11341 related_name: attrs.related_name.clone(),
11342 })
11343}
11344
11345fn check_bound_compatibility(
11346 field: &syn::Field,
11347 attrs: &FieldAttrs,
11348 kind: DetectedKind,
11349) -> syn::Result<()> {
11350 if attrs.max_length.is_some() && kind != DetectedKind::String {
11351 return Err(syn::Error::new_spanned(
11352 field,
11353 "`max_length` is only valid on `String` fields (or `Option<String>`)",
11354 ));
11355 }
11356 if attrs.choices.is_some() && kind != DetectedKind::String {
11357 return Err(syn::Error::new_spanned(
11358 field,
11359 "`choices` is only valid on `String` fields (or `Option<String>`) — \
11360 integer-valued enumerations should be modeled with a Rust enum and \
11361 custom (de)serializer for now",
11362 ));
11363 }
11364 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11365 return Err(syn::Error::new_spanned(
11366 field,
11367 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11368 ));
11369 }
11370 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11371 if min > max {
11372 return Err(syn::Error::new_spanned(
11373 field,
11374 format!("`min` ({min}) is greater than `max` ({max})"),
11375 ));
11376 }
11377 }
11378 Ok(())
11379}
11380
11381fn optional_u32(value: Option<u32>) -> TokenStream2 {
11382 if let Some(v) = value {
11383 quote!(::core::option::Option::Some(#v))
11384 } else {
11385 quote!(::core::option::Option::None)
11386 }
11387}
11388
11389fn optional_i64(value: Option<i64>) -> TokenStream2 {
11390 if let Some(v) = value {
11391 quote!(::core::option::Option::Some(#v))
11392 } else {
11393 quote!(::core::option::Option::None)
11394 }
11395}
11396
11397fn optional_str(value: Option<&str>) -> TokenStream2 {
11398 if let Some(v) = value {
11399 quote!(::core::option::Option::Some(#v))
11400 } else {
11401 quote!(::core::option::Option::None)
11402 }
11403}
11404
11405fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11406 let Some(pairs) = pairs else {
11407 return quote!(::core::option::Option::None);
11408 };
11409 let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11410 quote!(::core::option::Option::Some(&[#(#entries),*]))
11411}
11412
11413fn relation_tokens(
11414 field: &syn::Field,
11415 attrs: &FieldAttrs,
11416 fk_inner: Option<&syn::Type>,
11417 table: &str,
11418) -> syn::Result<TokenStream2> {
11419 let root = rustango_root();
11420 if let Some(inner) = fk_inner {
11421 if attrs.fk.is_some() || attrs.o2o.is_some() {
11422 return Err(syn::Error::new_spanned(
11423 field,
11424 "`ForeignKey<T>` already declares the FK target via the type parameter — \
11425 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11426 ));
11427 }
11428 let on = attrs.on.as_deref().unwrap_or("id");
11429 return Ok(quote! {
11430 ::core::option::Option::Some(#root::core::Relation::Fk {
11431 to: <#inner as #root::core::Model>::SCHEMA.table,
11432 on: #on,
11433 })
11434 });
11435 }
11436 match (&attrs.fk, &attrs.o2o) {
11437 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11438 field,
11439 "`fk` and `o2o` are mutually exclusive",
11440 )),
11441 (Some(to), None) => {
11442 let on = attrs.on.as_deref().unwrap_or("id");
11443 // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11444 // the model's own table. Threaded as a literal string at
11445 // macro-expansion time to sidestep the const-eval cycle
11446 // that `Self::SCHEMA.table` would create when referenced
11447 // inside Self::SCHEMA's own initializer.
11448 let resolved = if to == "self" { table } else { to };
11449 Ok(quote! {
11450 ::core::option::Option::Some(#root::core::Relation::Fk { to: #resolved, on: #on })
11451 })
11452 }
11453 (None, Some(to)) => {
11454 let on = attrs.on.as_deref().unwrap_or("id");
11455 let resolved = if to == "self" { table } else { to };
11456 Ok(quote! {
11457 ::core::option::Option::Some(#root::core::Relation::O2O { to: #resolved, on: #on })
11458 })
11459 }
11460 (None, None) => {
11461 if attrs.on.is_some() {
11462 return Err(syn::Error::new_spanned(
11463 field,
11464 "`on` requires `fk` or `o2o`",
11465 ));
11466 }
11467 Ok(quote!(::core::option::Option::None))
11468 }
11469 }
11470}
11471
11472/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11473/// about kinds without depending on `rustango-core` (which would require a
11474/// proc-macro/normal split it doesn't have today).
11475#[derive(Clone, Copy, PartialEq, Eq)]
11476enum DetectedKind {
11477 I16,
11478 I32,
11479 I64,
11480 F32,
11481 F64,
11482 Bool,
11483 String,
11484 DateTime,
11485 Date,
11486 Time,
11487 Uuid,
11488 Json,
11489 Decimal,
11490 Binary,
11491 /// `Array<String>` → PG `text[]` (#341).
11492 ArrayText,
11493 /// `Array<i32>` → PG `integer[]` (#341).
11494 ArrayInt,
11495 /// `Array<i64>` → PG `bigint[]` (#341).
11496 ArrayBigInt,
11497 /// `Range<i32>` → PG `int4range` (#343).
11498 RangeInt,
11499 /// `Range<i64>` → PG `int8range` (#343).
11500 RangeBigInt,
11501 /// `Range<Decimal>` → PG `numrange` (#343).
11502 RangeNumeric,
11503 /// `Range<NaiveDate>` → PG `daterange` (#343).
11504 RangeDate,
11505 /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11506 RangeDateTime,
11507 /// `HStore` → PG `hstore` (#342).
11508 HStore,
11509 /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11510 /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11511 /// in at the `FieldType` emission site (not carried on this enum).
11512 Vector,
11513 /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11514 /// from the `#[rustango(geometry(srid = N))]` field attribute,
11515 /// threaded in at the `FieldType` emission site.
11516 Geometry,
11517}
11518
11519impl DetectedKind {
11520 fn variant_tokens(self) -> TokenStream2 {
11521 let root = rustango_root();
11522 match self {
11523 Self::I16 => quote!(#root::core::FieldType::I16),
11524 Self::I32 => quote!(#root::core::FieldType::I32),
11525 Self::I64 => quote!(#root::core::FieldType::I64),
11526 Self::F32 => quote!(#root::core::FieldType::F32),
11527 Self::F64 => quote!(#root::core::FieldType::F64),
11528 Self::Bool => quote!(#root::core::FieldType::Bool),
11529 Self::String => quote!(#root::core::FieldType::String),
11530 Self::DateTime => quote!(#root::core::FieldType::DateTime),
11531 Self::Date => quote!(#root::core::FieldType::Date),
11532 Self::Time => quote!(#root::core::FieldType::Time),
11533 Self::Uuid => quote!(#root::core::FieldType::Uuid),
11534 Self::Json => quote!(#root::core::FieldType::Json),
11535 Self::Decimal => quote!(#root::core::FieldType::Decimal),
11536 Self::Binary => quote!(#root::core::FieldType::Binary),
11537 Self::ArrayText => {
11538 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11539 }
11540 Self::ArrayInt => {
11541 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11542 }
11543 Self::ArrayBigInt => {
11544 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11545 }
11546 Self::RangeInt => {
11547 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11548 }
11549 Self::RangeBigInt => {
11550 quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11551 }
11552 Self::RangeNumeric => {
11553 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11554 }
11555 Self::RangeDate => {
11556 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11557 }
11558 Self::RangeDateTime => {
11559 quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11560 }
11561 Self::HStore => quote!(#root::core::FieldType::HStore),
11562 // Dimension comes from the `vector(dims = N)` attribute,
11563 // applied at the emission site; `0` here is just a fallback.
11564 Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11565 // SRID comes from the `geometry(srid = N)` attribute, applied
11566 // at the emission site; `0` here is just a fallback.
11567 Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11568 }
11569 }
11570
11571 fn is_integer(self) -> bool {
11572 matches!(self, Self::I16 | Self::I32 | Self::I64)
11573 }
11574
11575 /// `(SqlValue::<Variant>, default expr)` for emitting the
11576 /// `match SqlValue { … }` arm in `LoadRelated::__rustango_load_related`
11577 /// for a `ForeignKey<T, K>` FK whose K maps to `self`. The default
11578 /// fires only when the parent's `__rustango_pk_value` returns a
11579 /// different variant than expected, which is a compile-time bug —
11580 /// but we still need a value-typed fallback to keep the match
11581 /// total.
11582 fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
11583 let root = rustango_root();
11584 match self {
11585 Self::I16 => (quote!(I16), quote!(0i16)),
11586 Self::I32 => (quote!(I32), quote!(0i32)),
11587 Self::I64 => (quote!(I64), quote!(0i64)),
11588 Self::F32 => (quote!(F32), quote!(0f32)),
11589 Self::F64 => (quote!(F64), quote!(0f64)),
11590 Self::Bool => (quote!(Bool), quote!(false)),
11591 Self::String => (quote!(String), quote!(::std::string::String::new())),
11592 Self::DateTime => (
11593 quote!(DateTime),
11594 quote!(<#root::__chrono::DateTime<#root::__chrono::Utc> as ::std::default::Default>::default()),
11595 ),
11596 Self::Date => (
11597 quote!(Date),
11598 quote!(<#root::__chrono::NaiveDate as ::std::default::Default>::default()),
11599 ),
11600 Self::Time => (
11601 quote!(Time),
11602 quote!(<#root::__chrono::NaiveTime as ::std::default::Default>::default()),
11603 ),
11604 Self::Uuid => (quote!(Uuid), quote!(#root::__uuid::Uuid::nil())),
11605 Self::Json => (quote!(Json), quote!(#root::__serde_json::Value::Null)),
11606 Self::Decimal => (
11607 quote!(Decimal),
11608 quote!(<#root::__rust_decimal::Decimal as ::std::default::Default>::default()),
11609 ),
11610 Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
11611 // Arrays (#341) can never be a foreign-key primary key, so
11612 // this arm is never reached at runtime — but the match must
11613 // stay total. A bare empty `SqlValue::Array` is the dummy.
11614 Self::ArrayText | Self::ArrayInt | Self::ArrayBigInt => {
11615 (quote!(Array), quote!(::std::vec::Vec::new()))
11616 }
11617 // Ranges (#343) likewise can't be a FK PK — never reached.
11618 Self::RangeInt
11619 | Self::RangeBigInt
11620 | Self::RangeNumeric
11621 | Self::RangeDate
11622 | Self::RangeDateTime => (quote!(RangeLiteral), quote!(::std::string::String::new())),
11623 // HStore (#342) can't be a FK PK — never reached.
11624 Self::HStore => (quote!(HStore), quote!(::std::vec::Vec::new())),
11625 // Vector (#824) can't be a FK PK — never reached.
11626 Self::Vector => (quote!(Vector), quote!(::std::vec::Vec::new())),
11627 // Geometry (#443) can't be a FK PK — never reached (arm is
11628 // exhaustiveness-only; never interpolated into emitted code).
11629 Self::Geometry => (quote!(Geometry), quote!(::std::vec::Vec::new())),
11630 }
11631 }
11632}
11633
11634/// Result of walking a field's Rust type. `kind` is the underlying
11635/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11636/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11637/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11638/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11639#[derive(Clone, Copy)]
11640struct DetectedType<'a> {
11641 kind: DetectedKind,
11642 nullable: bool,
11643 auto: bool,
11644 fk_inner: Option<&'a syn::Type>,
11645}
11646
11647/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11648/// non-`Auto` types — the caller should already have routed Auto-only
11649/// codegen through this helper, so a `None` indicates a macro-internal
11650/// invariant break.
11651fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11652 let Type::Path(TypePath { path, qself: None }) = ty else {
11653 return None;
11654 };
11655 let last = path.segments.last()?;
11656 if last.ident != "Auto" {
11657 return None;
11658 }
11659 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11660 return None;
11661 };
11662 args.args.iter().find_map(|a| match a {
11663 syn::GenericArgument::Type(t) => Some(t),
11664 _ => None,
11665 })
11666}
11667
11668fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11669 let Type::Path(TypePath { path, qself: None }) = ty else {
11670 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11671 };
11672 let last = path
11673 .segments
11674 .last()
11675 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11676
11677 if last.ident == "Option" {
11678 let inner = generic_inner(ty, &last.arguments, "Option")?;
11679 let inner_det = detect_type(inner)?;
11680 if inner_det.nullable {
11681 return Err(syn::Error::new_spanned(
11682 ty,
11683 "nested Option is not supported",
11684 ));
11685 }
11686 if inner_det.auto {
11687 return Err(syn::Error::new_spanned(
11688 ty,
11689 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11690 ));
11691 }
11692 return Ok(DetectedType {
11693 nullable: true,
11694 ..inner_det
11695 });
11696 }
11697
11698 if last.ident == "Auto" {
11699 let inner = generic_inner(ty, &last.arguments, "Auto")?;
11700 let inner_det = detect_type(inner)?;
11701 if inner_det.auto {
11702 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11703 }
11704 if inner_det.nullable {
11705 return Err(syn::Error::new_spanned(
11706 ty,
11707 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11708 ));
11709 }
11710 if inner_det.fk_inner.is_some() {
11711 return Err(syn::Error::new_spanned(
11712 ty,
11713 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11714 ));
11715 }
11716 if !matches!(
11717 inner_det.kind,
11718 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11719 ) {
11720 return Err(syn::Error::new_spanned(
11721 ty,
11722 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11723 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11724 (DEFAULT now())",
11725 ));
11726 }
11727 return Ok(DetectedType {
11728 auto: true,
11729 ..inner_det
11730 });
11731 }
11732
11733 if last.ident == "ForeignKey" {
11734 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11735 // Resolve the FK column's underlying SQL type from `K`. When the
11736 // user wrote `ForeignKey<T>` without a key parameter, the type
11737 // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11738 // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11739 // recurse into K so the column DDL emits the right SQL type
11740 // (VARCHAR for String, UUID for Uuid, …) and the load_related
11741 // emitter knows which `SqlValue` variant to match.
11742 let kind = match key_ty {
11743 Some(k) => detect_type(k)?.kind,
11744 None => DetectedKind::I64,
11745 };
11746 return Ok(DetectedType {
11747 kind,
11748 nullable: false,
11749 auto: false,
11750 fk_inner: Some(inner),
11751 });
11752 }
11753
11754 let kind = match last.ident.to_string().as_str() {
11755 "i16" => DetectedKind::I16,
11756 "i32" => DetectedKind::I32,
11757 "i64" => DetectedKind::I64,
11758 "f32" => DetectedKind::F32,
11759 "f64" => DetectedKind::F64,
11760 "bool" => DetectedKind::Bool,
11761 "String" => DetectedKind::String,
11762 "DateTime" => DetectedKind::DateTime,
11763 "NaiveDate" => DetectedKind::Date,
11764 "NaiveTime" => DetectedKind::Time,
11765 "Uuid" => DetectedKind::Uuid,
11766 "Value" => DetectedKind::Json,
11767 "Decimal" => DetectedKind::Decimal,
11768 // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11769 // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11770 // — that would emit Binary DDL and decode-fail at runtime.
11771 "Vec" => {
11772 let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11773 if let Type::Path(TypePath { path, qself: None }) = inner {
11774 if let Some(seg) = path.segments.last() {
11775 if seg.ident == "u8" && seg.arguments.is_empty() {
11776 return Ok(DetectedType {
11777 kind: DetectedKind::Binary,
11778 nullable: false,
11779 auto: false,
11780 fk_inner: None,
11781 });
11782 }
11783 }
11784 }
11785 return Err(syn::Error::new_spanned(
11786 ty,
11787 "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11788 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11789 ));
11790 }
11791 // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11792 // `integer[]` / `bigint[]` (Django `ArrayField`, #341).
11793 "Array" => {
11794 let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11795 let elem = match inner {
11796 Type::Path(TypePath { path, qself: None }) => {
11797 path.segments.last().map(|s| s.ident.to_string())
11798 }
11799 _ => None,
11800 };
11801 let kind = match elem.as_deref() {
11802 Some("String") => DetectedKind::ArrayText,
11803 Some("i32") => DetectedKind::ArrayInt,
11804 Some("i64") => DetectedKind::ArrayBigInt,
11805 _ => {
11806 return Err(syn::Error::new_spanned(
11807 ty,
11808 "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
11809 `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
11810 ));
11811 }
11812 };
11813 return Ok(DetectedType {
11814 kind,
11815 nullable: false,
11816 auto: false,
11817 fk_inner: None,
11818 });
11819 }
11820 // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
11821 // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
11822 // `int8range` / `numrange` / `daterange` / `tstzrange` (Django
11823 // `RangeField` family, #343).
11824 "Range" => {
11825 let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
11826 let elem = match inner {
11827 Type::Path(TypePath { path, qself: None }) => {
11828 path.segments.last().map(|s| s.ident.to_string())
11829 }
11830 _ => None,
11831 };
11832 let kind = match elem.as_deref() {
11833 Some("i32") => DetectedKind::RangeInt,
11834 Some("i64") => DetectedKind::RangeBigInt,
11835 Some("Decimal") => DetectedKind::RangeNumeric,
11836 Some("NaiveDate") => DetectedKind::RangeDate,
11837 Some("DateTime") => DetectedKind::RangeDateTime,
11838 _ => {
11839 return Err(syn::Error::new_spanned(
11840 ty,
11841 "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
11842 `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
11843 `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
11844 (→ tstzrange) are supported (#343)",
11845 ));
11846 }
11847 };
11848 return Ok(DetectedType {
11849 kind,
11850 nullable: false,
11851 auto: false,
11852 fk_inner: None,
11853 });
11854 }
11855 // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
11856 // (the `CastValue` impl bridges logical↔stored); the field's own
11857 // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
11858 // schema just needs `FieldType::String`.
11859 "Cast" => {
11860 return Ok(DetectedType {
11861 kind: DetectedKind::String,
11862 nullable: false,
11863 auto: false,
11864 fk_inner: None,
11865 });
11866 }
11867 // `HStore` → PG `hstore` (Django `HStoreField`, #342). No generic
11868 // parameter — always a string→string map.
11869 "HStore" => {
11870 return Ok(DetectedType {
11871 kind: DetectedKind::HStore,
11872 nullable: false,
11873 auto: false,
11874 fk_inner: None,
11875 });
11876 }
11877 // `Vector` → pgvector `vector(N)` (#824). The dimension is
11878 // supplied by `#[rustango(vector(dims = N))]`, not the type.
11879 "Vector" => {
11880 return Ok(DetectedType {
11881 kind: DetectedKind::Vector,
11882 nullable: false,
11883 auto: false,
11884 fk_inner: None,
11885 });
11886 }
11887 // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
11888 // supplied by `#[rustango(geometry(srid = N))]`, not the type.
11889 "Point" => {
11890 return Ok(DetectedType {
11891 kind: DetectedKind::Geometry,
11892 nullable: false,
11893 auto: false,
11894 fk_inner: None,
11895 });
11896 }
11897 other => {
11898 return Err(syn::Error::new_spanned(
11899 ty,
11900 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)"),
11901 ));
11902 }
11903 };
11904 Ok(DetectedType {
11905 kind,
11906 nullable: false,
11907 auto: false,
11908 fk_inner: None,
11909 })
11910}
11911
11912fn generic_inner<'a>(
11913 ty: &'a Type,
11914 arguments: &'a PathArguments,
11915 wrapper: &str,
11916) -> syn::Result<&'a Type> {
11917 let PathArguments::AngleBracketed(args) = arguments else {
11918 return Err(syn::Error::new_spanned(
11919 ty,
11920 format!("{wrapper} requires a generic argument"),
11921 ));
11922 };
11923 args.args
11924 .iter()
11925 .find_map(|a| match a {
11926 GenericArgument::Type(t) => Some(t),
11927 _ => None,
11928 })
11929 .ok_or_else(|| {
11930 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11931 })
11932}
11933
11934/// Like [`generic_inner`] but pulls *two* type args — the first is
11935/// required, the second is optional. Used by the `ForeignKey<T, K>`
11936/// detection where K defaults to `i64` when omitted.
11937fn generic_pair<'a>(
11938 ty: &'a Type,
11939 arguments: &'a PathArguments,
11940 wrapper: &str,
11941) -> syn::Result<(&'a Type, Option<&'a Type>)> {
11942 let PathArguments::AngleBracketed(args) = arguments else {
11943 return Err(syn::Error::new_spanned(
11944 ty,
11945 format!("{wrapper} requires a generic argument"),
11946 ));
11947 };
11948 let mut types = args.args.iter().filter_map(|a| match a {
11949 GenericArgument::Type(t) => Some(t),
11950 _ => None,
11951 });
11952 let first = types.next().ok_or_else(|| {
11953 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11954 })?;
11955 let second = types.next();
11956 Ok((first, second))
11957}
11958
11959fn to_snake_case(s: &str) -> String {
11960 let mut out = String::with_capacity(s.len() + 4);
11961 for (i, ch) in s.chars().enumerate() {
11962 if ch.is_ascii_uppercase() {
11963 if i > 0 {
11964 out.push('_');
11965 }
11966 out.push(ch.to_ascii_lowercase());
11967 } else {
11968 out.push(ch);
11969 }
11970 }
11971 out
11972}
11973
11974// ============================================================
11975// #[derive(Form)] — slice 8.4B
11976// ============================================================
11977
11978/// Per-field `#[form(...)]` attributes recognised by the derive.
11979#[derive(Default)]
11980struct FormFieldAttrs {
11981 min: Option<i64>,
11982 max: Option<i64>,
11983 min_length: Option<u32>,
11984 max_length: Option<u32>,
11985 /// `#[form(clean = "fn_name")]` — Django-shape `clean_<field>` hook.
11986 /// The named static method on the form struct is called after the
11987 /// field's typed parse + length/range checks; it gets the parsed
11988 /// value by reference and returns `Result<<FieldType>, String>`.
11989 /// On Ok, the returned value replaces the parsed one; on Err, the
11990 /// message is attached to the field error list. Issue #372.
11991 clean: Option<syn::Ident>,
11992}
11993
11994/// Container-level `#[form(...)]` attributes. Currently only the
11995/// Django-shape cross-field `validate` hook (issue #373).
11996#[derive(Default)]
11997struct FormContainerAttrs {
11998 /// `#[form(validate = "fn_name")]` — Django-shape `clean()` hook.
11999 /// After every per-field parse succeeds, the named method on the
12000 /// form struct is called with `&self` and may return
12001 /// `Result<(), FormErrors>`. Errors merge into the field error
12002 /// list. Issue #373.
12003 validate: Option<syn::Ident>,
12004}
12005
12006/// Detected shape of a form field's Rust type.
12007#[derive(Clone, Copy)]
12008enum FormFieldKind {
12009 String,
12010 I16,
12011 I32,
12012 I64,
12013 F32,
12014 F64,
12015 Bool,
12016}
12017
12018impl FormFieldKind {
12019 fn parse_method(self) -> &'static str {
12020 match self {
12021 Self::I16 => "i16",
12022 Self::I32 => "i32",
12023 Self::I64 => "i64",
12024 Self::F32 => "f32",
12025 Self::F64 => "f64",
12026 // String + Bool don't go through `str::parse`; the codegen
12027 // handles them inline.
12028 Self::String | Self::Bool => "",
12029 }
12030 }
12031}
12032
12033fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
12034 let root = rustango_root();
12035 let struct_name = &input.ident;
12036
12037 let Data::Struct(data) = &input.data else {
12038 return Err(syn::Error::new_spanned(
12039 struct_name,
12040 "Form can only be derived on structs",
12041 ));
12042 };
12043 let Fields::Named(named) = &data.fields else {
12044 return Err(syn::Error::new_spanned(
12045 struct_name,
12046 "Form requires a struct with named fields",
12047 ));
12048 };
12049
12050 // #373 — container-level `#[form(validate = "fn")]` hook.
12051 let container = parse_form_container_attrs(input)?;
12052 let post_field_clean: Vec<TokenStream2> = Vec::new();
12053 let _ = post_field_clean;
12054
12055 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12056 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12057
12058 for field in &named.named {
12059 let ident = field
12060 .ident
12061 .as_ref()
12062 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12063 let attrs = parse_form_field_attrs(field)?;
12064 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12065
12066 let name_lit = ident.to_string();
12067 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12068 // #372 — append the per-field `clean_<field>` call right after
12069 // the parse block when the attribute is set. The clean fn
12070 // takes &T and returns Result<T, String>; on Err we attach
12071 // the message to the field error list without aborting
12072 // (matches Django's "collect all field errors" shape).
12073 let clean_block = if let Some(clean_fn) = &attrs.clean {
12074 quote! {
12075 if __errors.fields().get(#name_lit).is_none() {
12076 match Self::#clean_fn(&#ident) {
12077 ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
12078 ::core::result::Result::Err(__msg) => {
12079 __errors.add(#name_lit, __msg);
12080 }
12081 }
12082 }
12083 }
12084 } else {
12085 quote! {}
12086 };
12087 field_blocks.push(quote! {
12088 #parse_block
12089 #clean_block
12090 });
12091 field_idents.push(ident);
12092 }
12093
12094 // #373 — after every per-field parse + clean succeeds, call the
12095 // cross-field validator if declared. Errors merge into the
12096 // outgoing FormErrors via the existing `FormErrors::merge` helper
12097 // (same primitive the DRF serializer cross-field hook uses).
12098 let cross_field_call = if let Some(validate_fn) = &container.validate {
12099 quote! {
12100 if __errors.is_empty() {
12101 let __candidate = Self { #( #field_idents ),* };
12102 if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12103 __errors.merge(__other);
12104 }
12105 if !__errors.is_empty() {
12106 return ::core::result::Result::Err(__errors);
12107 }
12108 return ::core::result::Result::Ok(__candidate);
12109 }
12110 }
12111 } else {
12112 quote! {}
12113 };
12114
12115 Ok(quote! {
12116 impl #root::forms::Form for #struct_name {
12117 fn parse(
12118 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12119 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12120 let mut __errors = #root::forms::FormErrors::default();
12121 #( #field_blocks )*
12122 #cross_field_call
12123 if !__errors.is_empty() {
12124 return ::core::result::Result::Err(__errors);
12125 }
12126 ::core::result::Result::Ok(Self {
12127 #( #field_idents ),*
12128 })
12129 }
12130 }
12131 })
12132}
12133
12134fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12135 let mut out = FormContainerAttrs::default();
12136 for attr in &input.attrs {
12137 if !attr.path().is_ident("form") {
12138 continue;
12139 }
12140 attr.parse_nested_meta(|meta| {
12141 if meta.path.is_ident("validate") {
12142 let s: LitStr = meta.value()?.parse()?;
12143 out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12144 return Ok(());
12145 }
12146 Err(meta.error("unknown form container attribute (supported: `validate`)"))
12147 })?;
12148 }
12149 Ok(out)
12150}
12151
12152fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12153 let mut out = FormFieldAttrs::default();
12154 for attr in &field.attrs {
12155 if !attr.path().is_ident("form") {
12156 continue;
12157 }
12158 attr.parse_nested_meta(|meta| {
12159 if meta.path.is_ident("min") {
12160 let lit: syn::LitInt = meta.value()?.parse()?;
12161 out.min = Some(lit.base10_parse::<i64>()?);
12162 return Ok(());
12163 }
12164 if meta.path.is_ident("max") {
12165 let lit: syn::LitInt = meta.value()?.parse()?;
12166 out.max = Some(lit.base10_parse::<i64>()?);
12167 return Ok(());
12168 }
12169 if meta.path.is_ident("min_length") {
12170 let lit: syn::LitInt = meta.value()?.parse()?;
12171 out.min_length = Some(lit.base10_parse::<u32>()?);
12172 return Ok(());
12173 }
12174 if meta.path.is_ident("max_length") {
12175 let lit: syn::LitInt = meta.value()?.parse()?;
12176 out.max_length = Some(lit.base10_parse::<u32>()?);
12177 return Ok(());
12178 }
12179 if meta.path.is_ident("clean") {
12180 let s: LitStr = meta.value()?.parse()?;
12181 out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12182 return Ok(());
12183 }
12184 Err(meta.error(
12185 "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12186 ))
12187 })?;
12188 }
12189 Ok(out)
12190}
12191
12192fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12193 let Type::Path(TypePath { path, qself: None }) = ty else {
12194 return Err(syn::Error::new(
12195 span,
12196 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12197 ));
12198 };
12199 let last = path
12200 .segments
12201 .last()
12202 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12203
12204 if last.ident == "Option" {
12205 let inner = generic_inner(ty, &last.arguments, "Option")?;
12206 let (kind, nested) = detect_form_field(inner, span)?;
12207 if nested {
12208 return Err(syn::Error::new(
12209 span,
12210 "nested Option in Form fields is not supported",
12211 ));
12212 }
12213 return Ok((kind, true));
12214 }
12215
12216 let kind = match last.ident.to_string().as_str() {
12217 "String" => FormFieldKind::String,
12218 "i16" => FormFieldKind::I16,
12219 "i32" => FormFieldKind::I32,
12220 "i64" => FormFieldKind::I64,
12221 "f32" => FormFieldKind::F32,
12222 "f64" => FormFieldKind::F64,
12223 "bool" => FormFieldKind::Bool,
12224 other => {
12225 return Err(syn::Error::new(
12226 span,
12227 format!(
12228 "Form field type `{other}` is not supported in v0.8 — use String / \
12229 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12230 ),
12231 ));
12232 }
12233 };
12234 Ok((kind, false))
12235}
12236
12237#[allow(clippy::too_many_lines)]
12238fn render_form_field_parse(
12239 ident: &syn::Ident,
12240 name_lit: &str,
12241 kind: FormFieldKind,
12242 nullable: bool,
12243 attrs: &FormFieldAttrs,
12244) -> TokenStream2 {
12245 // Pull the raw &str from the payload. Uses variable name `data` to
12246 // match the new `Form::parse(data: &HashMap<…>)` signature.
12247 let lookup = quote! {
12248 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12249 };
12250
12251 let parsed_value = match kind {
12252 FormFieldKind::Bool => quote! {
12253 let __v: bool = match __raw {
12254 ::core::option::Option::None => false,
12255 ::core::option::Option::Some(__s) => !matches!(
12256 __s.to_ascii_lowercase().as_str(),
12257 "" | "false" | "0" | "off" | "no"
12258 ),
12259 };
12260 },
12261 FormFieldKind::String => {
12262 if nullable {
12263 quote! {
12264 let __v: ::core::option::Option<::std::string::String> = match __raw {
12265 ::core::option::Option::None => ::core::option::Option::None,
12266 ::core::option::Option::Some(__s) if __s.is_empty() => {
12267 ::core::option::Option::None
12268 }
12269 ::core::option::Option::Some(__s) => {
12270 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12271 }
12272 };
12273 }
12274 } else {
12275 quote! {
12276 let __v: ::std::string::String = match __raw {
12277 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12278 ::core::clone::Clone::clone(__s)
12279 }
12280 _ => {
12281 __errors.add(#name_lit, "This field is required.");
12282 ::std::string::String::new()
12283 }
12284 };
12285 }
12286 }
12287 }
12288 FormFieldKind::I16
12289 | FormFieldKind::I32
12290 | FormFieldKind::I64
12291 | FormFieldKind::F32
12292 | FormFieldKind::F64 => {
12293 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12294 let ty_lit = kind.parse_method();
12295 let default_val = match kind {
12296 FormFieldKind::I16 => quote! { 0i16 },
12297 FormFieldKind::I32 => quote! { 0i32 },
12298 FormFieldKind::I64 => quote! { 0i64 },
12299 FormFieldKind::F32 => quote! { 0f32 },
12300 FormFieldKind::F64 => quote! { 0f64 },
12301 _ => quote! { Default::default() },
12302 };
12303 if nullable {
12304 quote! {
12305 let __v: ::core::option::Option<#parse_ty> = match __raw {
12306 ::core::option::Option::None => ::core::option::Option::None,
12307 ::core::option::Option::Some(__s) if __s.is_empty() => {
12308 ::core::option::Option::None
12309 }
12310 ::core::option::Option::Some(__s) => {
12311 match __s.parse::<#parse_ty>() {
12312 ::core::result::Result::Ok(__n) => {
12313 ::core::option::Option::Some(__n)
12314 }
12315 ::core::result::Result::Err(__e) => {
12316 __errors.add(
12317 #name_lit,
12318 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12319 );
12320 ::core::option::Option::None
12321 }
12322 }
12323 }
12324 };
12325 }
12326 } else {
12327 quote! {
12328 let __v: #parse_ty = match __raw {
12329 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12330 match __s.parse::<#parse_ty>() {
12331 ::core::result::Result::Ok(__n) => __n,
12332 ::core::result::Result::Err(__e) => {
12333 __errors.add(
12334 #name_lit,
12335 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12336 );
12337 #default_val
12338 }
12339 }
12340 }
12341 _ => {
12342 __errors.add(#name_lit, "This field is required.");
12343 #default_val
12344 }
12345 };
12346 }
12347 }
12348 }
12349 };
12350
12351 let validators = render_form_validators(name_lit, kind, nullable, attrs);
12352
12353 quote! {
12354 // `mut` so the per-field `clean` hook (#372) can rewrite the
12355 // parsed value in-place when it returns Ok with a normalized
12356 // form (e.g. trim / lowercase).
12357 let mut #ident = {
12358 #lookup
12359 #parsed_value
12360 #validators
12361 __v
12362 };
12363 }
12364}
12365
12366fn render_form_validators(
12367 name_lit: &str,
12368 kind: FormFieldKind,
12369 nullable: bool,
12370 attrs: &FormFieldAttrs,
12371) -> TokenStream2 {
12372 let mut checks: Vec<TokenStream2> = Vec::new();
12373
12374 let val_ref = if nullable {
12375 quote! { __v.as_ref() }
12376 } else {
12377 quote! { ::core::option::Option::Some(&__v) }
12378 };
12379
12380 let is_string = matches!(kind, FormFieldKind::String);
12381 let is_numeric = matches!(
12382 kind,
12383 FormFieldKind::I16
12384 | FormFieldKind::I32
12385 | FormFieldKind::I64
12386 | FormFieldKind::F32
12387 | FormFieldKind::F64
12388 );
12389
12390 if is_string {
12391 if let Some(min_len) = attrs.min_length {
12392 let min_len_usize = min_len as usize;
12393 checks.push(quote! {
12394 if let ::core::option::Option::Some(__s) = #val_ref {
12395 if __s.len() < #min_len_usize {
12396 __errors.add(
12397 #name_lit,
12398 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12399 );
12400 }
12401 }
12402 });
12403 }
12404 if let Some(max_len) = attrs.max_length {
12405 let max_len_usize = max_len as usize;
12406 checks.push(quote! {
12407 if let ::core::option::Option::Some(__s) = #val_ref {
12408 if __s.len() > #max_len_usize {
12409 __errors.add(
12410 #name_lit,
12411 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12412 );
12413 }
12414 }
12415 });
12416 }
12417 }
12418
12419 if is_numeric {
12420 if let Some(min) = attrs.min {
12421 checks.push(quote! {
12422 if let ::core::option::Option::Some(__n) = #val_ref {
12423 if (*__n as f64) < (#min as f64) {
12424 __errors.add(
12425 #name_lit,
12426 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12427 );
12428 }
12429 }
12430 });
12431 }
12432 if let Some(max) = attrs.max {
12433 checks.push(quote! {
12434 if let ::core::option::Option::Some(__n) = #val_ref {
12435 if (*__n as f64) > (#max as f64) {
12436 __errors.add(
12437 #name_lit,
12438 ::std::format!("Ensure this value is less than or equal to {}.", #max),
12439 );
12440 }
12441 }
12442 });
12443 }
12444 }
12445
12446 quote! { #( #checks )* }
12447}
12448
12449// ============================================================
12450// #[derive(ViewSet)]
12451// ============================================================
12452
12453struct ViewSetAttrs {
12454 model: syn::Path,
12455 fields: Option<Vec<String>>,
12456 filter_fields: Vec<String>,
12457 search_fields: Vec<String>,
12458 /// (field_name, desc)
12459 ordering: Vec<(String, bool)>,
12460 page_size: Option<usize>,
12461 read_only: bool,
12462 perms: ViewSetPermsAttrs,
12463 /// `#[viewset(serializer = SomeSerializer)]` — render list /
12464 /// retrieve / create responses through this `#[derive(Serializer)]`
12465 /// type instead of the default field-level projection (requires the
12466 /// `serializer` feature). Tri-dialect.
12467 serializer: Option<syn::Path>,
12468}
12469
12470#[derive(Default)]
12471struct ViewSetPermsAttrs {
12472 list: Vec<String>,
12473 retrieve: Vec<String>,
12474 create: Vec<String>,
12475 update: Vec<String>,
12476 destroy: Vec<String>,
12477}
12478
12479fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12480 let root = rustango_root();
12481 let struct_name = &input.ident;
12482
12483 // Must be a unit struct or an empty named struct.
12484 match &input.data {
12485 Data::Struct(s) => match &s.fields {
12486 Fields::Unit | Fields::Named(_) => {}
12487 Fields::Unnamed(_) => {
12488 return Err(syn::Error::new_spanned(
12489 struct_name,
12490 "ViewSet can only be derived on a unit struct or an empty named struct",
12491 ));
12492 }
12493 },
12494 _ => {
12495 return Err(syn::Error::new_spanned(
12496 struct_name,
12497 "ViewSet can only be derived on a struct",
12498 ));
12499 }
12500 }
12501
12502 let attrs = parse_viewset_attrs(input)?;
12503 let model_path = &attrs.model;
12504
12505 // `.fields(&[...])` call — None means skip (use all scalar fields).
12506 let fields_call = if let Some(ref fields) = attrs.fields {
12507 let lits = fields.iter().map(|f| f.as_str());
12508 quote!(.fields(&[ #(#lits),* ]))
12509 } else {
12510 quote!()
12511 };
12512
12513 let filter_fields_call = if attrs.filter_fields.is_empty() {
12514 quote!()
12515 } else {
12516 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12517 quote!(.filter_fields(&[ #(#lits),* ]))
12518 };
12519
12520 let search_fields_call = if attrs.search_fields.is_empty() {
12521 quote!()
12522 } else {
12523 let lits = attrs.search_fields.iter().map(|f| f.as_str());
12524 quote!(.search_fields(&[ #(#lits),* ]))
12525 };
12526
12527 let ordering_call = if attrs.ordering.is_empty() {
12528 quote!()
12529 } else {
12530 let pairs = attrs.ordering.iter().map(|(f, desc)| {
12531 let f = f.as_str();
12532 quote!((#f, #desc))
12533 });
12534 quote!(.ordering(&[ #(#pairs),* ]))
12535 };
12536
12537 let page_size_call = if let Some(n) = attrs.page_size {
12538 quote!(.page_size(#n))
12539 } else {
12540 quote!()
12541 };
12542
12543 let read_only_call = if attrs.read_only {
12544 quote!(.read_only())
12545 } else {
12546 quote!()
12547 };
12548
12549 // `.serializer::<S>()` — reshape responses through a derived
12550 // serializer. Requires the downstream crate to enable the
12551 // `serializer` feature (the method is gated on it).
12552 let serializer_call = if let Some(ref ser) = attrs.serializer {
12553 quote!(.serializer::<#ser>())
12554 } else {
12555 quote!()
12556 };
12557
12558 let perms = &attrs.perms;
12559 let perms_call = if perms.list.is_empty()
12560 && perms.retrieve.is_empty()
12561 && perms.create.is_empty()
12562 && perms.update.is_empty()
12563 && perms.destroy.is_empty()
12564 {
12565 quote!()
12566 } else {
12567 let list_lits = perms.list.iter().map(|s| s.as_str());
12568 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12569 let create_lits = perms.create.iter().map(|s| s.as_str());
12570 let update_lits = perms.update.iter().map(|s| s.as_str());
12571 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12572 quote! {
12573 .permissions(#root::viewset::ViewSetPerms {
12574 list: ::std::vec![ #(#list_lits.to_owned()),* ],
12575 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12576 create: ::std::vec![ #(#create_lits.to_owned()),* ],
12577 update: ::std::vec![ #(#update_lits.to_owned()),* ],
12578 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
12579 })
12580 }
12581 };
12582
12583 Ok(quote! {
12584 impl #struct_name {
12585 /// Build an `axum::Router` with the six standard REST endpoints
12586 /// for this ViewSet, mounted at `prefix`.
12587 ///
12588 /// Accepts any backend's pool — a `sqlx::PgPool`,
12589 /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
12590 /// enum — because they all `Into<Pool>` (#1273). This is why
12591 /// the derive no longer names `PgPool`, which would not
12592 /// exist on a project built without the `postgres` feature.
12593 pub fn router(
12594 prefix: &str,
12595 pool: impl ::core::convert::Into<#root::sql::Pool>,
12596 ) -> #root::__axum::Router {
12597 #root::viewset::ViewSet::for_model(
12598 <#model_path as #root::core::Model>::SCHEMA
12599 )
12600 #fields_call
12601 #filter_fields_call
12602 #search_fields_call
12603 #ordering_call
12604 #page_size_call
12605 #perms_call
12606 #read_only_call
12607 #serializer_call
12608 .router_pool(prefix, pool.into())
12609 }
12610 }
12611 })
12612}
12613
12614fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
12615 let mut model: Option<syn::Path> = None;
12616 let mut fields: Option<Vec<String>> = None;
12617 let mut filter_fields: Vec<String> = Vec::new();
12618 let mut search_fields: Vec<String> = Vec::new();
12619 let mut ordering: Vec<(String, bool)> = Vec::new();
12620 let mut page_size: Option<usize> = None;
12621 let mut read_only = false;
12622 let mut perms = ViewSetPermsAttrs::default();
12623 let mut serializer: Option<syn::Path> = None;
12624
12625 for attr in &input.attrs {
12626 if !attr.path().is_ident("viewset") {
12627 continue;
12628 }
12629 attr.parse_nested_meta(|meta| {
12630 if meta.path.is_ident("model") {
12631 let path: syn::Path = meta.value()?.parse()?;
12632 model = Some(path);
12633 return Ok(());
12634 }
12635 if meta.path.is_ident("serializer") {
12636 let path: syn::Path = meta.value()?.parse()?;
12637 serializer = Some(path);
12638 return Ok(());
12639 }
12640 if meta.path.is_ident("fields") {
12641 let s: LitStr = meta.value()?.parse()?;
12642 fields = Some(split_field_list(&s.value()));
12643 return Ok(());
12644 }
12645 if meta.path.is_ident("filter_fields") {
12646 let s: LitStr = meta.value()?.parse()?;
12647 filter_fields = split_field_list(&s.value());
12648 return Ok(());
12649 }
12650 if meta.path.is_ident("search_fields") {
12651 let s: LitStr = meta.value()?.parse()?;
12652 search_fields = split_field_list(&s.value());
12653 return Ok(());
12654 }
12655 if meta.path.is_ident("ordering") {
12656 let s: LitStr = meta.value()?.parse()?;
12657 ordering = parse_ordering_list(&s.value());
12658 return Ok(());
12659 }
12660 if meta.path.is_ident("page_size") {
12661 let lit: syn::LitInt = meta.value()?.parse()?;
12662 page_size = Some(lit.base10_parse::<usize>()?);
12663 return Ok(());
12664 }
12665 if meta.path.is_ident("read_only") {
12666 read_only = true;
12667 return Ok(());
12668 }
12669 if meta.path.is_ident("permissions") {
12670 meta.parse_nested_meta(|inner| {
12671 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
12672 let s: LitStr = inner.value()?.parse()?;
12673 Ok(split_field_list(&s.value()))
12674 };
12675 if inner.path.is_ident("list") {
12676 perms.list = parse_codenames(&inner)?;
12677 } else if inner.path.is_ident("retrieve") {
12678 perms.retrieve = parse_codenames(&inner)?;
12679 } else if inner.path.is_ident("create") {
12680 perms.create = parse_codenames(&inner)?;
12681 } else if inner.path.is_ident("update") {
12682 perms.update = parse_codenames(&inner)?;
12683 } else if inner.path.is_ident("destroy") {
12684 perms.destroy = parse_codenames(&inner)?;
12685 } else {
12686 return Err(inner.error(
12687 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
12688 ));
12689 }
12690 Ok(())
12691 })?;
12692 return Ok(());
12693 }
12694 Err(meta.error(
12695 "unknown viewset attribute (supported: model, fields, filter_fields, \
12696 search_fields, ordering, page_size, read_only, serializer, permissions(...))",
12697 ))
12698 })?;
12699 }
12700
12701 let model = model.ok_or_else(|| {
12702 syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
12703 })?;
12704
12705 Ok(ViewSetAttrs {
12706 model,
12707 fields,
12708 filter_fields,
12709 search_fields,
12710 ordering,
12711 page_size,
12712 read_only,
12713 perms,
12714 serializer,
12715 })
12716}
12717
12718// ============================================================ #[derive(Serializer)]
12719
12720struct SerializerContainerAttrs {
12721 model: syn::Path,
12722 /// `#[serializer(validate = "fn_name")]` on the struct — DRF-shape
12723 /// cross-field validation hook (#436). The named inherent method
12724 /// must take `&self` and return
12725 /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
12726 /// `validate()` runs every per-field validator first; then calls
12727 /// the cross-field method if declared; aggregates all errors into
12728 /// one `FormErrors`.
12729 cross_validate: Option<syn::Ident>,
12730}
12731
12732#[derive(Default)]
12733struct SerializerFieldAttrs {
12734 read_only: bool,
12735 write_only: bool,
12736 source: Option<String>,
12737 skip: bool,
12738 /// `#[serializer(method = "fn_name")]` — DRF SerializerMethodField
12739 /// analog. The macro emits `from_model` initializer that calls
12740 /// `Self::fn_name(&model)` and stores the return value.
12741 method: Option<String>,
12742 /// `#[serializer(validate = "fn_name")]` — per-field validator
12743 /// callable run by `Self::validate(&self)`. Must return
12744 /// `Result<(), String>`. Errors land in `FormErrors` keyed by
12745 /// the field name.
12746 validate: Option<String>,
12747 /// `#[serializer(nested)]` on a field whose type is another
12748 /// `Serializer` — the macro emits `from_model` initializer that
12749 /// reads the parent via `model.<source>.value()` then calls the
12750 /// child serializer's `from_model(parent)`. When the FK is
12751 /// unloaded the field falls back to `Default::default()` (does
12752 /// NOT panic) so a missing prefetch in prod degrades gracefully.
12753 /// Source field on the model defaults to the field name; override
12754 /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
12755 /// panic-on-unloaded behavior for tests.
12756 nested: bool,
12757 /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
12758 /// strict behavior: panic when the FK isn't loaded. Useful in
12759 /// test code where forgetting select_related must trip a hard
12760 /// failure rather than render a blank nested object.
12761 nested_strict: bool,
12762 /// `#[serializer(many = TagSerializer)]` — declare the field as
12763 /// a list of nested serializers. Field type must be `Vec<S>`
12764 /// where `S` is the inner serializer. The macro initializes the
12765 /// field to `Vec::new()` in `from_model` and emits a typed
12766 /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
12767 /// maps each model row through `S::from_model`. Auto-load isn't
12768 /// possible (the M2M / one-to-many accessor is async); callers
12769 /// fetch the children + call the setter post-from_model.
12770 many: Option<syn::Type>,
12771 /// `#[serializer(slug = "name")]` — DRF `SlugRelatedField` analog.
12772 /// Source field on the model must be a `ForeignKey<T>`; the
12773 /// macro emits `from_model` glue that walks
12774 /// `model.<source>.value()?.<slug>` and clones it. Field type on
12775 /// the serializer is typically `String` (whatever type the slug
12776 /// column has). When the FK is unloaded the field falls back to
12777 /// `Default::default()`, same graceful-degrade contract as
12778 /// `nested`. Source defaults to the field name; override with
12779 /// `source = "..."`. v0.44.
12780 slug: Option<String>,
12781 /// `#[serializer(max_length = N)]` — DRF `MaxLengthValidator`. Caps
12782 /// the character count of a string field on write. Overrides the
12783 /// model's `max_length`; when absent the model value is inherited.
12784 max_length: Option<u64>,
12785 /// `#[serializer(min_length = N)]` — DRF `MinLengthValidator`.
12786 /// Serializer-only (the model has no `min_length` column).
12787 min_length: Option<u64>,
12788 /// `#[serializer(min = N)]` — DRF `MinValueValidator`. Inclusive
12789 /// integer lower bound; overrides the model's `min` when given.
12790 min: Option<i64>,
12791 /// `#[serializer(max = N)]` — DRF `MaxValueValidator`. Inclusive
12792 /// integer upper bound; overrides the model's `max` when given.
12793 max: Option<i64>,
12794}
12795
12796fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
12797 let mut model: Option<syn::Path> = None;
12798 let mut cross_validate: Option<syn::Ident> = None;
12799 for attr in &input.attrs {
12800 if !attr.path().is_ident("serializer") {
12801 continue;
12802 }
12803 attr.parse_nested_meta(|meta| {
12804 if meta.path.is_ident("model") {
12805 let _eq: syn::Token![=] = meta.input.parse()?;
12806 model = Some(meta.input.parse()?);
12807 return Ok(());
12808 }
12809 if meta.path.is_ident("validate") {
12810 // #436 — container-level `validate = "fn_name"` for the
12811 // DRF cross-field-validation shape. Field-level
12812 // `#[serializer(validate = "...")]` on a field is
12813 // parsed separately in `parse_serializer_field_attrs`.
12814 let s: LitStr = meta.value()?.parse()?;
12815 cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
12816 return Ok(());
12817 }
12818 Err(meta.error(
12819 "unknown serializer container attribute \
12820 (supported: `model`, `validate`)",
12821 ))
12822 })?;
12823 }
12824 let model = model.ok_or_else(|| {
12825 syn::Error::new_spanned(
12826 &input.ident,
12827 "`#[serializer(model = SomeModel)]` is required",
12828 )
12829 })?;
12830 Ok(SerializerContainerAttrs {
12831 model,
12832 cross_validate,
12833 })
12834}
12835
12836fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
12837 let mut out = SerializerFieldAttrs::default();
12838 for attr in &field.attrs {
12839 if !attr.path().is_ident("serializer") {
12840 continue;
12841 }
12842 attr.parse_nested_meta(|meta| {
12843 if meta.path.is_ident("read_only") {
12844 out.read_only = true;
12845 return Ok(());
12846 }
12847 if meta.path.is_ident("write_only") {
12848 out.write_only = true;
12849 return Ok(());
12850 }
12851 if meta.path.is_ident("skip") {
12852 out.skip = true;
12853 return Ok(());
12854 }
12855 if meta.path.is_ident("source") {
12856 let s: LitStr = meta.value()?.parse()?;
12857 out.source = Some(s.value());
12858 return Ok(());
12859 }
12860 if meta.path.is_ident("method") {
12861 let s: LitStr = meta.value()?.parse()?;
12862 out.method = Some(s.value());
12863 return Ok(());
12864 }
12865 if meta.path.is_ident("validate") {
12866 let s: LitStr = meta.value()?.parse()?;
12867 out.validate = Some(s.value());
12868 return Ok(());
12869 }
12870 if meta.path.is_ident("many") {
12871 let _eq: syn::Token![=] = meta.input.parse()?;
12872 out.many = Some(meta.input.parse()?);
12873 return Ok(());
12874 }
12875 if meta.path.is_ident("nested") {
12876 out.nested = true;
12877 // Optional strict flag inside parentheses:
12878 // #[serializer(nested(strict))]
12879 if meta.input.peek(syn::token::Paren) {
12880 meta.parse_nested_meta(|inner| {
12881 if inner.path.is_ident("strict") {
12882 out.nested_strict = true;
12883 return Ok(());
12884 }
12885 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
12886 })?;
12887 }
12888 return Ok(());
12889 }
12890 if meta.path.is_ident("slug") {
12891 let s: LitStr = meta.value()?.parse()?;
12892 out.slug = Some(s.value());
12893 return Ok(());
12894 }
12895 if meta.path.is_ident("max_length") {
12896 let lit: syn::LitInt = meta.value()?.parse()?;
12897 out.max_length = Some(lit.base10_parse::<u64>()?);
12898 return Ok(());
12899 }
12900 if meta.path.is_ident("min_length") {
12901 let lit: syn::LitInt = meta.value()?.parse()?;
12902 out.min_length = Some(lit.base10_parse::<u64>()?);
12903 return Ok(());
12904 }
12905 if meta.path.is_ident("min") {
12906 let lit: syn::LitInt = meta.value()?.parse()?;
12907 out.min = Some(lit.base10_parse::<i64>()?);
12908 return Ok(());
12909 }
12910 if meta.path.is_ident("max") {
12911 let lit: syn::LitInt = meta.value()?.parse()?;
12912 out.max = Some(lit.base10_parse::<i64>()?);
12913 return Ok(());
12914 }
12915 Err(meta.error(
12916 "unknown serializer field attribute (supported: \
12917 `read_only`, `write_only`, `source`, `skip`, `method`, \
12918 `validate`, `nested`, `many`, `slug`, `max_length`, \
12919 `min_length`, `min`, `max`)",
12920 ))
12921 })?;
12922 }
12923 // Validate: read_only + write_only is nonsensical
12924 if out.read_only && out.write_only {
12925 return Err(syn::Error::new_spanned(
12926 field,
12927 "a field cannot be both `read_only` and `write_only`",
12928 ));
12929 }
12930 if out.method.is_some() && out.source.is_some() {
12931 return Err(syn::Error::new_spanned(
12932 field,
12933 "`method` and `source` are mutually exclusive — `method` computes \
12934 the value from a method, `source` reads it from a different model field",
12935 ));
12936 }
12937 if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
12938 return Err(syn::Error::new_spanned(
12939 field,
12940 "`slug` is mutually exclusive with `method`, `nested`, and `many` \
12941 — pick one strategy for populating the field",
12942 ));
12943 }
12944 Ok(out)
12945}
12946
12947fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
12948 let root = rustango_root();
12949 let struct_name = &input.ident;
12950 let struct_name_lit = struct_name.to_string();
12951
12952 let Data::Struct(data) = &input.data else {
12953 return Err(syn::Error::new_spanned(
12954 struct_name,
12955 "Serializer can only be derived on structs",
12956 ));
12957 };
12958 let Fields::Named(named) = &data.fields else {
12959 return Err(syn::Error::new_spanned(
12960 struct_name,
12961 "Serializer requires a struct with named fields",
12962 ));
12963 };
12964
12965 let container = parse_serializer_container_attrs(input)?;
12966 let model_path = &container.model;
12967
12968 // Classify each field. `ty` is only consumed by the
12969 // `#[cfg(feature = "openapi")]` block below, but we always
12970 // capture it to keep the field-info build a single pass.
12971 #[allow(dead_code)]
12972 struct FieldInfo {
12973 ident: syn::Ident,
12974 ty: syn::Type,
12975 attrs: SerializerFieldAttrs,
12976 }
12977 let mut fields_info: Vec<FieldInfo> = Vec::new();
12978 for field in &named.named {
12979 let ident = field.ident.clone().expect("named field has ident");
12980 let attrs = parse_serializer_field_attrs(field)?;
12981 fields_info.push(FieldInfo {
12982 ident,
12983 ty: field.ty.clone(),
12984 attrs,
12985 });
12986 }
12987
12988 // Generate from_model body: struct literal with each field assigned.
12989 let from_model_fields = fields_info.iter().map(|fi| {
12990 let ident = &fi.ident;
12991 let ty = &fi.ty;
12992 if let Some(_inner) = &fi.attrs.many {
12993 // Many — collection field. Initialize empty; caller
12994 // populates via the macro-emitted set_<field> helper
12995 // after fetching the M2M children.
12996 quote! { #ident: ::std::vec::Vec::new() }
12997 } else if let Some(method) = &fi.attrs.method {
12998 // SerializerMethodField: call Self::<method>(&model) to
12999 // compute the value. Method signature must be
13000 // `fn <method>(model: &T) -> <field type>`.
13001 let method_ident = syn::Ident::new(method, ident.span());
13002 quote! { #ident: Self::#method_ident(model) }
13003 } else if let Some(slug_field) = &fi.attrs.slug {
13004 // v0.44 — SlugRelatedField. Source defaults to the field
13005 // name on this struct; override via `source = "..."`. The
13006 // source field on the model is expected to be a
13007 // `ForeignKey<T>`; the slug field on the parent is named
13008 // by the attribute value. When the FK is unloaded the
13009 // field falls back to `Default::default()` — same
13010 // graceful-degrade contract as `nested`.
13011 let src_name = fi
13012 .attrs
13013 .source
13014 .as_deref()
13015 .unwrap_or(&fi.ident.to_string())
13016 .to_owned();
13017 let src_ident = syn::Ident::new(&src_name, ident.span());
13018 let slug_ident = syn::Ident::new(slug_field, ident.span());
13019 quote! {
13020 #ident: match model.#src_ident.value() {
13021 ::core::option::Option::Some(__loaded) =>
13022 ::core::clone::Clone::clone(&__loaded.#slug_ident),
13023 ::core::option::Option::None =>
13024 ::core::default::Default::default(),
13025 }
13026 }
13027 } else if fi.attrs.nested {
13028 // Nested serializer. Source defaults to the field name on
13029 // this struct; override via `source = "..."`. The source
13030 // field on the model is expected to be a `ForeignKey<T>`
13031 // whose `.value()` returns `Option<&T>` after lazy-load.
13032 //
13033 // Behavior matrix (tweakable per-field):
13034 // * FK loaded → nested object materializes via
13035 // ChildSerializer::from_model(parent).
13036 // * FK unloaded → fall back to ChildSerializer::default()
13037 // (so prod doesn't crash on a missing
13038 // prefetch — just renders a blank nested
13039 // object). Add `#[serializer(nested,
13040 // strict)]` to keep the v0.18.1
13041 // panic-on-unloaded behavior for tests
13042 // that want hard guardrails.
13043 let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
13044 let src_ident = syn::Ident::new(&src_name, ident.span());
13045 if fi.attrs.nested_strict {
13046 let panic_msg = format!(
13047 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
13048 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
13049 );
13050 quote! {
13051 #ident: <#ty as #root::serializer::ModelSerializer>::from_model(
13052 model.#src_ident.value().expect(#panic_msg),
13053 )
13054 }
13055 } else {
13056 quote! {
13057 #ident: match model.#src_ident.value() {
13058 ::core::option::Option::Some(__loaded) =>
13059 <#ty as #root::serializer::ModelSerializer>::from_model(__loaded),
13060 ::core::option::Option::None =>
13061 ::core::default::Default::default(),
13062 }
13063 }
13064 }
13065 } else if fi.attrs.write_only || fi.attrs.skip {
13066 // Not read from model — use default
13067 quote! { #ident: ::core::default::Default::default() }
13068 } else if let Some(src) = &fi.attrs.source {
13069 let src_ident = syn::Ident::new(src, ident.span());
13070 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13071 } else {
13072 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13073 }
13074 });
13075
13076 // is_writable predicate (also used by writable_lits /
13077 // writable_source_lits below).
13078 let is_writable = |fi: &&FieldInfo| {
13079 !fi.attrs.read_only
13080 && !fi.attrs.skip
13081 && fi.attrs.method.is_none()
13082 && !fi.attrs.nested
13083 && fi.attrs.many.is_none()
13084 && fi.attrs.slug.is_none()
13085 };
13086
13087 // Declarative field constraints (DRF `validators=[...]`): one block
13088 // per writable field, run inside `validate()`. Each resolves its
13089 // bounds as the serializer attr when given (`#[serializer(max_length
13090 // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13091 // `max` / `choices`), then dispatches on the field's JSON value via
13092 // `forms::validators::check_value` (string → length/choices, integer
13093 // → min/max).
13094 let opt_usize = |v: Option<u64>| match v {
13095 Some(n) => {
13096 let n = n as usize;
13097 quote!(::core::option::Option::Some(#n))
13098 }
13099 None => quote!(::core::option::Option::None),
13100 };
13101 let opt_i64 = |v: Option<i64>| match v {
13102 Some(n) => quote!(::core::option::Option::Some(#n)),
13103 None => quote!(::core::option::Option::None),
13104 };
13105 let constraint_blocks: Vec<_> = fields_info
13106 .iter()
13107 .filter(is_writable)
13108 .map(|fi| {
13109 let ident = &fi.ident;
13110 let fname = ident.to_string();
13111 let mname = fi
13112 .attrs
13113 .source
13114 .clone()
13115 .unwrap_or_else(|| fi.ident.to_string());
13116 let attr_max_len = opt_usize(fi.attrs.max_length);
13117 let attr_min_len = opt_usize(fi.attrs.min_length);
13118 let attr_min = opt_i64(fi.attrs.min);
13119 let attr_max = opt_i64(fi.attrs.max);
13120 quote! {
13121 {
13122 let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13123 let __max_length: ::core::option::Option<usize> = #attr_max_len
13124 .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13125 let __min_length: ::core::option::Option<usize> = #attr_min_len;
13126 let __min: ::core::option::Option<i64> =
13127 #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13128 let __max: ::core::option::Option<i64> =
13129 #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13130 let __choices = __sf.and_then(|__f| __f.choices);
13131 let __v = #root::__serde_json::to_value(&self.#ident)
13132 .unwrap_or(#root::__serde_json::Value::Null);
13133 #root::forms::validators::check_value(
13134 #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13135 &mut __errors,
13136 );
13137 }
13138 }
13139 })
13140 .collect();
13141 let has_constraints = !constraint_blocks.is_empty();
13142
13143 // Per-field validators (DRF-shape `validators=[...]`). Emit a
13144 // `validate(&self)` method that runs each user-defined validator
13145 // and aggregates errors into `FormErrors`.
13146 let validator_calls: Vec<_> = fields_info
13147 .iter()
13148 .filter_map(|fi| {
13149 let ident = &fi.ident;
13150 let name_lit = ident.to_string();
13151 let method = fi.attrs.validate.as_ref()?;
13152 let method_ident = syn::Ident::new(method, ident.span());
13153 Some(quote! {
13154 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13155 __errors.add(#name_lit.to_owned(), __e);
13156 }
13157 })
13158 })
13159 .collect();
13160 // #436 — DRF cross-field `validate(self)` shape. If the
13161 // container declared `#[serializer(validate = "fn_name")]`,
13162 // the macro-generated `validate(&self)` runs every per-field
13163 // validator first, then calls the user's cross-field method,
13164 // merging its `FormErrors` into the per-field errors. Either
13165 // alone is enough to emit the wrapper.
13166 let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13167 quote! {
13168 // Merge cross-field errors into the per-field bucket so
13169 // a single .validate() call surfaces both layers.
13170 if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13171 __errors.merge(__cross);
13172 }
13173 }
13174 });
13175 let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13176 // Anything to run? Declarative constraints (for any writable field) +
13177 // per-field validators + cross-field hook.
13178 let has_run_validations = has_validators || has_constraints;
13179 // Shared body: declarative field constraints first (length / range /
13180 // choices, serializer-attr-or-model), then per-field validators, then
13181 // the cross-field hook.
13182 let validate_body = quote! {
13183 let mut __errors = #root::forms::FormErrors::default();
13184 #( #constraint_blocks )*
13185 #( #validator_calls )*
13186 #cross_validate_call
13187 if __errors.is_empty() {
13188 ::core::result::Result::Ok(())
13189 } else {
13190 ::core::result::Result::Err(__errors)
13191 }
13192 };
13193 // Inherent `validate(&self)` — kept for back-compat with direct
13194 // `serializer.validate()` calls that don't import `ModelSerializer`.
13195 // Emitted only when the serializer declares per-field/cross-field
13196 // validators (unchanged rule) so it never collides with a
13197 // hand-written inherent `validate`.
13198 let validate_method = if has_validators {
13199 quote! {
13200 impl #struct_name {
13201 /// Run the declarative field constraints, every
13202 /// `#[serializer(validate = "...")]` per-field validator,
13203 /// and (when declared) the container-level cross-field
13204 /// validator. Aggregates errors into `FormErrors` keyed by
13205 /// the field name. Returns `Ok(())` when all pass.
13206 pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13207 #validate_body
13208 }
13209 }
13210 }
13211 } else {
13212 quote! {}
13213 };
13214 // Trait-method override so the type-erased ViewSet write path can
13215 // dispatch `ModelSerializer::validate` generically. Emitted whenever
13216 // there's anything to run (declarative constraints inherited from the
13217 // model count); otherwise the trait's default no-op is used.
13218 let trait_validate_override = if has_run_validations {
13219 quote! {
13220 fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13221 #validate_body
13222 }
13223 }
13224 } else {
13225 quote! {}
13226 };
13227
13228 // For every `#[serializer(many = S)]` field, emit a
13229 // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13230 // helper that maps the parents through `S::from_model`.
13231 let many_setters: Vec<_> = fields_info
13232 .iter()
13233 .filter_map(|fi| {
13234 let many_ty = fi.attrs.many.as_ref()?;
13235 let ident = &fi.ident;
13236 let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
13237 Some(quote! {
13238 /// Populate this `many` field by mapping each parent model
13239 /// through the inner serializer's `from_model`. Call after
13240 /// fetching the M2M / one-to-many children since
13241 /// `from_model` itself can't await an SQL query.
13242 pub fn #setter(
13243 &mut self,
13244 models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13245 ) -> &mut Self {
13246 self.#ident = models.iter()
13247 .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13248 .collect();
13249 self
13250 }
13251 })
13252 })
13253 .collect();
13254 let many_setters_impl = if many_setters.is_empty() {
13255 quote! {}
13256 } else {
13257 quote! {
13258 impl #struct_name {
13259 #( #many_setters )*
13260 }
13261 }
13262 };
13263
13264 // Generate custom Serialize: skip write_only fields
13265 let output_fields: Vec<_> = fields_info
13266 .iter()
13267 .filter(|fi| !fi.attrs.write_only)
13268 .collect();
13269 let output_field_count = output_fields.len();
13270 let serialize_fields = output_fields.iter().map(|fi| {
13271 let ident = &fi.ident;
13272 let name_lit = ident.to_string();
13273 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13274 });
13275
13276 // writable_fields: normal + write_only.
13277 // Exclude:
13278 // - `read_only` — server-computed.
13279 // - `skip` — caller sets manually post-from_model.
13280 // - `method` — computed from a Self::fn(&model) call; accepting
13281 // it on write is meaningless.
13282 // - `nested` / `many` — populated from related-model data, not
13283 // from a field on the wire body.
13284 // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13285 // `many` in `writable_fields()`, which made the ViewSet write
13286 // path accept those fields from the JSON body and try to bind
13287 // them to the SQL UPDATE — a silent no-op at best, a type
13288 // mismatch at worst.
13289 // (`is_writable` is defined above, near the constraint codegen.)
13290 let writable_lits: Vec<_> = fields_info
13291 .iter()
13292 .filter(is_writable)
13293 .map(|fi| fi.ident.to_string())
13294 .collect();
13295
13296 // `writable_source_fields`: the MODEL field names of writable
13297 // serializer fields (source-resolved). The ViewSet write path skips
13298 // every model column NOT in this set, so `read_only` / `method` /
13299 // computed fields a client posts are ignored instead of written.
13300 let writable_source_lits: Vec<String> = fields_info
13301 .iter()
13302 .filter(is_writable)
13303 .map(|fi| {
13304 fi.attrs
13305 .source
13306 .clone()
13307 .unwrap_or_else(|| fi.ident.to_string())
13308 })
13309 .collect();
13310
13311 // `from_writable_json`: build a partial instance for input
13312 // validation. Writable fields are parsed from the JSON body (keyed
13313 // by serializer field name); every other field defaults. Per-field
13314 // type errors collect into `FormErrors` keyed by the field name.
13315 // Construction is field-by-field (not `Self::default()`) so it works
13316 // for serializers regardless of a struct-level `Default`.
13317 let from_writable_json_inits: Vec<_> = fields_info
13318 .iter()
13319 .map(|fi| {
13320 let ident = &fi.ident;
13321 let fname = ident.to_string();
13322 let ty = &fi.ty;
13323 if is_writable(&fi) {
13324 quote! {
13325 #ident: match __obj.and_then(|__o| __o.get(#fname)) {
13326 ::core::option::Option::Some(__v) => {
13327 match #root::__serde_json::from_value::<#ty>(
13328 ::core::clone::Clone::clone(__v),
13329 ) {
13330 ::core::result::Result::Ok(__x) => __x,
13331 ::core::result::Result::Err(__e) => {
13332 __errors.add(#fname.to_owned(), __e.to_string());
13333 ::core::default::Default::default()
13334 }
13335 }
13336 }
13337 ::core::option::Option::None => ::core::default::Default::default(),
13338 }
13339 }
13340 } else {
13341 quote! { #ident: ::core::default::Default::default() }
13342 }
13343 })
13344 .collect();
13345
13346 // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
13347 // Only includes fields shown in JSON output (skips write_only). For each
13348 // `Option<T>` field, omit from `required` and add `.nullable()`.
13349 let openapi_impl = {
13350 #[cfg(feature = "openapi")]
13351 {
13352 let property_calls = output_fields.iter().map(|fi| {
13353 let ident = &fi.ident;
13354 let name_lit = ident.to_string();
13355 let ty = &fi.ty;
13356 let nullable_call = if is_option(ty) {
13357 quote! { .nullable() }
13358 } else {
13359 quote! {}
13360 };
13361 quote! {
13362 .property(
13363 #name_lit,
13364 <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13365 #nullable_call,
13366 )
13367 }
13368 });
13369 let required_lits: Vec<_> = output_fields
13370 .iter()
13371 .filter(|fi| !is_option(&fi.ty))
13372 .map(|fi| fi.ident.to_string())
13373 .collect();
13374 quote! {
13375 impl #root::openapi::OpenApiSchema for #struct_name {
13376 fn openapi_schema() -> #root::openapi::Schema {
13377 #root::openapi::Schema::object()
13378 #( #property_calls )*
13379 .required([ #( #required_lits ),* ])
13380 }
13381 }
13382 }
13383 }
13384 #[cfg(not(feature = "openapi"))]
13385 {
13386 quote! {}
13387 }
13388 };
13389
13390 Ok(quote! {
13391 impl #root::serializer::ModelSerializer for #struct_name {
13392 type Model = #model_path;
13393
13394 fn from_model(model: &Self::Model) -> Self {
13395 Self {
13396 #( #from_model_fields ),*
13397 }
13398 }
13399
13400 fn writable_fields() -> &'static [&'static str] {
13401 &[ #( #writable_lits ),* ]
13402 }
13403
13404 fn writable_source_fields() -> &'static [&'static str] {
13405 &[ #( #writable_source_lits ),* ]
13406 }
13407
13408 fn from_writable_json(
13409 __body: &#root::__serde_json::Value,
13410 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
13411 let mut __errors = #root::forms::FormErrors::default();
13412 let __obj = __body.as_object();
13413 let __out = Self {
13414 #( #from_writable_json_inits ),*
13415 };
13416 if __errors.is_empty() {
13417 ::core::result::Result::Ok(__out)
13418 } else {
13419 ::core::result::Result::Err(__errors)
13420 }
13421 }
13422
13423 #trait_validate_override
13424 }
13425
13426 impl #root::__serde::Serialize for #struct_name {
13427 fn serialize<S>(&self, serializer: S)
13428 -> ::core::result::Result<S::Ok, S::Error>
13429 where
13430 S: #root::__serde::Serializer,
13431 {
13432 use #root::__serde::ser::SerializeStruct;
13433 let mut __state = serializer.serialize_struct(
13434 #struct_name_lit,
13435 #output_field_count,
13436 )?;
13437 #( #serialize_fields )*
13438 __state.end()
13439 }
13440 }
13441
13442 #openapi_impl
13443
13444 #validate_method
13445
13446 #many_setters_impl
13447 })
13448}
13449
13450/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
13451/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
13452/// when the feature is off.
13453#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
13454fn is_option(ty: &syn::Type) -> bool {
13455 if let syn::Type::Path(p) = ty {
13456 if let Some(last) = p.path.segments.last() {
13457 return last.ident == "Option";
13458 }
13459 }
13460 false
13461}