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 // `try_init` so duplicate installers (e.g. tests already
334 // holding a subscriber) don't panic.
335 let _ = tracing_subscriber::fmt()
336 .with_env_filter(
337 EnvFilter::try_from_default_env()
338 .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
339 )
340 .try_init();
341 }
342 let __rt = #builder_call
343 .enable_all()
344 .build()
345 .expect("failed to build tokio runtime");
346 __rt.block_on(async move #user_body)
347 }})?;
348
349 Ok(quote! {
350 #input
351 })
352}
353
354enum Flavor {
355 MultiThread,
356 CurrentThread,
357}
358
359fn parse_flavor(args: &TokenStream2) -> Flavor {
360 // Cheap parser: look for the literal token sequence
361 // `flavor = "current_thread"`. Everything else (including
362 // bare `multi_thread` or no args) defaults to multi-thread.
363 let s = args.to_string();
364 if s.contains("current_thread") {
365 Flavor::CurrentThread
366 } else {
367 Flavor::MultiThread
368 }
369}
370
371/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
372struct QInput {
373 base_path: syn::Path,
374 field: syn::Ident,
375 value: syn::Expr,
376}
377
378impl syn::parse::Parse for QInput {
379 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
380 let base_path: syn::Path = input.parse()?;
381 input.parse::<syn::Token![.]>()?;
382 let field: syn::Ident = input.parse()?;
383 input.parse::<syn::Token![=]>()?;
384 let value: syn::Expr = input.parse()?;
385 Ok(QInput {
386 base_path,
387 field,
388 value,
389 })
390 }
391}
392
393fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
394 let q: QInput = syn::parse2(input)?;
395 let root = rustango_root();
396 let field_str = q.field.to_string();
397 let field_span = q.field.span();
398 let (base, suffix) = match field_str.find("__") {
399 Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
400 None => (field_str.as_str(), ""),
401 };
402 if base.is_empty() {
403 return Err(syn::Error::new(
404 field_span,
405 "Q!(): field name is empty before `__` suffix",
406 ));
407 }
408 let base_ident = syn::Ident::new(base, field_span);
409 let value = &q.value;
410 let path = &q.base_path;
411
412 // Most suffixes map directly to a Column method with the value
413 // forwarded unchanged. Some need value-shape massaging (wildcards
414 // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
415 // ISNULL). Unknown suffixes fail the build.
416 let expanded = match suffix {
417 "" | "exact" => quote! {
418 #root::core::Column::eq(#path::#base_ident, #value)
419 },
420 "ne" => quote! {
421 #root::core::Column::ne(#path::#base_ident, #value)
422 },
423 "gt" => quote! {
424 #root::core::Column::gt(#path::#base_ident, #value)
425 },
426 "gte" => quote! {
427 #root::core::Column::gte(#path::#base_ident, #value)
428 },
429 "lt" => quote! {
430 #root::core::Column::lt(#path::#base_ident, #value)
431 },
432 "lte" => quote! {
433 #root::core::Column::lte(#path::#base_ident, #value)
434 },
435 "iexact" => quote! {
436 // Django emulates `__iexact` as case-insensitive equality.
437 // The non-wildcard `ILIKE value` is semantically identical
438 // for plain strings; LIKE-metachars `%` `_` in the rhs would
439 // accidentally match more — document the caveat.
440 #root::core::Column::ilike(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
441 },
442 "contains" => quote! {
443 #root::core::Column::like(
444 #path::#base_ident,
445 ::std::format!("%{}%", #value),
446 )
447 },
448 "icontains" => quote! {
449 #root::core::Column::ilike(
450 #path::#base_ident,
451 ::std::format!("%{}%", #value),
452 )
453 },
454 "startswith" => quote! {
455 #root::core::Column::like(
456 #path::#base_ident,
457 ::std::format!("{}%", #value),
458 )
459 },
460 "istartswith" => quote! {
461 #root::core::Column::ilike(
462 #path::#base_ident,
463 ::std::format!("{}%", #value),
464 )
465 },
466 "endswith" => quote! {
467 #root::core::Column::like(
468 #path::#base_ident,
469 ::std::format!("%{}", #value),
470 )
471 },
472 "iendswith" => quote! {
473 #root::core::Column::ilike(
474 #path::#base_ident,
475 ::std::format!("%{}", #value),
476 )
477 },
478 "in" => quote! {
479 #root::core::Column::is_in(#path::#base_ident, #value)
480 },
481 "not_in" => quote! {
482 #root::core::Column::not_in(#path::#base_ident, #value)
483 },
484 "isnull" => {
485 // Must be a bool literal at macro time so we can route to
486 // is_null vs is_not_null without a runtime branch.
487 let b = match value {
488 syn::Expr::Lit(syn::ExprLit {
489 lit: syn::Lit::Bool(b),
490 ..
491 }) => b.value(),
492 _ => {
493 return Err(syn::Error::new_spanned(
494 value,
495 "Q!(): `__isnull` requires a `true` or `false` literal",
496 ));
497 }
498 };
499 if b {
500 quote! { #root::core::Column::is_null(#path::#base_ident) }
501 } else {
502 quote! { #root::core::Column::is_not_null(#path::#base_ident) }
503 }
504 }
505 "between" => {
506 // Accept a tuple literal `(lo, hi)`.
507 let tuple = match value {
508 syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
509 _ => {
510 return Err(syn::Error::new_spanned(
511 value,
512 "Q!(): `__between` requires a tuple literal `(lo, hi)`",
513 ));
514 }
515 };
516 let lo = &tuple.elems[0];
517 let hi = &tuple.elems[1];
518 quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
519 }
520 "regex" => quote! {
521 #root::core::Column::regex(#path::#base_ident, #value)
522 },
523 "iregex" => quote! {
524 #root::core::Column::iregex(#path::#base_ident, #value)
525 },
526 _ => {
527 return Err(syn::Error::new(
528 field_span,
529 format!(
530 "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",
531 suffix
532 ),
533 ));
534 }
535 };
536 Ok(expanded)
537}
538
539fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
540 // Default to "./migrations" if invoked without args.
541 let path_str = if input.is_empty() {
542 "./migrations".to_string()
543 } else {
544 let lit: LitStr = syn::parse2(input)?;
545 lit.value()
546 };
547
548 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
549 syn::Error::new(
550 proc_macro2::Span::call_site(),
551 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
552 )
553 })?;
554 let abs = std::path::Path::new(&manifest).join(&path_str);
555
556 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
557 if abs.is_dir() {
558 let read = std::fs::read_dir(&abs).map_err(|e| {
559 syn::Error::new(
560 proc_macro2::Span::call_site(),
561 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
562 )
563 })?;
564 for entry in read.flatten() {
565 let path = entry.path();
566 if !path.is_file() {
567 continue;
568 }
569 if path.extension().and_then(|s| s.to_str()) != Some("json") {
570 continue;
571 }
572 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
573 continue;
574 };
575 entries.push((stem.to_owned(), path));
576 }
577 }
578 entries.sort_by(|a, b| a.0.cmp(&b.0));
579
580 // Compile-time chain validation: read each migration's JSON,
581 // pull `name` and `prev` (file-stem-keyed for the chain check),
582 // and verify every `prev` points to another migration in the
583 // slice. Mismatches between the file stem and the embedded
584 // `name` field — and broken `prev` chains — fail at MACRO
585 // EXPANSION time so a misshapen migration set never compiles.
586 //
587 // This is the v0.4 Slice 5 distinguisher: rustango's JSON
588 // migrations + a Rust proc-macro that reads them is the unique
589 // combo nothing else in the Django-shape Rust camp can match
590 // (Cot's are imperative Rust code, Loco's are SeaORM up/down,
591 // Rwf's are raw SQL — none have a static chain to validate).
592 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
593 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
594 for (stem, path) in &entries {
595 let raw = std::fs::read_to_string(path).map_err(|e| {
596 syn::Error::new(
597 proc_macro2::Span::call_site(),
598 format!(
599 "embed_migrations!: cannot read {} for chain validation: {e}",
600 path.display()
601 ),
602 )
603 })?;
604 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
605 syn::Error::new(
606 proc_macro2::Span::call_site(),
607 format!(
608 "embed_migrations!: {} is not valid JSON: {e}",
609 path.display()
610 ),
611 )
612 })?;
613 let name = json
614 .get("name")
615 .and_then(|v| v.as_str())
616 .ok_or_else(|| {
617 syn::Error::new(
618 proc_macro2::Span::call_site(),
619 format!(
620 "embed_migrations!: {} is missing the `name` field",
621 path.display()
622 ),
623 )
624 })?
625 .to_owned();
626 if name != *stem {
627 return Err(syn::Error::new(
628 proc_macro2::Span::call_site(),
629 format!(
630 "embed_migrations!: file stem `{stem}` does not match the migration's \
631 `name` field `{name}` — rename the file or fix the JSON",
632 ),
633 ));
634 }
635 let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
636 chain_names.push(name.clone());
637 prev_refs.push((name, prev));
638 }
639
640 let name_set: std::collections::HashSet<&str> =
641 chain_names.iter().map(String::as_str).collect();
642 for (name, prev) in &prev_refs {
643 if let Some(p) = prev {
644 if !name_set.contains(p.as_str()) {
645 return Err(syn::Error::new(
646 proc_macro2::Span::call_site(),
647 format!(
648 "embed_migrations!: broken migration chain — `{name}` declares \
649 prev=`{p}` but no migration with that name exists in {}",
650 abs.display()
651 ),
652 ));
653 }
654 }
655 }
656
657 let pairs: Vec<TokenStream2> = entries
658 .iter()
659 .map(|(name, path)| {
660 let path_lit = path.display().to_string();
661 quote! { (#name, ::core::include_str!(#path_lit)) }
662 })
663 .collect();
664
665 Ok(quote! {
666 {
667 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
668 __RUSTANGO_EMBEDDED
669 }
670 })
671}
672
673fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
674 let root = rustango_root();
675 let struct_name = &input.ident;
676
677 let Data::Struct(data) = &input.data else {
678 return Err(syn::Error::new_spanned(
679 struct_name,
680 "Model can only be derived on structs",
681 ));
682 };
683 let Fields::Named(named) = &data.fields else {
684 return Err(syn::Error::new_spanned(
685 struct_name,
686 "Model requires a struct with named fields",
687 ));
688 };
689
690 let container = parse_container_attrs(input)?;
691 let table = container
692 .table
693 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
694 let model_name = struct_name.to_string();
695
696 let collected = collect_fields(named, &table)?;
697
698 // Validate that #[rustango(display = "…")] names a real field.
699 if let Some((ref display, span)) = container.display {
700 if !collected.field_names.iter().any(|n| n == display) {
701 return Err(syn::Error::new(
702 span,
703 format!("`display = \"{display}\"` does not match any field on this struct"),
704 ));
705 }
706 }
707 let display = container.display.map(|(name, _)| name);
708 let app_label = container.app.clone();
709
710 // Validate admin field-name lists against declared field names.
711 // Note: `list_display` is intentionally NOT validated here. As of
712 // v0.32 it may also reference inventory-registered computed
713 // fields (via `register_admin_computed!`) whose existence the
714 // macro can't see at compile time — they're submitted from any
715 // crate that depends on rustango. The runtime list-view resolves
716 // unknown names against the inventory + silently drops the
717 // truly-bogus ones, which is the cheaper trade-off versus
718 // forcing a per-Model attr to opt out.
719 if let Some(admin) = &container.admin {
720 for (label, list) in [
721 ("search_fields", &admin.search_fields),
722 ("readonly_fields", &admin.readonly_fields),
723 ("list_filter", &admin.list_filter),
724 ] {
725 if let Some((names, span)) = list {
726 for name in names {
727 if !collected.field_names.iter().any(|n| n == name) {
728 return Err(syn::Error::new(
729 *span,
730 format!(
731 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
732 ),
733 ));
734 }
735 }
736 }
737 }
738 if let Some((pairs, span)) = &admin.ordering {
739 for (name, _) in pairs {
740 if !collected.field_names.iter().any(|n| n == name) {
741 return Err(syn::Error::new(
742 *span,
743 format!(
744 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
745 ),
746 ));
747 }
748 }
749 }
750 if let Some((groups, span)) = &admin.fieldsets {
751 for (_, fields) in groups {
752 for name in fields {
753 if !collected.field_names.iter().any(|n| n == name) {
754 return Err(syn::Error::new(
755 *span,
756 format!(
757 "`fieldsets`: \"{name}\" is not a declared field on this struct"
758 ),
759 ));
760 }
761 }
762 }
763 }
764 }
765 if let Some(audit) = &container.audit {
766 if let Some((names, span)) = &audit.track {
767 for name in names {
768 if !collected.field_names.iter().any(|n| n == name) {
769 return Err(syn::Error::new(
770 *span,
771 format!(
772 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
773 ),
774 ));
775 }
776 }
777 }
778 }
779
780 // Issue #291 / T2.5 — validate each `default_order` column name
781 // against the model's collected fields. Typos fail at macro-expand
782 // time, not at the database.
783 for (col, _desc, span) in &container.default_order {
784 if !collected.field_names.iter().any(|n| n == col) {
785 return Err(syn::Error::new(
786 *span,
787 format!(
788 "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
789 ),
790 ));
791 }
792 }
793
794 // Build the audit_track list for ModelSchema: None when no audit attr,
795 // Some(empty) when audit present without track, Some(names) when explicit.
796 let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
797 audit
798 .track
799 .as_ref()
800 .map(|(names, _)| names.clone())
801 .unwrap_or_default()
802 });
803
804 // Merge field-level indexes into the container's index list.
805 let mut all_indexes: Vec<IndexAttr> = container.indexes;
806 for field in &named.named {
807 let ident = field.ident.as_ref().expect("named");
808 let col = to_snake_case(&ident.to_string()); // column name fallback
809 // Re-parse field attrs to check for index flag
810 if let Ok(fa) = parse_field_attrs(field) {
811 if fa.index {
812 let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
813 let auto_name = if fa.index_unique {
814 format!("{table}_{col_name}_uq_idx")
815 } else {
816 format!("{table}_{col_name}_idx")
817 };
818 all_indexes.push(IndexAttr {
819 name: fa.index_name.or(Some(auto_name)),
820 columns: vec![col_name],
821 unique: fa.index_unique,
822 method: fa.index_method,
823 where_clause: None,
824 include: Vec::new(),
825 });
826 }
827 }
828 }
829
830 let model_impl = model_impl_tokens(
831 struct_name,
832 &model_name,
833 &table,
834 display.as_deref(),
835 app_label.as_deref(),
836 container.admin.as_ref(),
837 &container.default_order,
838 &collected.field_schemas,
839 collected.soft_delete_column.as_deref(),
840 container.permissions,
841 audit_track_names.as_deref(),
842 &container.m2m,
843 &all_indexes,
844 &container.checks,
845 &container.excludes,
846 &container.composite_fks,
847 &container.generic_fks,
848 container.scope.as_deref(),
849 container.is_view,
850 container.verbose_name.as_deref(),
851 container.verbose_name_plural.as_deref(),
852 container.managed,
853 container.base_manager_name.as_deref(),
854 container.order_with_respect_to.as_deref(),
855 container.proxy,
856 &container.required_db_features,
857 container.required_db_vendor.as_deref(),
858 container.default_related_name.as_deref(),
859 container.db_table_comment.as_deref(),
860 container
861 .get_latest_by
862 .as_ref()
863 .map(|(c, d)| (c.as_str(), *d)),
864 &container.extra_permissions,
865 &container.default_permissions,
866 &container.global_scopes,
867 &container.reverse_has_relations,
868 &container.generic_has_relations,
869 );
870 let module_ident = column_module_ident(struct_name);
871 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
872 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
873 let track_set: Option<std::collections::HashSet<&str>> = audit
874 .track
875 .as_ref()
876 .map(|(names, _)| names.iter().map(String::as_str).collect());
877 collected
878 .column_entries
879 .iter()
880 .filter(|c| {
881 track_set
882 .as_ref()
883 .map_or(true, |s| s.contains(c.name.as_str()))
884 })
885 .collect()
886 });
887 let inherent_impl = inherent_impl_tokens(
888 struct_name,
889 &collected,
890 collected.primary_key.as_ref(),
891 &column_consts,
892 audited_fields.as_deref(),
893 &all_indexes,
894 &container.manager_fns,
895 );
896 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
897 let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
898 let reverse_helpers = reverse_helper_tokens(
899 struct_name,
900 &collected.fk_relations,
901 container.default_related_name.as_deref(),
902 );
903 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
904 let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
905 // Issue #817 — `#[rustango(through(...))]` accessors.
906 let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
907 // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
908 let reverse_has_accessors =
909 reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
910 let generic_fk_accessors = generic_fk_accessor_tokens(
911 struct_name,
912 &container.generic_fks,
913 &collected.column_entries,
914 );
915
916 // Issue #271 / T1.9 — `#[rustango(manager(ext = "FooManagerExt"))]`
917 // emits an empty extension trait so users can add methods via
918 // `impl FooManagerExt for QuerySet<Foo>` without hand-writing the
919 // trait declaration. See `crates/rustango/src/manager.rs` for the
920 // pattern this replaces.
921 let manager_trait = container.manager_ext.as_ref().map(|name| {
922 let model_name_str = struct_name.to_string();
923 let doc = format!(
924 "Custom-Manager extension trait for [`{model_name_str}`]. \
925 Generated by `#[rustango(manager(ext = ...))]`. Add methods \
926 via `impl {name} for QuerySet<{model_name_str}> {{ ... }}`."
927 );
928 quote! {
929 #[doc = #doc]
930 pub trait #name: ::core::marker::Sized {}
931 }
932 });
933
934 Ok(quote! {
935 #model_impl
936 #inherent_impl
937 #from_row_impl
938 #column_module
939 #reverse_helpers
940 #m2m_accessors
941 #generic_m2m_accessors
942 #through_accessors
943 #reverse_has_accessors
944 #generic_fk_accessors
945 #manager_trait
946
947 #root::core::inventory::submit! {
948 #root::core::ModelEntry {
949 schema: <#struct_name as #root::core::Model>::SCHEMA,
950 // `module_path!()` evaluates at the registration site,
951 // so a Model declared in `crate::blog::models` records
952 // `"<crate>::blog::models"` and `resolved_app_label()`
953 // can infer "blog" without an explicit attribute.
954 module_path: ::core::module_path!(),
955 }
956 }
957 })
958}
959
960/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
961/// matches `field_name` against the model's FK fields and, for a
962/// match, decodes the FK target via the parent's macro-generated
963/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
964/// `ForeignKey::Loaded` on `self`.
965///
966/// Always emitted (with empty arms for FK-less models, which
967/// return `Ok(false)` for any field name) so the `T: LoadRelated`
968/// trait bound on `fetch_on` is universally satisfied — users
969/// never have to think about implementing it.
970fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
971 let root = rustango_root();
972 let arms = fk_relations.iter().map(|rel| {
973 let parent_ty = &rel.parent_type;
974 let fk_col = rel.fk_column.as_str();
975 // FK field's Rust ident matches its SQL column name in v0.8
976 // (no `column = "..."` rename ships on FK fields).
977 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
978 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
979 let assign = if rel.nullable {
980 quote! {
981 self.#field_ident = ::core::option::Option::Some(
982 #root::sql::ForeignKey::loaded(_pk, _parent),
983 );
984 }
985 } else {
986 quote! {
987 self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
988 }
989 };
990 quote! {
991 #fk_col => {
992 let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
993 // Audit #451 — multi-hop `select_related("a__b__c")`:
994 // stitch the deeper relation onto this parent first,
995 // decoding it at the accumulated `__next_alias`. The
996 // parent type also impls `LoadRelated`, so this recurses
997 // the FK chain to arbitrary depth.
998 if let ::core::option::Option::Some(__r) = __rest {
999 let _ = #root::sql::LoadRelated::__rustango_load_related(
1000 &mut _parent, row, __r, &__next_alias,
1001 )?;
1002 }
1003 // Loud-in-debug, default-in-release: a divergence
1004 // between the FK field's declared `K` (drives the
1005 // expected `SqlValue::<Variant>`) and the parent's
1006 // `__rustango_pk_value` output is a macro-internal
1007 // invariant break — surfacing the panic in dev
1008 // catches it before users hit silent PK=0 corruption.
1009 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1010 #root::core::SqlValue::#variant_ident(v) => v,
1011 _other => {
1012 ::core::debug_assert!(
1013 false,
1014 "rustango macro bug: load_related on FK `{}` expected \
1015 SqlValue::{} from parent's __rustango_pk_value but got \
1016 {:?} — file a bug at https://github.com/ujeenet/rustango",
1017 #fk_col,
1018 ::core::stringify!(#variant_ident),
1019 _other,
1020 );
1021 #default_expr
1022 }
1023 };
1024 #assign
1025 ::core::result::Result::Ok(true)
1026 }
1027 }
1028 });
1029 quote! {
1030 #[cfg(feature = "postgres")]
1031 impl #root::sql::LoadRelated for #struct_name {
1032 #[allow(unused_variables)]
1033 fn __rustango_load_related(
1034 &mut self,
1035 row: &#root::sql::sqlx::postgres::PgRow,
1036 field_name: &str,
1037 alias: &str,
1038 ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1039 // Audit #451 — split the multi-hop path: `base` is the FK
1040 // on THIS model, `__rest` (if any) is the remaining chain
1041 // to stitch onto the loaded parent. `__next_alias` is the
1042 // accumulated join alias the parent's columns live under
1043 // (`{alias}__{next-hop}`), matching `lower_select_related`.
1044 let (__base, __rest): (&str, ::core::option::Option<&str>) =
1045 match field_name.split_once("__") {
1046 ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1047 ::core::option::Option::None => (field_name, ::core::option::Option::None),
1048 };
1049 let __next_alias: ::std::string::String = match __rest {
1050 ::core::option::Option::Some(__r) => {
1051 let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1052 ::std::format!("{}__{}", alias, __rb)
1053 }
1054 ::core::option::Option::None => ::std::string::String::new(),
1055 };
1056 match __base {
1057 #( #arms )*
1058 _ => ::core::result::Result::Ok(false),
1059 }
1060 }
1061 }
1062 }
1063}
1064
1065/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1066/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1067/// which expands to a `LoadRelatedMy` impl when rustango is built with
1068/// the `mysql` feature, and to nothing otherwise. The decoded parent
1069/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1070/// decoder, also batch8) so the dual emission is symmetric across
1071/// backends.
1072fn load_related_impl_my_tokens(
1073 struct_name: &syn::Ident,
1074 fk_relations: &[FkRelation],
1075) -> TokenStream2 {
1076 let root = rustango_root();
1077 let arms = fk_relations.iter().map(|rel| {
1078 let parent_ty = &rel.parent_type;
1079 let fk_col = rel.fk_column.as_str();
1080 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1081 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1082 let assign = if rel.nullable {
1083 quote! {
1084 __self.#field_ident = ::core::option::Option::Some(
1085 #root::sql::ForeignKey::loaded(_pk, _parent),
1086 );
1087 }
1088 } else {
1089 quote! {
1090 __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1091 }
1092 };
1093 // `self` IS hygiene-tracked through macro_rules — emitted from
1094 // a different context than the `&mut self` parameter inside
1095 // the macro_rules-expanded fn. Pass it through as `__self`
1096 // and let the macro_rules rebind it to the receiver.
1097 quote! {
1098 #fk_col => {
1099 let mut _parent: #parent_ty =
1100 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1101 // Audit #451 — multi-hop: stitch the deeper relation onto
1102 // the parent at the accumulated alias (see PG twin).
1103 if let ::core::option::Option::Some(__r) = __rest {
1104 let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1105 &mut _parent, row, __r, &__next_alias,
1106 )?;
1107 }
1108 // See note in `load_related_impl_tokens` (PG twin) —
1109 // the same loud-in-debug invariant guard.
1110 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1111 #root::core::SqlValue::#variant_ident(v) => v,
1112 _other => {
1113 ::core::debug_assert!(
1114 false,
1115 "rustango macro bug: load_related on FK `{}` expected \
1116 SqlValue::{} from parent's __rustango_pk_value but got \
1117 {:?} — file a bug at https://github.com/ujeenet/rustango",
1118 #fk_col,
1119 ::core::stringify!(#variant_ident),
1120 _other,
1121 );
1122 #default_expr
1123 }
1124 };
1125 #assign
1126 ::core::result::Result::Ok(true)
1127 }
1128 }
1129 });
1130 quote! {
1131 #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1132 #( #arms )*
1133 });
1134 }
1135}
1136
1137/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1138/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1139/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1140fn load_related_impl_sqlite_tokens(
1141 struct_name: &syn::Ident,
1142 fk_relations: &[FkRelation],
1143) -> TokenStream2 {
1144 let root = rustango_root();
1145 let arms = fk_relations.iter().map(|rel| {
1146 let parent_ty = &rel.parent_type;
1147 let fk_col = rel.fk_column.as_str();
1148 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1149 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1150 let assign = if rel.nullable {
1151 quote! {
1152 __self.#field_ident = ::core::option::Option::Some(
1153 #root::sql::ForeignKey::loaded(_pk, _parent),
1154 );
1155 }
1156 } else {
1157 quote! {
1158 __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1159 }
1160 };
1161 quote! {
1162 #fk_col => {
1163 let mut _parent: #parent_ty =
1164 <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1165 // Audit #451 — multi-hop: stitch the deeper relation onto
1166 // the parent at the accumulated alias (see PG twin).
1167 if let ::core::option::Option::Some(__r) = __rest {
1168 let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1169 &mut _parent, row, __r, &__next_alias,
1170 )?;
1171 }
1172 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1173 #root::core::SqlValue::#variant_ident(v) => v,
1174 _other => {
1175 ::core::debug_assert!(
1176 false,
1177 "rustango macro bug: load_related on FK `{}` expected \
1178 SqlValue::{} from parent's __rustango_pk_value but got \
1179 {:?} — file a bug at https://github.com/ujeenet/rustango",
1180 #fk_col,
1181 ::core::stringify!(#variant_ident),
1182 _other,
1183 );
1184 #default_expr
1185 }
1186 };
1187 #assign
1188 ::core::result::Result::Ok(true)
1189 }
1190 }
1191 });
1192 quote! {
1193 #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1194 #( #arms )*
1195 });
1196 }
1197}
1198
1199/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1200/// matches `field_name` against the model's FK fields and returns
1201/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1202/// group children by parent PK.
1203///
1204/// Always emitted (with `_ => None` for FK-less models) so the
1205/// trait bound on `fetch_with_prefetch` is universally satisfied.
1206fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1207 let root = rustango_root();
1208 let arms = fk_relations.iter().map(|rel| {
1209 let fk_col = rel.fk_column.as_str();
1210 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1211 if rel.pk_kind == DetectedKind::I64 {
1212 // i64 FK — return the stored PK so prefetch_related can
1213 // group children by it. Nullable variant unwraps via
1214 // `as_ref().map(...)`: an unset (NULL) FK column yields
1215 // `None` and that child sits out of the grouping (correct
1216 // semantics — it has no parent to attach to).
1217 if rel.nullable {
1218 quote! {
1219 #fk_col => self.#field_ident
1220 .as_ref()
1221 .map(|fk| #root::sql::ForeignKey::pk(fk)),
1222 }
1223 } else {
1224 quote! {
1225 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1226 }
1227 }
1228 } else {
1229 // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1230 // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1231 // i64-keyed grouping path — the trait signature is
1232 // `Option<i64>` and a non-i64 PK can't lower into it.
1233 // The FK still works for everything else (CRUD, lazy
1234 // load via `.get()`, select_related JOINs); only the
1235 // bulk prefetch grouper needs the integer key.
1236 quote! {
1237 #fk_col => ::core::option::Option::None,
1238 }
1239 }
1240 });
1241 // PK-type-agnostic version: every FK arm emits an
1242 // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1243 // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1244 // opt OUT of the legacy i64 method (it returns None) but opt IN
1245 // here.
1246 let value_arms = fk_relations.iter().map(|rel| {
1247 let fk_col = rel.fk_column.as_str();
1248 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1249 if rel.nullable {
1250 quote! {
1251 #fk_col => self.#field_ident
1252 .as_ref()
1253 .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1254 #root::sql::ForeignKey::pk(fk)
1255 )),
1256 }
1257 } else {
1258 quote! {
1259 #fk_col => ::core::option::Option::Some(
1260 ::core::convert::Into::<#root::core::SqlValue>::into(
1261 self.#field_ident.pk()
1262 )
1263 ),
1264 }
1265 }
1266 });
1267 quote! {
1268 impl #root::sql::FkPkAccess for #struct_name {
1269 #[allow(unused_variables)]
1270 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1271 match field_name {
1272 #( #arms )*
1273 _ => ::core::option::Option::None,
1274 }
1275 }
1276 #[allow(unused_variables)]
1277 fn __rustango_fk_pk_value(
1278 &self,
1279 field_name: &str,
1280 ) -> ::core::option::Option<#root::core::SqlValue> {
1281 match field_name {
1282 #( #value_arms )*
1283 _ => ::core::option::Option::None,
1284 }
1285 }
1286 }
1287 }
1288}
1289
1290/// For every `ForeignKey<Parent>` field on `Child`, emit
1291/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1292/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
1293/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
1294/// — the canonical reverse-FK fetch. One round trip, no N+1.
1295///
1296/// **PG-only emission**: the accessor is bounded on
1297/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1298/// both of which are gated behind the `postgres` cargo feature. The
1299/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1300/// model derive itself compiles on tri-dialect / sqlite-only
1301/// downstream builds — the accessor just isn't materialised. A tri-
1302/// dialect `_set_pool` variant is a separate follow-up.
1303fn reverse_helper_tokens(
1304 child_ident: &syn::Ident,
1305 fk_relations: &[FkRelation],
1306 default_related_name: Option<&str>,
1307) -> TokenStream2 {
1308 let root = rustango_root();
1309 if fk_relations.is_empty() {
1310 return TokenStream2::new();
1311 }
1312 // Method-name resolution per FK (issue #816 + follow-up):
1313 // 1. Field-level `#[rustango(related_name = "...")]` on the FK
1314 // itself — wins over everything else. Django's
1315 // `ForeignKey(related_name="...")`.
1316 // 2. Container-level `default_related_name = "..."` on the
1317 // child — Django's `class Meta: default_related_name`.
1318 // Applies to every FK on this model that didn't override.
1319 // 3. Fallback: `<child_snake>_set` — Django's `<child>_set`
1320 // convention. `Post` → `post_set`, `BlogComment` →
1321 // `blog_comment_set`. Avoids English-plural edge cases.
1322 //
1323 // The PG-on-executor variant keeps the resolved name; the
1324 // tri-dialect `_pool` variant appends `_pool` to it (matches the
1325 // framework's convention for the `&Pool` flavor of every helper).
1326 let default_pg_suffix = default_related_name
1327 .map(str::to_owned)
1328 .unwrap_or_else(|| format!("{}_set", to_snake_case(&child_ident.to_string())));
1329 let impls = fk_relations.iter().map(|rel| {
1330 let pg_suffix = rel
1331 .related_name
1332 .clone()
1333 .unwrap_or_else(|| default_pg_suffix.clone());
1334 let pool_suffix = format!("{}_pool", pg_suffix);
1335 let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1336 let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1337 let parent_ty = &rel.parent_type;
1338 let fk_col = rel.fk_column.as_str();
1339 let doc = format!(
1340 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1341 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1342 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1343 further `{child_ident}::objects()` filters via direct queryset use."
1344 );
1345 let pool_doc = format!(
1346 "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1347 [`#root::sql::Pool`] and dispatches per backend so the \
1348 reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1349 Use this from framework code that holds a `&Pool` (admin, \
1350 tenancy resolver, viewset handlers); reach for the executor- \
1351 bound variant when you already have a typed `sqlx::Executor`."
1352 );
1353 quote! {
1354 #[cfg(feature = "postgres")]
1355 impl #parent_ty {
1356 #[doc = #doc]
1357 ///
1358 /// # Errors
1359 /// Returns [`#root::sql::ExecError`] for SQL-writing
1360 /// or driver failures.
1361 pub async fn #pg_method_ident<'_c, _E>(
1362 &self,
1363 _executor: _E,
1364 ) -> ::core::result::Result<
1365 ::std::vec::Vec<#child_ident>,
1366 #root::sql::ExecError,
1367 >
1368 where
1369 _E: #root::sql::sqlx::Executor<
1370 '_c,
1371 Database = #root::sql::sqlx::Postgres,
1372 >,
1373 {
1374 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1375 #root::query::QuerySet::<#child_ident>::new()
1376 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1377 .fetch_on(_executor)
1378 .await
1379 }
1380 }
1381
1382 impl #parent_ty {
1383 #[doc = #pool_doc]
1384 ///
1385 /// # Errors
1386 /// Returns [`#root::sql::ExecError`] for SQL-writing
1387 /// or driver failures.
1388 pub async fn #pool_method_ident(
1389 &self,
1390 pool: &#root::sql::Pool,
1391 ) -> ::core::result::Result<
1392 ::std::vec::Vec<#child_ident>,
1393 #root::sql::ExecError,
1394 > {
1395 use #root::sql::FetcherPool as _;
1396 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1397 #root::query::QuerySet::<#child_ident>::new()
1398 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1399 .fetch(pool)
1400 .await
1401 }
1402 }
1403 }
1404 });
1405 quote! { #( #impls )* }
1406}
1407
1408/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1409/// relation declared on the model.
1410/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1411/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1412///
1413/// Closes #239 + #240 — the Django-shape `comment.content_object` /
1414/// `comment.content_object = post` ergonomics on top of the existing
1415/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1416///
1417/// `column_entries` is passed so we can resolve each `ct_column` /
1418/// `pk_column` SQL name back to its Rust field ident — the macro
1419/// only sees the column-side strings in the attribute, but the
1420/// emitted accessor needs to read the actual struct field.
1421fn generic_fk_accessor_tokens(
1422 struct_name: &syn::Ident,
1423 generic_fks: &[GenericFkAttr],
1424 column_entries: &[ColumnEntry],
1425) -> TokenStream2 {
1426 let root = rustango_root();
1427 if generic_fks.is_empty() {
1428 return TokenStream2::new();
1429 }
1430 let methods = generic_fks.iter().filter_map(|gfk| {
1431 // Resolve `ct_column` + `pk_column` to the struct's Rust
1432 // field idents. A typo (column name doesn't match any field)
1433 // emits no method for that registration — the user will see
1434 // the compiler reject the SCHEMA literal anyway, so there's
1435 // a clear error path without us double-reporting.
1436 let ct_ident = column_entries
1437 .iter()
1438 .find(|c| c.column == gfk.ct_column)
1439 .map(|c| c.ident.clone())?;
1440 let pk_ident = column_entries
1441 .iter()
1442 .find(|c| c.column == gfk.pk_column)
1443 .map(|c| c.ident.clone())?;
1444
1445 let accessor_ident =
1446 syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1447 let setter_ident =
1448 syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1449 let name_literal = gfk.name.as_str();
1450
1451 Some(quote! {
1452 #[doc = concat!(
1453 "Resolve the polymorphic `",
1454 #name_literal,
1455 "` relation. Reads `self.",
1456 stringify!(#ct_ident),
1457 "` + `self.",
1458 stringify!(#pk_ident),
1459 "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1460 "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1461 #name_literal,
1462 "\", ...))]`."
1463 )]
1464 pub async fn #accessor_ident(
1465 &self,
1466 pool: &#root::sql::Pool,
1467 ) -> ::core::result::Result<
1468 ::core::option::Option<#root::__serde_json::Value>,
1469 #root::sql::ExecError,
1470 > {
1471 let gfk = #root::contenttypes::GenericForeignKey::new(
1472 self.#ct_ident as i64,
1473 self.#pk_ident as i64,
1474 );
1475 gfk.get_object(pool).await
1476 }
1477
1478 #[doc = concat!(
1479 "Set the polymorphic `",
1480 #name_literal,
1481 "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1482 stringify!(#ct_ident),
1483 "` and `self.",
1484 stringify!(#pk_ident),
1485 "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1486 #name_literal,
1487 "\", ...))]`."
1488 )]
1489 pub async fn #setter_ident<T: #root::core::Model>(
1490 &mut self,
1491 pool: &#root::sql::Pool,
1492 target_pk: i64,
1493 ) -> ::core::result::Result<(), #root::sql::ExecError> {
1494 let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1495 pool,
1496 target_pk,
1497 ).await?;
1498 self.#ct_ident = gfk.content_type_id as _;
1499 self.#pk_ident = gfk.object_pk as _;
1500 ::core::result::Result::Ok(())
1501 }
1502 })
1503 });
1504 quote! {
1505 impl #struct_name {
1506 #( #methods )*
1507 }
1508 }
1509}
1510
1511fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1512 let root = rustango_root();
1513 if m2m_relations.is_empty() {
1514 return TokenStream2::new();
1515 }
1516 let methods = m2m_relations.iter().map(|rel| {
1517 let method_name = format!("{}_m2m", rel.name);
1518 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1519 let through = rel.through.as_str();
1520 let src_col = rel.src.as_str();
1521 let dst_col = rel.dst.as_str();
1522 quote! {
1523 pub fn #method_ident(&self) -> #root::sql::M2MManager {
1524 #root::sql::M2MManager {
1525 src_pk: self.__rustango_pk_value(),
1526 through: #through,
1527 src_col: #src_col,
1528 dst_col: #dst_col,
1529 }
1530 }
1531 }
1532 });
1533 quote! {
1534 impl #struct_name {
1535 #( #methods )*
1536 }
1537 }
1538}
1539
1540/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1541/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1542fn generic_m2m_accessor_tokens(
1543 struct_name: &syn::Ident,
1544 relations: &[GenericM2MAttr],
1545) -> TokenStream2 {
1546 let root = rustango_root();
1547 if relations.is_empty() {
1548 return TokenStream2::new();
1549 }
1550 let methods = relations.iter().map(|rel| {
1551 let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1552 let through = rel.through.as_str();
1553 let pk_col = rel.pk_column.as_str();
1554 let ct_col = rel.ct_column.as_str();
1555 let related_col = rel.related_column.as_str();
1556 quote! {
1557 pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1558 #root::sql::GenericM2MManager {
1559 src_pk: self.__rustango_pk_value(),
1560 src_schema: <Self as #root::core::Model>::SCHEMA,
1561 through: #through,
1562 pk_col: #pk_col,
1563 ct_col: #ct_col,
1564 dst_col: #related_col,
1565 }
1566 }
1567 }
1568 });
1569 quote! {
1570 impl #struct_name {
1571 #( #methods )*
1572 }
1573 }
1574}
1575
1576/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1577/// associated functions for each `#[rustango(reverse_has(...))]`
1578/// attribute. Issue #830.
1579///
1580/// The two emitted functions return ready-to-use `WhereExpr` nodes
1581/// that downstream callers drop into
1582/// `QuerySet::<Self>::where_raw(...)`:
1583///
1584/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1585/// <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1586/// `whereHas` parity (without the closure-style sub-predicate
1587/// refinement — that's a follow-up; users can layer additional
1588/// predicates by constructing the SelectQuery themselves and
1589/// calling `where_raw(exists(query))` from `crate::core::subquery`).
1590/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1591/// `whereDoesntHave` parity.
1592///
1593/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1594/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1595/// machinery threads the outer-table reference through automatically
1596/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1597fn reverse_has_accessor_tokens(
1598 struct_name: &syn::Ident,
1599 reverse_has_relations: &[ReverseHasAttr],
1600) -> TokenStream2 {
1601 let root = rustango_root();
1602 if reverse_has_relations.is_empty() {
1603 return TokenStream2::new();
1604 }
1605 let methods = reverse_has_relations.iter().map(|rel| {
1606 let exists_name = format!("{}_exists_expr", rel.name);
1607 let not_exists_name = format!("{}_not_exists_expr", rel.name);
1608 let count_name = format!("{}_count", rel.name);
1609 let fetch_name = format!("{}_fetch", rel.name);
1610 let first_name = format!("{}_first", rel.name);
1611 let pluck_name = format!("{}_pluck", rel.name);
1612 let accessor_name = rel.name.as_str();
1613 let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1614 let not_exists_ident = syn::Ident::new(¬_exists_name, struct_name.span());
1615 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1616 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1617 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1618 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1619 let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1620 let child = &rel.child;
1621 let child_fk_column = rel.child_fk_column.as_str();
1622 let self_pk_column = rel.self_pk_column.as_str();
1623 let exists_doc = format!(
1624 "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1625 FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1626 Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1627 filter to {struct_name}s with at least one matching child.",
1628 );
1629 let not_exists_doc = format!(
1630 "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1631 (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1632 <outer>.{self_pk_column})`. Drop into \
1633 `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1634 {struct_name}s with **no** matching child.",
1635 );
1636 let count_doc = format!(
1637 "Eloquent `$model->{name}->count()` analog — returns \
1638 the number of `{child}` rows whose `{child_fk_column}` \
1639 matches this `{struct_name}` instance's primary key. \
1640 Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1641 {child_fk_column} = <self.pk>`.",
1642 name = rel.name,
1643 );
1644 let accessor_doc = format!(
1645 "Eloquent `$model->{name}` accessor — returns a \
1646 `QuerySet<{child}>` filtered to rows whose \
1647 `{child_fk_column}` matches this `{struct_name}` \
1648 instance's primary key. **Chainable**: compose `.filter()` \
1649 / `.order_by()` / `.limit()` etc. on top, then call \
1650 `.fetch(&pool)` (the QuerySet trait method) when \
1651 done. For the simple \"fetch all\" hot path with no \
1652 further composition, prefer the bare-name \
1653 `{name}_fetch(&pool)` companion.",
1654 name = rel.name,
1655 );
1656 let fetch_doc = format!(
1657 "Eloquent `$model->{name}->get()` — bare-name hot-path \
1658 over `{name}(&self).fetch(&pool)`. Use this when \
1659 you don't need further `.filter()` / `.order_by()` \
1660 composition; falls back to the chainable accessor when \
1661 you do. Avoids the `_pool` suffix on the most common \
1662 call-site shape.",
1663 name = rel.name,
1664 );
1665 quote! {
1666 #[doc = #accessor_doc]
1667 pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1668 #root::query::QuerySet::<#child>::new()
1669 .filter(#child_fk_column, self.__rustango_pk_value())
1670 }
1671
1672 #[doc = #fetch_doc]
1673 pub async fn #fetch_ident(
1674 &self,
1675 pool: &#root::sql::Pool,
1676 ) -> ::core::result::Result<
1677 ::std::vec::Vec<#child>,
1678 #root::sql::ExecError,
1679 > {
1680 use #root::sql::FetcherPool as _;
1681 self.#accessor_ident().fetch(pool).await
1682 }
1683
1684 /// Eloquent `$model->relation->first()` / `hasOne`
1685 /// semantics — bare-name shortcut over
1686 /// `self.<name>().first(&pool)`. Returns `None` when no
1687 /// child rows match. Useful when the relation is
1688 /// nominally many-to-one in shape but at most one row is
1689 /// expected (latest comment, primary tag, etc.).
1690 pub async fn #first_ident(
1691 &self,
1692 pool: &#root::sql::Pool,
1693 ) -> ::core::result::Result<
1694 ::core::option::Option<#child>,
1695 #root::sql::ExecError,
1696 > {
1697 self.#accessor_ident().first(pool).await
1698 }
1699
1700 /// Eloquent `$model->relation->pluck($col)` — project a
1701 /// single column from the child rows into a `Vec<U>`.
1702 /// Skips the typed `Child` decode, which is cheaper when
1703 /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1704 pub async fn #pluck_ident<U>(
1705 &self,
1706 col: &'static str,
1707 pool: &#root::sql::Pool,
1708 ) -> ::core::result::Result<
1709 ::std::vec::Vec<U>,
1710 #root::sql::ExecError,
1711 >
1712 where
1713 U: #root::sql::MaybePgScalar
1714 + #root::sql::MaybeMyScalar
1715 + #root::sql::MaybeSqliteScalar
1716 + ::core::marker::Send
1717 + ::core::marker::Unpin,
1718 {
1719 self.#accessor_ident()
1720 .values_list_flat(col)
1721 .fetch::<U>(pool)
1722 .await
1723 }
1724
1725 #[doc = #exists_doc]
1726 pub fn #exists_ident() -> #root::core::WhereExpr {
1727 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1728 let child_schema =
1729 <#child as #root::core::Model>::SCHEMA;
1730 let inner = SelectQuery {
1731 where_clause: WhereExpr::ExprCompare {
1732 lhs: Expr::Column(#child_fk_column),
1733 op: Op::Eq,
1734 rhs: Expr::OuterRef(#self_pk_column),
1735 },
1736 ..SelectQuery::new(child_schema)
1737 };
1738 WhereExpr::Exists(::std::boxed::Box::new(inner))
1739 }
1740
1741 #[doc = #not_exists_doc]
1742 pub fn #not_exists_ident() -> #root::core::WhereExpr {
1743 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1744 let child_schema =
1745 <#child as #root::core::Model>::SCHEMA;
1746 let inner = SelectQuery {
1747 where_clause: WhereExpr::ExprCompare {
1748 lhs: Expr::Column(#child_fk_column),
1749 op: Op::Eq,
1750 rhs: Expr::OuterRef(#self_pk_column),
1751 },
1752 ..SelectQuery::new(child_schema)
1753 };
1754 WhereExpr::NotExists(::std::boxed::Box::new(inner))
1755 }
1756
1757 #[doc = #count_doc]
1758 pub async fn #count_ident(
1759 &self,
1760 pool: &#root::sql::Pool,
1761 ) -> ::core::result::Result<
1762 i64,
1763 #root::sql::ExecError,
1764 > {
1765 use #root::sql::CounterPool as _;
1766 #root::query::QuerySet::<#child>::new()
1767 .filter(#child_fk_column, self.__rustango_pk_value())
1768 .count(pool)
1769 .await
1770 }
1771 }
1772 });
1773 quote! {
1774 impl #struct_name {
1775 #( #methods )*
1776 }
1777 }
1778}
1779
1780/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1781/// `#[rustango(through(...))]` attribute. Issue #817.
1782///
1783/// Each accessor builds a correlated subquery via
1784/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1785/// intermediate table, filters on the FK column pointing at this
1786/// model, and projects the intermediate PK. The outer queryset
1787/// filters the far model by its FK-to-intermediate column being in
1788/// that set.
1789///
1790/// The returned `QuerySet<Far>` is **chainable** — the subquery
1791/// lives inside a `where_raw` clause so the user's later
1792/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1793/// disturb it.
1794///
1795/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1796/// SQLite — no LATERAL or backend-specific syntax involved.
1797fn through_accessor_tokens(
1798 struct_name: &syn::Ident,
1799 through_relations: &[ThroughAttr],
1800) -> TokenStream2 {
1801 let root = rustango_root();
1802 if through_relations.is_empty() {
1803 return TokenStream2::new();
1804 }
1805 let methods = through_relations.iter().map(|rel| {
1806 let method_name = format!("{}_through", rel.name);
1807 let count_name = format!("{}_through_count", rel.name);
1808 let fetch_name = format!("{}_through_fetch", rel.name);
1809 let first_name = format!("{}_through_first", rel.name);
1810 let pluck_name = format!("{}_through_pluck", rel.name);
1811 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1812 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1813 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1814 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1815 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1816 let far = &rel.far;
1817 let intermediate = &rel.intermediate;
1818 let far_fk_column = rel.far_fk_column.as_str();
1819 let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1820 let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1821 let doc = format!(
1822 "Eloquent `hasManyThrough` accessor — returns a \
1823 `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1824 instance through the intermediate `{intermediate}` table. \
1825 Generated SQL shape: \
1826 `… WHERE {far_fk_column} IN (SELECT \
1827 {intermediate_pk_column} FROM <{intermediate}> WHERE \
1828 {intermediate_fk_column} = self.pk)`. Chainable like any \
1829 other QuerySet — compose `.filter()` / `.order_by()` / \
1830 `.limit()` etc. on top.",
1831 );
1832 let count_doc = format!(
1833 "Eloquent `$model->{name}->count()` analog for the \
1834 through-relation — returns the number of `{far}` rows \
1835 reachable through `{intermediate}`. Equivalent to \
1836 `self.{name}_through().count(pool)` but spelled \
1837 as a bare instance method for parity with the \
1838 `reverse_has` `<name>_count` shape.",
1839 name = rel.name,
1840 );
1841 quote! {
1842 #[doc = #doc]
1843 pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1844 use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1845 let intermediate_schema =
1846 <#intermediate as #root::core::Model>::SCHEMA;
1847 let sub = SelectQuery {
1848 where_clause: WhereExpr::Predicate(Filter {
1849 column: #intermediate_fk_column,
1850 op: Op::Eq,
1851 value: self.__rustango_pk_value(),
1852 }),
1853 projection: ::core::option::Option::Some(
1854 ::std::vec![#intermediate_pk_column],
1855 ),
1856 ..SelectQuery::new(intermediate_schema)
1857 };
1858 #root::query::QuerySet::<#far>::new().where_raw(
1859 WhereExpr::InSubquery {
1860 column: #far_fk_column,
1861 negated: false,
1862 subquery: ::std::boxed::Box::new(sub),
1863 },
1864 )
1865 }
1866
1867 #[doc = #count_doc]
1868 pub async fn #count_ident(
1869 &self,
1870 pool: &#root::sql::Pool,
1871 ) -> ::core::result::Result<
1872 i64,
1873 #root::sql::ExecError,
1874 > {
1875 use #root::sql::CounterPool as _;
1876 self.#method_ident().count(pool).await
1877 }
1878
1879 /// Eloquent `$model->relation->get()` for the
1880 /// through-relation — bare-name hot-path over
1881 /// `self.<name>_through().fetch(&pool)`. Use this
1882 /// when you don't need further `.filter()` /
1883 /// `.order_by()` composition; falls back to the
1884 /// chainable accessor when you do. Avoids the `_pool`
1885 /// suffix on the most common call-site shape.
1886 pub async fn #fetch_ident(
1887 &self,
1888 pool: &#root::sql::Pool,
1889 ) -> ::core::result::Result<
1890 ::std::vec::Vec<#far>,
1891 #root::sql::ExecError,
1892 > {
1893 use #root::sql::FetcherPool as _;
1894 self.#method_ident().fetch(pool).await
1895 }
1896
1897 /// Eloquent `hasOneThrough` analog — bare-name shortcut
1898 /// over `self.<name>_through().first(&pool)`. Returns
1899 /// `None` when no far rows are reachable through the
1900 /// intermediate. Useful when at most one row is
1901 /// expected (latest comment by country, primary tag,
1902 /// etc.).
1903 pub async fn #first_ident(
1904 &self,
1905 pool: &#root::sql::Pool,
1906 ) -> ::core::result::Result<
1907 ::core::option::Option<#far>,
1908 #root::sql::ExecError,
1909 > {
1910 self.#method_ident().first(pool).await
1911 }
1912
1913 /// Pluck a single column from the far rows into `Vec<U>`
1914 /// — cheaper than the typed `<Far>` decode when only one
1915 /// scalar column is needed.
1916 pub async fn #pluck_ident<U>(
1917 &self,
1918 col: &'static str,
1919 pool: &#root::sql::Pool,
1920 ) -> ::core::result::Result<
1921 ::std::vec::Vec<U>,
1922 #root::sql::ExecError,
1923 >
1924 where
1925 U: #root::sql::MaybePgScalar
1926 + #root::sql::MaybeMyScalar
1927 + #root::sql::MaybeSqliteScalar
1928 + ::core::marker::Send
1929 + ::core::marker::Unpin,
1930 {
1931 self.#method_ident()
1932 .values_list_flat(col)
1933 .fetch::<U>(pool)
1934 .await
1935 }
1936 }
1937 });
1938 quote! {
1939 impl #struct_name {
1940 #( #methods )*
1941 }
1942 }
1943}
1944
1945struct ColumnEntry {
1946 /// The struct field ident, used both for the inherent const name on
1947 /// the model and for the inner column type's name.
1948 ident: syn::Ident,
1949 /// The struct's field type, used as `Column::Value`.
1950 value_ty: Type,
1951 /// Rust-side field name (e.g. `"id"`).
1952 name: String,
1953 /// SQL-side column name (e.g. `"user_id"`).
1954 column: String,
1955 /// `#root::core::FieldType::I64` etc.
1956 field_type_tokens: TokenStream2,
1957}
1958
1959struct CollectedFields {
1960 field_schemas: Vec<TokenStream2>,
1961 from_row_inits: Vec<TokenStream2>,
1962 /// Aliased counterparts of `from_row_inits` — read columns via
1963 /// `format!("{prefix}__{col}")` aliases so a Model can be
1964 /// decoded from a JOINed row's projected target columns.
1965 from_aliased_row_inits: Vec<TokenStream2>,
1966 /// Static column-name list — used by the simple insert path
1967 /// (no `Auto<T>` fields). Aligned with `insert_values`.
1968 insert_columns: Vec<TokenStream2>,
1969 /// Static `Into<SqlValue>` expressions, one per field. Aligned
1970 /// with `insert_columns`. Used by the simple insert path only.
1971 insert_values: Vec<TokenStream2>,
1972 /// Per-field push expressions for the dynamic (Auto-aware)
1973 /// insert path. Each statement either unconditionally pushes
1974 /// `(column, value)` or, for an `Auto<T>` field, conditionally
1975 /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
1976 insert_pushes: Vec<TokenStream2>,
1977 /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
1978 /// when `has_auto == false`.
1979 returning_cols: Vec<TokenStream2>,
1980 /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
1981 /// field. Run after `insert_returning` to populate the model.
1982 auto_assigns: Vec<TokenStream2>,
1983 /// `(ident, column_literal)` pairs for every Auto field. Used by
1984 /// the bulk_insert codegen to rebuild assigns against `_row_mut`
1985 /// instead of `self`.
1986 auto_field_idents: Vec<(syn::Ident, String)>,
1987 /// #1028 — `(ident, column)` for each `generated_as` field. Drives
1988 /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
1989 /// back into the struct after insert (MySQL has no RETURNING → the
1990 /// field stays at its placeholder, deferred, matching Django 6.0).
1991 generated_field_idents: Vec<(syn::Ident, String)>,
1992 /// Inner `T` of the first `Auto<T>` field, for the MySQL
1993 /// `LAST_INSERT_ID()` assignment in `AssignAutoPkPool`.
1994 first_auto_value_ty: Option<Type>,
1995 /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
1996 /// by the all-Auto-Unset bulk path (Auto cols dropped from
1997 /// `columns`).
1998 bulk_pushes_no_auto: Vec<TokenStream2>,
1999 /// Bulk-insert per-row pushes for **all fields including Auto**.
2000 /// Used by the all-Auto-Set bulk path (Auto col included with the
2001 /// caller-supplied value).
2002 bulk_pushes_all: Vec<TokenStream2>,
2003 /// Column-name literals for non-Auto fields only (paired with
2004 /// `bulk_pushes_no_auto`).
2005 bulk_columns_no_auto: Vec<TokenStream2>,
2006 /// Column-name literals for every field including Auto (paired
2007 /// with `bulk_pushes_all`).
2008 bulk_columns_all: Vec<TokenStream2>,
2009 /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2010 /// + the loop that asserts every row matches. One pair per Auto
2011 /// field. Empty when `has_auto == false`.
2012 bulk_auto_uniformity: Vec<TokenStream2>,
2013 /// Identifier of the first Auto field, used as the witness for
2014 /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2015 first_auto_ident: Option<syn::Ident>,
2016 /// `true` if any field on the struct is `Auto<T>`.
2017 has_auto: bool,
2018 /// `true` when the primary-key field's Rust type is `Auto<T>`.
2019 /// Gates `save()` codegen — only Auto PKs let us infer
2020 /// insert-vs-update from the in-memory value.
2021 pk_is_auto: bool,
2022 /// `Assignment` constructors for every non-PK column. Drives the
2023 /// UPDATE branch of `save()`.
2024 update_assignments: Vec<TokenStream2>,
2025 /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2026 /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2027 upsert_update_columns: Vec<TokenStream2>,
2028 primary_key: Option<(syn::Ident, String)>,
2029 column_entries: Vec<ColumnEntry>,
2030 /// Rust-side field names, in declaration order. Used to validate
2031 /// container attributes like `display = "…"`.
2032 field_names: Vec<String>,
2033 /// FK fields on this child model. Drives the reverse-relation
2034 /// helper emit — for each FK, the macro adds an inherent
2035 /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2036 /// method on the parent type.
2037 fk_relations: Vec<FkRelation>,
2038 /// SQL column name of the `#[rustango(soft_delete)]` field, if
2039 /// the model has one. Drives emission of the `soft_delete_on` /
2040 /// `restore_on` inherent methods. At most one such column per
2041 /// model is allowed; collect_fields rejects duplicates.
2042 soft_delete_column: Option<String>,
2043 /// Rust field ident of the `#[rustango(soft_delete)]` field —
2044 /// companion to `soft_delete_column` for emitting predicates
2045 /// that need to read the field off `&self` (e.g. `trashed()`).
2046 soft_delete_field_ident: Option<syn::Ident>,
2047}
2048
2049#[derive(Clone)]
2050struct FkRelation {
2051 /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2052 /// helper is emitted as `impl <ParentType> { … }`.
2053 parent_type: Type,
2054 /// SQL column name on the child table for this FK (e.g. `"author"`).
2055 /// Used in the generated `WHERE <fk_column> = $1` clause.
2056 fk_column: String,
2057 /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2058 /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2059 /// default `ForeignKey<T>` (no explicit K); other kinds when the
2060 /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2061 pk_kind: DetectedKind,
2062 /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2063 /// FK column). Drives the `Some(...)` wrapping in load_related
2064 /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2065 /// the codegen matches the field's declared shape.
2066 nullable: bool,
2067 /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2068 /// override. When set, the reverse helper picks this name instead
2069 /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2070 /// #816 (issue's "Related" note re: per-FK override).
2071 related_name: Option<String>,
2072}
2073
2074fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2075 let root = rustango_root();
2076 let cap = named.named.len();
2077 let mut out = CollectedFields {
2078 field_schemas: Vec::with_capacity(cap),
2079 from_row_inits: Vec::with_capacity(cap),
2080 from_aliased_row_inits: Vec::with_capacity(cap),
2081 insert_columns: Vec::with_capacity(cap),
2082 insert_values: Vec::with_capacity(cap),
2083 insert_pushes: Vec::with_capacity(cap),
2084 returning_cols: Vec::new(),
2085 auto_assigns: Vec::new(),
2086 auto_field_idents: Vec::new(),
2087 generated_field_idents: Vec::new(),
2088 first_auto_value_ty: None,
2089 bulk_pushes_no_auto: Vec::with_capacity(cap),
2090 bulk_pushes_all: Vec::with_capacity(cap),
2091 bulk_columns_no_auto: Vec::with_capacity(cap),
2092 bulk_columns_all: Vec::with_capacity(cap),
2093 bulk_auto_uniformity: Vec::new(),
2094 first_auto_ident: None,
2095 has_auto: false,
2096 pk_is_auto: false,
2097 update_assignments: Vec::with_capacity(cap),
2098 upsert_update_columns: Vec::with_capacity(cap),
2099 primary_key: None,
2100 column_entries: Vec::with_capacity(cap),
2101 field_names: Vec::with_capacity(cap),
2102 fk_relations: Vec::new(),
2103 soft_delete_column: None,
2104 soft_delete_field_ident: None,
2105 };
2106
2107 for field in &named.named {
2108 let info = process_field(field, table)?;
2109 out.field_names.push(info.ident.to_string());
2110 out.field_schemas.push(info.schema);
2111 out.from_row_inits.push(info.from_row_init);
2112 out.from_aliased_row_inits.push(info.from_aliased_row_init);
2113 if let Some(parent_ty) = info.fk_inner.clone() {
2114 out.fk_relations.push(FkRelation {
2115 parent_type: parent_ty,
2116 fk_column: info.column.clone(),
2117 pk_kind: info.fk_pk_kind,
2118 nullable: info.nullable,
2119 related_name: info.related_name.clone(),
2120 });
2121 }
2122 if info.soft_delete {
2123 if out.soft_delete_column.is_some() {
2124 return Err(syn::Error::new_spanned(
2125 field,
2126 "only one field may be marked `#[rustango(soft_delete)]`",
2127 ));
2128 }
2129 out.soft_delete_column = Some(info.column.clone());
2130 out.soft_delete_field_ident = Some(info.ident.clone());
2131 }
2132 let column = info.column.as_str();
2133 let ident = info.ident;
2134 // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2135 // skip every write path — Postgres recomputes the value
2136 // from EXPR. Push only the column-entry record (so typed
2137 // column constants still exist for filtering / projection)
2138 // and the schema literal (already pushed above) and move
2139 // on. No insert_columns/values, no insert_pushes, no
2140 // bulk_*, no update_assignments, no upsert_update_columns,
2141 // no returning_cols.
2142 if info.generated_as.is_some() {
2143 out.column_entries.push(ColumnEntry {
2144 ident: ident.clone(),
2145 value_ty: info.value_ty.clone(),
2146 name: ident.to_string(),
2147 column: info.column.clone(),
2148 field_type_tokens: info.field_type_tokens,
2149 });
2150 // #1028 — refresh the DB-computed value after insert on
2151 // RETURNING-capable backends. The column joins `returning_cols`
2152 // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2153 // ident is recorded so the `AssignAutoPkPool` impl decodes it
2154 // back into the struct. MySQL has no `INSERT … RETURNING`, so
2155 // it keeps the placeholder (deferred refresh, matching Django).
2156 out.returning_cols.push(quote!(#column));
2157 out.generated_field_idents
2158 .push((ident.clone(), info.column.clone()));
2159 continue;
2160 }
2161 out.insert_columns.push(quote!(#column));
2162 out.insert_values.push(quote! {
2163 ::core::convert::Into::<#root::core::SqlValue>::into(
2164 ::core::clone::Clone::clone(&self.#ident)
2165 )
2166 });
2167 if info.auto {
2168 out.has_auto = true;
2169 if out.first_auto_ident.is_none() {
2170 out.first_auto_ident = Some(ident.clone());
2171 out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
2172 }
2173 // `default_uuid_v7` (issue #823) generates the PK Rust-side
2174 // before binding, so the value is already in
2175 // `self.#ident` after the insert_push — RETURNING is
2176 // redundant. Skip adding this column to returning_cols /
2177 // auto_assigns / auto_field_idents to avoid (a) an
2178 // unnecessary RETURNING column on every dialect, and (b)
2179 // the MySQL `LAST_INSERT_ID()` path that can only fill an
2180 // integer PK.
2181 if !info.default_uuid_v7 {
2182 out.returning_cols.push(quote!(#column));
2183 out.auto_field_idents
2184 .push((ident.clone(), info.column.clone()));
2185 out.auto_assigns.push(quote! {
2186 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2187 });
2188 }
2189 if info.default_uuid_v7 {
2190 // Rust-side UUIDv7 generation (issue #823, Eloquent
2191 // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2192 // and bind; Auto::Set → bind the user's value. The
2193 // column is ALWAYS present in the INSERT statement —
2194 // no RETURNING / no DB DEFAULT needed.
2195 out.insert_pushes.push(quote! {
2196 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2197 self.#ident = #root::sql::Auto::Set(
2198 #root::__uuid::Uuid::now_v7(),
2199 );
2200 }
2201 if let #root::sql::Auto::Set(_v) = &self.#ident {
2202 _columns.push(#column);
2203 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2204 ::core::clone::Clone::clone(_v)
2205 ));
2206 }
2207 });
2208 } else {
2209 out.insert_pushes.push(quote! {
2210 if let #root::sql::Auto::Set(_v) = &self.#ident {
2211 _columns.push(#column);
2212 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2213 ::core::clone::Clone::clone(_v)
2214 ));
2215 }
2216 });
2217 }
2218 // Bulk: Auto fields appear only in the all-Set path,
2219 // never in the Unset path (we drop them from `columns`).
2220 out.bulk_columns_all.push(quote!(#column));
2221 out.bulk_pushes_all.push(quote! {
2222 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2223 ::core::clone::Clone::clone(&_row.#ident)
2224 ));
2225 });
2226 // Uniformity check: every row's Auto state must match the
2227 // first row's. Mixed Set/Unset within one bulk_insert is
2228 // rejected here so the column list stays consistent.
2229 let ident_clone = ident.clone();
2230 out.bulk_auto_uniformity.push(quote! {
2231 for _r in rows.iter().skip(1) {
2232 if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2233 return ::core::result::Result::Err(
2234 #root::sql::ExecError::Sql(
2235 #root::sql::SqlError::BulkAutoMixed
2236 )
2237 );
2238 }
2239 }
2240 });
2241 } else {
2242 out.insert_pushes.push(quote! {
2243 _columns.push(#column);
2244 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2245 ::core::clone::Clone::clone(&self.#ident)
2246 ));
2247 });
2248 // Bulk: non-Auto fields appear in BOTH paths.
2249 out.bulk_columns_no_auto.push(quote!(#column));
2250 out.bulk_columns_all.push(quote!(#column));
2251 let push_expr = quote! {
2252 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2253 ::core::clone::Clone::clone(&_row.#ident)
2254 ));
2255 };
2256 out.bulk_pushes_no_auto.push(push_expr.clone());
2257 out.bulk_pushes_all.push(push_expr);
2258 }
2259 if info.primary_key {
2260 if out.primary_key.is_some() {
2261 return Err(syn::Error::new_spanned(
2262 field,
2263 "only one field may be marked `#[rustango(primary_key)]`",
2264 ));
2265 }
2266 out.primary_key = Some((ident.clone(), info.column.clone()));
2267 if info.auto {
2268 out.pk_is_auto = true;
2269 }
2270 } else if info.auto_now_add {
2271 // Immutable post-insert: skip from UPDATE entirely.
2272 } else if info.auto_now {
2273 // `auto_now` columns: bind `chrono::Utc::now()` on every
2274 // UPDATE so the column is always overridden with the
2275 // wall-clock at write time, regardless of what value the
2276 // user left in the struct field.
2277 out.update_assignments.push(quote! {
2278 #root::core::Assignment {
2279 column: #column,
2280 value: ::core::convert::Into::<#root::core::Expr>::into(
2281 ::core::convert::Into::<#root::core::SqlValue>::into(
2282 #root::__chrono::Utc::now()
2283 )
2284 ),
2285 }
2286 });
2287 out.upsert_update_columns.push(quote!(#column));
2288 } else {
2289 out.update_assignments.push(quote! {
2290 #root::core::Assignment {
2291 column: #column,
2292 value: ::core::convert::Into::<#root::core::Expr>::into(
2293 ::core::convert::Into::<#root::core::SqlValue>::into(
2294 ::core::clone::Clone::clone(&self.#ident)
2295 )
2296 ),
2297 }
2298 });
2299 out.upsert_update_columns.push(quote!(#column));
2300 }
2301 out.column_entries.push(ColumnEntry {
2302 ident: ident.clone(),
2303 value_ty: info.value_ty.clone(),
2304 name: ident.to_string(),
2305 column: info.column.clone(),
2306 field_type_tokens: info.field_type_tokens,
2307 });
2308 }
2309 Ok(out)
2310}
2311
2312fn model_impl_tokens(
2313 struct_name: &syn::Ident,
2314 model_name: &str,
2315 table: &str,
2316 display: Option<&str>,
2317 app_label: Option<&str>,
2318 admin: Option<&AdminAttrs>,
2319 default_order: &[(String, bool, proc_macro2::Span)],
2320 field_schemas: &[TokenStream2],
2321 soft_delete_column: Option<&str>,
2322 permissions: bool,
2323 audit_track: Option<&[String]>,
2324 m2m_relations: &[M2MAttr],
2325 indexes: &[IndexAttr],
2326 checks: &[CheckAttr],
2327 excludes: &[ExcludeAttr],
2328 composite_fks: &[CompositeFkAttr],
2329 generic_fks: &[GenericFkAttr],
2330 scope: Option<&str>,
2331 is_view: bool,
2332 verbose_name: Option<&str>,
2333 verbose_name_plural: Option<&str>,
2334 managed: bool,
2335 base_manager_name: Option<&str>,
2336 order_with_respect_to: Option<&str>,
2337 proxy: bool,
2338 required_db_features: &[String],
2339 required_db_vendor: Option<&str>,
2340 default_related_name: Option<&str>,
2341 db_table_comment: Option<&str>,
2342 get_latest_by: Option<(&str, bool)>,
2343 extra_permissions: &[(String, String)],
2344 default_permissions: &[String],
2345 global_scopes: &[GlobalScopeAttr],
2346 reverse_has_relations: &[ReverseHasAttr],
2347 generic_has_relations: &[GenericHasAttr],
2348) -> TokenStream2 {
2349 let root = rustango_root();
2350 let display_tokens = if let Some(name) = display {
2351 quote!(::core::option::Option::Some(#name))
2352 } else {
2353 quote!(::core::option::Option::None)
2354 };
2355 let app_label_tokens = if let Some(name) = app_label {
2356 quote!(::core::option::Option::Some(#name))
2357 } else {
2358 quote!(::core::option::Option::None)
2359 };
2360 let soft_delete_tokens = if let Some(col) = soft_delete_column {
2361 quote!(::core::option::Option::Some(#col))
2362 } else {
2363 quote!(::core::option::Option::None)
2364 };
2365 let audit_track_tokens = match audit_track {
2366 None => quote!(::core::option::Option::None),
2367 Some(names) => {
2368 let lits = names.iter().map(|n| n.as_str());
2369 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2370 }
2371 };
2372 let admin_tokens = admin_config_tokens(admin);
2373 // Default `tenant` so single-tenant projects (no `scope` attr
2374 // anywhere) keep the v0.24.x behavior. Container-attr parser
2375 // already validated the value is "registry" or "tenant".
2376 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2377 Some("registry") => quote!(#root::core::ModelScope::Registry),
2378 _ => quote!(#root::core::ModelScope::Tenant),
2379 };
2380 let verbose_name_tokens = optional_str(verbose_name);
2381 let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2382 let base_manager_name_tokens = optional_str(base_manager_name);
2383 let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2384 let required_db_features_lits: Vec<&str> =
2385 required_db_features.iter().map(String::as_str).collect();
2386 let required_db_vendor_tokens = optional_str(required_db_vendor);
2387 let default_related_name_tokens = optional_str(default_related_name);
2388 let db_table_comment_tokens = optional_str(db_table_comment);
2389 let get_latest_by_tokens = match get_latest_by {
2390 Some((col, desc)) => {
2391 quote!(::core::option::Option::Some((#col, #desc)))
2392 }
2393 None => quote!(::core::option::Option::None),
2394 };
2395 let extra_permission_tokens: Vec<_> = extra_permissions
2396 .iter()
2397 .map(|(c, l)| quote!((#c, #l)))
2398 .collect();
2399 let default_permission_tokens: Vec<_> = default_permissions
2400 .iter()
2401 .map(|action| quote!(#action))
2402 .collect();
2403 let indexes_tokens = indexes.iter().map(|idx| {
2404 // When no explicit `name = "..."` was given, derive a stable,
2405 // collision-free name from the table + columns (Django-shape
2406 // `<table>_<col>_<col>_idx`) instead of a shared literal that
2407 // would clash the moment a model declares two unnamed indexes.
2408 // Capped at Postgres's 63-char identifier limit.
2409 let derived_name = idx.name.clone().unwrap_or_else(|| {
2410 let mut n = format!("{table}_{}_idx", idx.columns.join("_"));
2411 if n.len() > 63 {
2412 n.truncate(63);
2413 }
2414 n
2415 });
2416 let name = derived_name.as_str();
2417 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2418 let unique = idx.unique;
2419 // Map the parsed method string onto the IndexMethod enum
2420 // variant — kept at the codegen layer so the IR doesn't
2421 // carry the string form.
2422 let method_variant = match idx.method.as_str() {
2423 "gin" => quote!(#root::core::IndexMethod::Gin),
2424 "gist" => quote!(#root::core::IndexMethod::Gist),
2425 "brin" => quote!(#root::core::IndexMethod::Brin),
2426 "spgist" => quote!(#root::core::IndexMethod::SpGist),
2427 "hash" => quote!(#root::core::IndexMethod::Hash),
2428 "bloom" => quote!(#root::core::IndexMethod::Bloom),
2429 _ => quote!(#root::core::IndexMethod::BTree),
2430 };
2431 let where_clause = match &idx.where_clause {
2432 Some(s) => quote!(::core::option::Option::Some(#s)),
2433 None => quote!(::core::option::Option::None),
2434 };
2435 let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2436 quote! {
2437 #root::core::IndexSchema {
2438 name: #name,
2439 columns: &[ #(#cols),* ],
2440 unique: #unique,
2441 method: #method_variant,
2442 where_clause: #where_clause,
2443 include: &[ #(#include_lits),* ],
2444 }
2445 }
2446 });
2447 let checks_tokens = checks.iter().map(|c| {
2448 let name = c.name.as_str();
2449 let expr = c.expr.as_str();
2450 quote! {
2451 #root::core::CheckConstraint {
2452 name: #name,
2453 expr: #expr,
2454 }
2455 }
2456 });
2457 let excludes_tokens = excludes.iter().map(|e| {
2458 let name = e.name.as_str();
2459 let using = e.using.as_str();
2460 let element_tokens = e.elements.iter().map(|(col, op)| {
2461 let col_s = col.as_str();
2462 let op_s = op.as_str();
2463 quote!((#col_s, #op_s))
2464 });
2465 let where_tokens = match e.where_clause.as_deref() {
2466 Some(w) => quote!(::core::option::Option::Some(#w)),
2467 None => quote!(::core::option::Option::None),
2468 };
2469 quote! {
2470 #root::core::ExclusionConstraint {
2471 name: #name,
2472 using: #using,
2473 elements: &[ #(#element_tokens),* ],
2474 where_clause: #where_tokens,
2475 }
2476 }
2477 });
2478 let composite_fk_tokens = composite_fks.iter().map(|rel| {
2479 let name = rel.name.as_str();
2480 let to = rel.to.as_str();
2481 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2482 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2483 quote! {
2484 #root::core::CompositeFkRelation {
2485 name: #name,
2486 to: #to,
2487 from: &[ #(#from_cols),* ],
2488 on: &[ #(#on_cols),* ],
2489 }
2490 }
2491 });
2492 let generic_fk_tokens = generic_fks.iter().map(|rel| {
2493 let name = rel.name.as_str();
2494 let ct_col = rel.ct_column.as_str();
2495 let pk_col = rel.pk_column.as_str();
2496 quote! {
2497 #root::core::GenericRelation {
2498 name: #name,
2499 ct_column: #ct_col,
2500 pk_column: #pk_col,
2501 }
2502 }
2503 });
2504 // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2505 // no `#[rustango(default_order = "...")]` attribute was supplied.
2506 let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2507 let col_lit = col.as_str();
2508 quote! { (#col_lit, #desc) }
2509 });
2510
2511 // Issue #820 — `global_scopes` slice literal. Empty when no
2512 // `#[rustango(global_scope(...))]` attribute was supplied. The
2513 // `apply` path is re-emitted verbatim so it resolves in the
2514 // consumer's scope; the name is stored as a string literal.
2515 let global_scope_tokens = global_scopes.iter().map(|s| {
2516 let name = s.name.as_str();
2517 let apply = &s.apply;
2518 quote! {
2519 #root::core::GlobalScope {
2520 name: #name,
2521 apply: #apply,
2522 }
2523 }
2524 });
2525
2526 let m2m_tokens = m2m_relations.iter().map(|rel| {
2527 let name = rel.name.as_str();
2528 let to = rel.to.as_str();
2529 let through = rel.through.as_str();
2530 let src = rel.src.as_str();
2531 let dst = rel.dst.as_str();
2532 let auto_create = rel.auto_create;
2533 quote! {
2534 #root::core::M2MRelation {
2535 name: #name,
2536 to: #to,
2537 through: #through,
2538 src_col: #src,
2539 dst_col: #dst,
2540 auto_create: #auto_create,
2541 }
2542 }
2543 });
2544 // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2545 // when the model declares `#[rustango(reverse_has(...))]`. Each
2546 // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2547 // a const expression. Models without reverse_has fall through to
2548 // the trait's empty default — no override emitted.
2549 let reverse_relations_override = if reverse_has_relations.is_empty() {
2550 quote!()
2551 } else {
2552 let entries = reverse_has_relations.iter().map(|rel| {
2553 let name = rel.name.as_str();
2554 let child = &rel.child;
2555 let child_fk_column = rel.child_fk_column.as_str();
2556 let self_pk_column = rel.self_pk_column.as_str();
2557 quote! {
2558 #root::core::ReverseRelation {
2559 name: #name,
2560 child_schema: <#child as #root::core::Model>::SCHEMA,
2561 child_fk_column: #child_fk_column,
2562 self_pk_column: #self_pk_column,
2563 }
2564 }
2565 });
2566 quote! {
2567 fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2568 const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2569 RELS
2570 }
2571 }
2572 };
2573 // Issue #830 — `Model::generic_reverse_relations()` override for
2574 // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2575 let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2576 quote!()
2577 } else {
2578 let entries = generic_has_relations.iter().map(|rel| {
2579 let name = rel.name.as_str();
2580 let child = &rel.child;
2581 let ct_column = rel.ct_column.as_str();
2582 let pk_column = rel.pk_column.as_str();
2583 let self_pk_column = rel.self_pk_column.as_str();
2584 quote! {
2585 #root::core::GenericReverseRelation {
2586 name: #name,
2587 child_schema: <#child as #root::core::Model>::SCHEMA,
2588 ct_column: #ct_column,
2589 pk_column: #pk_column,
2590 self_pk_column: #self_pk_column,
2591 }
2592 }
2593 });
2594 quote! {
2595 fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2596 const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2597 RELS
2598 }
2599 }
2600 };
2601 quote! {
2602 impl #root::core::Model for #struct_name {
2603 const SCHEMA: &'static #root::core::ModelSchema = &#root::core::ModelSchema {
2604 name: #model_name,
2605 table: #table,
2606 fields: &[ #(#field_schemas),* ],
2607 display: #display_tokens,
2608 app_label: #app_label_tokens,
2609 admin: #admin_tokens,
2610 soft_delete_column: #soft_delete_tokens,
2611 permissions: #permissions,
2612 audit_track: #audit_track_tokens,
2613 m2m: &[ #(#m2m_tokens),* ],
2614 indexes: &[ #(#indexes_tokens),* ],
2615 check_constraints: &[ #(#checks_tokens),* ],
2616 exclusion_constraints: &[ #(#excludes_tokens),* ],
2617 composite_relations: &[ #(#composite_fk_tokens),* ],
2618 generic_relations: &[ #(#generic_fk_tokens),* ],
2619 scope: #scope_tokens,
2620 default_order: &[ #(#default_order_tokens),* ],
2621 is_view: #is_view,
2622 verbose_name: #verbose_name_tokens,
2623 verbose_name_plural: #verbose_name_plural_tokens,
2624 managed: #managed,
2625 base_manager_name: #base_manager_name_tokens,
2626 order_with_respect_to: #order_with_respect_to_tokens,
2627 proxy: #proxy,
2628 required_db_features: &[ #(#required_db_features_lits),* ],
2629 required_db_vendor: #required_db_vendor_tokens,
2630 default_related_name: #default_related_name_tokens,
2631 db_table_comment: #db_table_comment_tokens,
2632 get_latest_by: #get_latest_by_tokens,
2633 extra_permissions: &[ #(#extra_permission_tokens),* ],
2634 default_permissions: &[ #(#default_permission_tokens),* ],
2635 global_scopes: &[ #(#global_scope_tokens),* ],
2636 };
2637
2638 #reverse_relations_override
2639 #generic_reverse_relations_override
2640 }
2641 }
2642}
2643
2644/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2645/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2646/// otherwise a static reference to a populated `AdminConfig`.
2647fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2648 let root = rustango_root();
2649 let Some(admin) = admin else {
2650 return quote!(::core::option::Option::None);
2651 };
2652
2653 let list_display = admin
2654 .list_display
2655 .as_ref()
2656 .map(|(v, _)| v.as_slice())
2657 .unwrap_or(&[]);
2658 let list_display_lits = list_display.iter().map(|s| s.as_str());
2659
2660 let search_fields = admin
2661 .search_fields
2662 .as_ref()
2663 .map(|(v, _)| v.as_slice())
2664 .unwrap_or(&[]);
2665 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2666
2667 let readonly_fields = admin
2668 .readonly_fields
2669 .as_ref()
2670 .map(|(v, _)| v.as_slice())
2671 .unwrap_or(&[]);
2672 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2673
2674 let list_filter = admin
2675 .list_filter
2676 .as_ref()
2677 .map(|(v, _)| v.as_slice())
2678 .unwrap_or(&[]);
2679 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2680
2681 let actions = admin
2682 .actions
2683 .as_ref()
2684 .map(|(v, _)| v.as_slice())
2685 .unwrap_or(&[]);
2686 let actions_lits = actions.iter().map(|s| s.as_str());
2687
2688 let fieldsets = admin
2689 .fieldsets
2690 .as_ref()
2691 .map(|(v, _)| v.as_slice())
2692 .unwrap_or(&[]);
2693 let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
2694 let title = title.as_str();
2695 let field_lits = fields.iter().map(|s| s.as_str());
2696 quote!(#root::core::Fieldset {
2697 title: #title,
2698 fields: &[ #( #field_lits ),* ],
2699 })
2700 });
2701
2702 let list_display_links = admin
2703 .list_display_links
2704 .as_ref()
2705 .map(|(v, _)| v.as_slice())
2706 .unwrap_or(&[]);
2707 let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2708
2709 let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2710 let actions_on_top = admin.actions_on_top.unwrap_or(true);
2711 let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2712 let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2713
2714 let prepopulated = admin
2715 .prepopulated_fields
2716 .as_ref()
2717 .map(|(v, _)| v.as_slice())
2718 .unwrap_or(&[]);
2719 let prepopulated_tokens = prepopulated.iter().map(|(target, sources)| {
2720 let target = target.as_str();
2721 let source_lits = sources.iter().map(|s| s.as_str());
2722 quote!(#root::core::PrepopulatedField {
2723 target: #target,
2724 sources: &[ #( #source_lits ),* ],
2725 })
2726 });
2727
2728 let raw_id_fields = admin
2729 .raw_id_fields
2730 .as_ref()
2731 .map(|(v, _)| v.as_slice())
2732 .unwrap_or(&[]);
2733 let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2734
2735 let autocomplete_fields = admin
2736 .autocomplete_fields
2737 .as_ref()
2738 .map(|(v, _)| v.as_slice())
2739 .unwrap_or(&[]);
2740 let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
2741
2742 // #352 — list_select_related accepts "all" | "none" | "field, field, …".
2743 let list_select_related_tokens = match admin.list_select_related.as_deref() {
2744 None | Some("all") => quote!(#root::core::ListSelectRelated::All),
2745 Some("none") => quote!(#root::core::ListSelectRelated::None),
2746 Some(raw) => {
2747 let names: Vec<&str> = raw
2748 .split(',')
2749 .map(str::trim)
2750 .filter(|s| !s.is_empty())
2751 .collect();
2752 quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
2753 }
2754 };
2755
2756 // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
2757 // a Vec<(String, String)>. Empty / unset → no overrides.
2758 let formfield_pairs: Vec<(&str, &str)> = admin
2759 .formfield_overrides
2760 .as_ref()
2761 .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
2762 .unwrap_or_default();
2763 let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
2764 let field = *field;
2765 let widget = *widget;
2766 quote!((#field, #widget))
2767 });
2768
2769 let list_per_page = admin.list_per_page.unwrap_or(0);
2770
2771 let ordering_pairs = admin
2772 .ordering
2773 .as_ref()
2774 .map(|(v, _)| v.as_slice())
2775 .unwrap_or(&[]);
2776 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
2777 let name = name.as_str();
2778 let desc = *desc;
2779 quote!((#name, #desc))
2780 });
2781
2782 quote! {
2783 ::core::option::Option::Some(&#root::core::AdminConfig {
2784 list_display: &[ #( #list_display_lits ),* ],
2785 search_fields: &[ #( #search_fields_lits ),* ],
2786 list_per_page: #list_per_page,
2787 ordering: &[ #( #ordering_tokens ),* ],
2788 readonly_fields: &[ #( #readonly_fields_lits ),* ],
2789 list_filter: &[ #( #list_filter_lits ),* ],
2790 actions: &[ #( #actions_lits ),* ],
2791 fieldsets: &[ #( #fieldset_tokens ),* ],
2792 list_display_links: &[ #( #list_display_links_lits ),* ],
2793 search_help_text: #search_help_text,
2794 actions_on_top: #actions_on_top,
2795 actions_on_bottom: #actions_on_bottom,
2796 date_hierarchy: #date_hierarchy,
2797 prepopulated_fields: &[ #( #prepopulated_tokens ),* ],
2798 raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
2799 autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
2800 list_select_related: #list_select_related_tokens,
2801 formfield_overrides: &[ #( #formfield_tokens ),* ],
2802 })
2803 }
2804}
2805
2806fn inherent_impl_tokens(
2807 struct_name: &syn::Ident,
2808 fields: &CollectedFields,
2809 primary_key: Option<&(syn::Ident, String)>,
2810 column_consts: &TokenStream2,
2811 audited_fields: Option<&[&ColumnEntry]>,
2812 indexes: &[IndexAttr],
2813 manager_fns: &[syn::Ident],
2814) -> TokenStream2 {
2815 let root = rustango_root();
2816 // Audit-emit fragments threaded into write paths. Non-empty only
2817 // when the model carries `#[rustango(audit(...))]`. They reborrow
2818 // `_executor` (a `&mut PgConnection` for audited models — the
2819 // macro switches the signature below) so the data write and the
2820 // audit INSERT both run on the same caller-supplied connection.
2821 let executor_passes_to_data_write = if audited_fields.is_some() {
2822 quote!(&mut *_executor)
2823 } else {
2824 quote!(_executor)
2825 };
2826 let executor_param = if audited_fields.is_some() {
2827 quote!(_executor: &mut #root::sql::sqlx::PgConnection)
2828 } else {
2829 quote!(_executor: _E)
2830 };
2831 let executor_generics = if audited_fields.is_some() {
2832 quote!()
2833 } else {
2834 quote!(<'_c, _E>)
2835 };
2836 let executor_where = if audited_fields.is_some() {
2837 quote!()
2838 } else {
2839 quote! {
2840 where
2841 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
2842 }
2843 };
2844 // For audited models the `_on` methods take `&mut PgConnection`, so
2845 // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
2846 // must acquire a connection first. Non-audited models keep the
2847 // direct delegation since `&PgPool` IS an Executor.
2848 let pool_to_save_on = if audited_fields.is_some() {
2849 quote! {
2850 let mut _conn = pool.acquire().await?;
2851 self.save_on(&mut *_conn).await
2852 }
2853 } else {
2854 quote!(self.save_on(pool).await)
2855 };
2856 let pool_to_insert_on = if audited_fields.is_some() {
2857 quote! {
2858 let mut _conn = pool.acquire().await?;
2859 self.insert_on(&mut *_conn).await
2860 }
2861 } else {
2862 quote!(self.insert_on(pool).await)
2863 };
2864 let pool_to_delete_on = if audited_fields.is_some() {
2865 quote! {
2866 let mut _conn = pool.acquire().await?;
2867 self.delete_on(&mut *_conn).await
2868 }
2869 } else {
2870 quote!(self.delete_on(pool).await)
2871 };
2872 let pool_to_bulk_insert_on = if audited_fields.is_some() {
2873 quote! {
2874 let mut _conn = pool.acquire().await?;
2875 Self::bulk_insert_on(rows, &mut *_conn).await
2876 }
2877 } else {
2878 quote!(Self::bulk_insert_on(rows, pool).await)
2879 };
2880 // Pre-existing bug surfaced by batch 22's first audited Auto<T>
2881 // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
2882 // directly, but `upsert_on` for audited models takes
2883 // `&mut PgConnection` (the audit emit needs a real connection).
2884 // Add the missing acquire shim to keep audited Auto-PK upsert
2885 // compiling.
2886 let pool_to_upsert_on = if audited_fields.is_some() {
2887 quote! {
2888 let mut _conn = pool.acquire().await?;
2889 self.upsert_on(&mut *_conn).await
2890 }
2891 } else {
2892 quote!(self.upsert_on(pool).await)
2893 };
2894
2895 // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
2896 // (audit-on-connection over &Pool needs a bi-dialect transaction
2897 // helper, deferred). Two body shapes:
2898 // - has_auto: build InsertQuery skipping Auto::Unset columns,
2899 // request Auto cols in `returning`, dispatch via
2900 // `insert_returning_pool`, then on the returned `PgRow` /
2901 // `MySqlAutoId(id)` enum — pull each Auto field from the PG
2902 // row OR drop the single i64 into the first Auto field on MySQL
2903 // (multi-Auto models on MySQL error at runtime since
2904 // `LAST_INSERT_ID()` only reports one)
2905 // - non-Auto: build InsertQuery with explicit columns/values and
2906 // call `insert_pool` (no returning needed)
2907 // pool_insert_method body for the audited Auto-PK case is moved
2908 // to after audit_pair_tokens / audit_pk_to_string (they live
2909 // ~150 lines below). This block keeps the non-audited and
2910 // non-Auto branches in place — the audited Auto-PK arm is
2911 // computed below and merged via the dispatch helper variable.
2912 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
2913 // Audited models with explicit (non-Auto) PKs go through
2914 // the non-Auto insert path below — the audit emit is one
2915 // round-trip after the INSERT inside the same tx via
2916 // audit::save_one_with_audit? No, INSERT semantics
2917 // differ. For non-Auto PK + audited, route through a
2918 // dedicated insert + audit emit on the same tx, but defer
2919 // the macro emission to the audit-bundle-aware block below
2920 // — this `quote!()` placeholder gets overwritten there.
2921 quote!()
2922 } else if audited_fields.is_some() && fields.has_auto {
2923 // Audited Auto-PK insert_pool — assembled after the audit
2924 // bundles. Placeholder; real emission below.
2925 quote!()
2926 } else if fields.has_auto {
2927 let pushes = &fields.insert_pushes;
2928 let returning_cols = &fields.returning_cols;
2929 // When every `Auto<T>` field is filled Rust-side
2930 // (`default_uuid_v7`, issue #823), there is no column to read
2931 // back from the database — `returning_cols` is empty. Route
2932 // through plain `insert_pool` instead of
2933 // `insert_returning_pool` to skip the redundant RETURNING /
2934 // LAST_INSERT_ID round-trip.
2935 if fields.returning_cols.is_empty() {
2936 quote! {
2937 /// Insert this row against either backend. Every
2938 /// `Auto<T>` PK on this model is filled Rust-side
2939 /// (e.g. `default_uuid_v7`) before binding, so no
2940 /// RETURNING round-trip is needed.
2941 ///
2942 /// # Errors
2943 /// As [`Self::insert`].
2944 pub async fn insert_pool(
2945 &mut self,
2946 pool: &#root::sql::Pool,
2947 ) -> ::core::result::Result<(), #root::sql::ExecError> {
2948 let mut _columns: ::std::vec::Vec<&'static str> =
2949 ::std::vec::Vec::new();
2950 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2951 ::std::vec::Vec::new();
2952 #( #pushes )*
2953 let _query = #root::core::InsertQuery {
2954 model: <Self as #root::core::Model>::SCHEMA,
2955 columns: _columns,
2956 values: _values,
2957 returning: ::std::vec::Vec::new(),
2958 on_conflict: ::core::option::Option::None,
2959 };
2960 #root::sql::insert_pool(pool, &_query).await
2961 }
2962
2963 /// Eloquent `Model::insertOrIgnore()` — same shape
2964 /// as the auto-PK branch above. Returns `Ok(true)`
2965 /// when inserted, `Ok(false)` when a conflict caused
2966 /// the INSERT to silently skip.
2967 ///
2968 /// # Errors
2969 /// As [`Self::insert`].
2970 pub async fn insert_or_ignore(
2971 &mut self,
2972 pool: &#root::sql::Pool,
2973 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
2974 let mut _columns: ::std::vec::Vec<&'static str> =
2975 ::std::vec::Vec::new();
2976 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2977 ::std::vec::Vec::new();
2978 #( #pushes )*
2979 let _query = #root::core::InsertQuery {
2980 model: <Self as #root::core::Model>::SCHEMA,
2981 columns: _columns,
2982 values: _values,
2983 returning: ::std::vec::Vec::new(),
2984 on_conflict: ::core::option::Option::Some(
2985 #root::core::ConflictClause::DoNothing,
2986 ),
2987 };
2988 let dialect = pool.dialect();
2989 let stmt = dialect.compile_insert(&_query)?;
2990 let rows = #root::sql::raw_execute_pool(
2991 pool, &stmt.sql, stmt.params,
2992 ).await?;
2993 ::core::result::Result::Ok(rows > 0)
2994 }
2995 }
2996 } else {
2997 quote! {
2998 /// Insert this row against either backend, populating any
2999 /// `Auto<T>` PK from the auto-assigned value.
3000 ///
3001 /// # Errors
3002 /// As [`Self::insert`].
3003 pub async fn insert_pool(
3004 &mut self,
3005 pool: &#root::sql::Pool,
3006 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3007 let mut _columns: ::std::vec::Vec<&'static str> =
3008 ::std::vec::Vec::new();
3009 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3010 ::std::vec::Vec::new();
3011 #( #pushes )*
3012 let _query = #root::core::InsertQuery {
3013 model: <Self as #root::core::Model>::SCHEMA,
3014 columns: _columns,
3015 values: _values,
3016 returning: ::std::vec![ #( #returning_cols ),* ],
3017 on_conflict: ::core::option::Option::None,
3018 };
3019 let _result = #root::sql::insert_returning_pool(
3020 pool, &_query,
3021 ).await?;
3022 #root::sql::apply_auto_pk(_result, self)
3023 }
3024
3025 /// Eloquent `Model::insertOrIgnore()` — INSERT this
3026 /// row or silently skip on unique-constraint
3027 /// violation. Returns `Ok(true)` when a row was
3028 /// inserted, `Ok(false)` when a conflict caused the
3029 /// INSERT to silently skip.
3030 ///
3031 /// **Caveat on auto-PK models**: when the row is
3032 /// skipped (conflict), this instance's `Auto<T>`
3033 /// fields stay `Unset` — no PK is back-populated
3034 /// because the server didn't auto-assign one. For
3035 /// "insert then read back the PK or the existing
3036 /// row's PK", use the `upsert` family or
3037 /// `get_or_create`.
3038 ///
3039 /// # Errors
3040 /// As [`Self::insert`].
3041 pub async fn insert_or_ignore(
3042 &mut self,
3043 pool: &#root::sql::Pool,
3044 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3045 let mut _columns: ::std::vec::Vec<&'static str> =
3046 ::std::vec::Vec::new();
3047 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3048 ::std::vec::Vec::new();
3049 #( #pushes )*
3050 let _query = #root::core::InsertQuery {
3051 model: <Self as #root::core::Model>::SCHEMA,
3052 columns: _columns,
3053 values: _values,
3054 returning: ::std::vec::Vec::new(),
3055 on_conflict: ::core::option::Option::Some(
3056 #root::core::ConflictClause::DoNothing,
3057 ),
3058 };
3059 let dialect = pool.dialect();
3060 let stmt = dialect.compile_insert(&_query)?;
3061 let rows = #root::sql::raw_execute_pool(
3062 pool, &stmt.sql, stmt.params,
3063 ).await?;
3064 ::core::result::Result::Ok(rows > 0)
3065 }
3066 }
3067 }
3068 } else {
3069 let insert_columns = &fields.insert_columns;
3070 let insert_values = &fields.insert_values;
3071 quote! {
3072 /// Insert this row into its table against either backend.
3073 /// Equivalent to [`Self::insert`] but takes
3074 /// [`#root::sql::Pool`].
3075 ///
3076 /// # Errors
3077 /// As [`Self::insert`].
3078 pub async fn insert_pool(
3079 &self,
3080 pool: &#root::sql::Pool,
3081 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3082 let _query = #root::core::InsertQuery {
3083 model: <Self as #root::core::Model>::SCHEMA,
3084 columns: ::std::vec![ #( #insert_columns ),* ],
3085 values: ::std::vec![ #( #insert_values ),* ],
3086 returning: ::std::vec::Vec::new(),
3087 on_conflict: ::core::option::Option::None,
3088 };
3089 #root::sql::insert_pool(pool, &_query).await
3090 }
3091
3092 /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3093 /// or silently skip on unique-constraint violation. Maps
3094 /// to per-dialect "INSERT ... DO NOTHING on conflict":
3095 /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3096 /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL
3097 /// `INSERT IGNORE INTO …`.
3098 ///
3099 /// Returns `Ok(true)` when a row was inserted,
3100 /// `Ok(false)` when a conflict caused the INSERT to
3101 /// silently skip.
3102 ///
3103 /// Use for "create-if-absent" patterns where you don't
3104 /// need the row back. For "find-or-create with the row
3105 /// returned", use the queryset-level
3106 /// `crate::sql::get_or_create` free function.
3107 ///
3108 /// # Errors
3109 /// As [`Self::insert`], plus any dialect-specific
3110 /// translation error from the ConflictClause writer.
3111 pub async fn insert_or_ignore(
3112 &self,
3113 pool: &#root::sql::Pool,
3114 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3115 let _query = #root::core::InsertQuery {
3116 model: <Self as #root::core::Model>::SCHEMA,
3117 columns: ::std::vec![ #( #insert_columns ),* ],
3118 values: ::std::vec![ #( #insert_values ),* ],
3119 returning: ::std::vec::Vec::new(),
3120 on_conflict: ::core::option::Option::Some(
3121 #root::core::ConflictClause::DoNothing,
3122 ),
3123 };
3124 let dialect = pool.dialect();
3125 let stmt = dialect.compile_insert(&_query)?;
3126 let rows = #root::sql::raw_execute_pool(pool, &stmt.sql, stmt.params).await?;
3127 ::core::result::Result::Ok(rows > 0)
3128 }
3129 }
3130 };
3131
3132 // pool_save_method moved to after audit_pair_tokens /
3133 // audit_pk_to_string (they live ~70 lines below) — needed for
3134 // the audited branch which builds an UpdateQuery + PendingEntry
3135 // and dispatches via audit::save_one_with_audit.
3136
3137 // pool_delete_method moved to after audit_pair_tokens / audit_pk_to_string
3138 // are computed (they live ~80 lines below).
3139
3140 // Build the (column, JSON value) pair list used by every
3141 // snapshot-style audit emission. Reused across delete_on,
3142 // soft_delete_on, restore_on, and (later) bulk paths. Empty
3143 // when the model isn't audited.
3144 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3145 .map(|tracked| {
3146 tracked
3147 .iter()
3148 .map(|c| {
3149 let column_lit = c.column.as_str();
3150 let ident = &c.ident;
3151 quote! {
3152 (
3153 #column_lit,
3154 #root::__serde_json::to_value(&self.#ident)
3155 .unwrap_or(#root::__serde_json::Value::Null),
3156 )
3157 }
3158 })
3159 .collect()
3160 })
3161 .unwrap_or_default();
3162 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3163 if fields.pk_is_auto {
3164 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3165 } else {
3166 quote!(::std::format!("{}", &self.#pk_ident))
3167 }
3168 } else {
3169 quote!(::std::string::String::new())
3170 };
3171 let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
3172 if audited_fields.is_some() {
3173 let pairs = audit_pair_tokens.iter();
3174 let pk_str = audit_pk_to_string.clone();
3175 quote! {
3176 let _audit_entry = #root::audit::PendingEntry {
3177 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3178 entity_pk: #pk_str,
3179 operation: #op_path,
3180 source: #root::audit::current_source(),
3181 changes: #root::audit::snapshot_changes(&[
3182 #( #pairs ),*
3183 ]),
3184 };
3185 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3186 }
3187 } else {
3188 quote!()
3189 }
3190 };
3191 let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create));
3192 let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete));
3193 let audit_softdelete_emit = make_op_emit(quote!(#root::audit::AuditOp::SoftDelete));
3194 let audit_restore_emit = make_op_emit(quote!(#root::audit::AuditOp::Restore));
3195
3196 // `save_pool(&Pool)` — emitted for every model with a PK.
3197 // Audited Auto-PK models are deferred (the Auto::Unset →
3198 // insert_pool path needs the audited-insert flow from a future
3199 // batch). Three body shapes:
3200 // - non-audited, plain PK: build UpdateQuery + dispatch through
3201 // sql::update_pool
3202 // - non-audited, Auto-PK: same, but Auto::Unset routes to
3203 // self.insert_pool which already handles RETURNING / LAST_INSERT_ID
3204 // - audited, plain PK: build UpdateQuery + PendingEntry, dispatch
3205 // through audit::save_one_with_audit (per-backend tx wraps
3206 // UPDATE + audit emit atomically). Snapshot-style audit (post-
3207 // write field values) — diff-style audit (with pre-UPDATE
3208 // SELECT for `before` values) needs per-tracked-column codegen
3209 // that doesn't fit the runtime-helper pattern; legacy &PgPool
3210 // `save` keeps the diff for now.
3211 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3212 let pk_column_lit = pk_col.as_str();
3213 let assignments = &fields.update_assignments;
3214 if audited_fields.is_some() {
3215 if fields.pk_is_auto {
3216 // Auto-PK + audited: defer. The Auto::Unset insert
3217 // path needs a transactional INSERT + LAST_INSERT_ID
3218 // + audit emit flow — that's a follow-up batch.
3219 quote!()
3220 } else {
3221 let pairs = audit_pair_tokens.iter();
3222 let pairs2 = audit_pair_tokens.iter();
3223 let pk_str = audit_pk_to_string.clone();
3224 let pk_str2 = audit_pk_to_string.clone();
3225 quote! {
3226 /// Save (UPDATE) this row against either backend
3227 /// with audit emission inside the same transaction.
3228 /// Bi-dialect counterpart of [`Self::save`] for
3229 /// audited models with non-`Auto<T>` PKs.
3230 ///
3231 /// Captures **post-write** field state (snapshot
3232 /// audit). The legacy &PgPool [`Self::save`]
3233 /// captures BEFORE+AFTER for true diff audit;
3234 /// porting that to the &Pool path needs runtime
3235 /// per-tracked-column decoding and is deferred.
3236 ///
3237 /// # Errors
3238 /// As [`Self::save`].
3239 pub async fn save_pool(
3240 &mut self,
3241 pool: &#root::sql::Pool,
3242 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3243 let _query = #root::core::UpdateQuery {
3244 model: <Self as #root::core::Model>::SCHEMA,
3245 set: ::std::vec![ #( #assignments ),* ],
3246 where_clause: #root::core::WhereExpr::Predicate(
3247 #root::core::Filter {
3248 column: #pk_column_lit,
3249 op: #root::core::Op::Eq,
3250 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3251 ::core::clone::Clone::clone(&self.#pk_ident)
3252 ),
3253 }
3254 ),
3255 };
3256 let _audit_entry = #root::audit::PendingEntry {
3257 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3258 entity_pk: #pk_str,
3259 operation: #root::audit::AuditOp::Update,
3260 source: #root::audit::current_source(),
3261 changes: #root::audit::snapshot_changes(&[
3262 #( #pairs ),*
3263 ]),
3264 };
3265 let _affected = #root::audit::save_one_with_audit(
3266 pool, &_query, &_audit_entry,
3267 ).await?;
3268 ::core::result::Result::Ok(_affected)
3269 }
3270
3271 /// `save_pool` narrowed to a Rust-field allowlist — issue #66
3272 /// (Django `Model.save(update_fields=[...])`).
3273 /// Audit emission shrinks to the same column set so
3274 /// the audit log reflects exactly what was written.
3275 ///
3276 /// # Errors
3277 /// As [`Self::save_pool`], plus
3278 /// [`#root::core::QueryError::UnknownField`] wrapped
3279 /// in `ExecError::Query` for unknown field names.
3280 pub async fn save_partial(
3281 &mut self,
3282 fields: &[&str],
3283 pool: &#root::sql::Pool,
3284 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3285 if fields.is_empty() {
3286 #root::__tracing::warn!(
3287 target: "rustango::save_partial",
3288 model = <Self as #root::core::Model>::SCHEMA.name,
3289 "save_partial called with empty field list — no-op"
3290 );
3291 return ::core::result::Result::Ok(0);
3292 }
3293 let _schema = <Self as #root::core::Model>::SCHEMA;
3294 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3295 ::std::collections::HashSet::with_capacity(fields.len());
3296 for f in fields {
3297 match _schema.field(f) {
3298 ::core::option::Option::Some(fs) => {
3299 _wanted_cols.insert(fs.column);
3300 }
3301 ::core::option::Option::None => {
3302 return ::core::result::Result::Err(
3303 #root::sql::ExecError::Query(
3304 #root::core::QueryError::UnknownField {
3305 model: _schema.name,
3306 field: (*f).to_owned(),
3307 }
3308 )
3309 );
3310 }
3311 }
3312 }
3313 let _full: ::std::vec::Vec<#root::core::Assignment> =
3314 ::std::vec![ #( #assignments ),* ];
3315 let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3316 .into_iter()
3317 .filter(|a| _wanted_cols.contains(a.column))
3318 .collect();
3319 if _filtered.is_empty() {
3320 #root::__tracing::warn!(
3321 target: "rustango::save_partial",
3322 model = _schema.name,
3323 "save_partial: every named field maps to a non-assignable column — no-op"
3324 );
3325 return ::core::result::Result::Ok(0);
3326 }
3327 let _query = #root::core::UpdateQuery {
3328 model: _schema,
3329 set: _filtered,
3330 where_clause: #root::core::WhereExpr::Predicate(
3331 #root::core::Filter {
3332 column: #pk_column_lit,
3333 op: #root::core::Op::Eq,
3334 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3335 ::core::clone::Clone::clone(&self.#pk_ident)
3336 ),
3337 }
3338 ),
3339 };
3340 // Narrow the audit snapshot to the same column set.
3341 let _all_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3342 ::std::vec![ #( #pairs2 ),* ];
3343 let _narrowed: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3344 _all_pairs
3345 .into_iter()
3346 .filter(|(col, _)| _wanted_cols.contains(col))
3347 .collect();
3348 let _audit_entry = #root::audit::PendingEntry {
3349 entity_table: _schema.table,
3350 entity_pk: #pk_str2,
3351 operation: #root::audit::AuditOp::Update,
3352 source: #root::audit::current_source(),
3353 changes: #root::audit::snapshot_changes(&_narrowed),
3354 };
3355 let _affected = #root::audit::save_one_with_audit(
3356 pool, &_query, &_audit_entry,
3357 ).await?;
3358 ::core::result::Result::Ok(_affected)
3359 }
3360
3361 /// Typed-column counterpart of [`Self::save_partial`] —
3362 /// issue #67. `fields` is a tuple of [`Column`]
3363 /// constants whose `Model` matches `Self`; typos and
3364 /// model mismatches surface at *compile time*
3365 /// (`Author::name` inside a `Post::save_partial_typed`
3366 /// call is a type error, no runtime check).
3367 ///
3368 /// ```ignore
3369 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3370 /// ```
3371 ///
3372 /// Lowers to [`Self::save_partial`] under the hood;
3373 /// audit narrowing + every other semantic is identical.
3374 ///
3375 /// [`Column`]: #root::core::Column
3376 ///
3377 /// # Errors
3378 /// As [`Self::save_partial`].
3379 pub async fn save_partial_typed<
3380 L: #root::core::TypedFieldList<Self>,
3381 >(
3382 &mut self,
3383 fields: L,
3384 pool: &#root::sql::Pool,
3385 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3386 let _names = fields.rust_field_names();
3387 let _refs: ::std::vec::Vec<&str> =
3388 _names.iter().copied().collect();
3389 self.save_partial(&_refs, pool).await
3390 }
3391 }
3392 }
3393 } else {
3394 let dispatch_unset = if fields.pk_is_auto {
3395 quote! {
3396 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3397 return self.insert_pool(pool).await.map(|()| 1u64);
3398 }
3399 }
3400 } else {
3401 quote!()
3402 };
3403 quote! {
3404 /// Save this row to its table against either backend.
3405 /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3406 /// `UPDATE` keyed on the PK.
3407 ///
3408 /// # Errors
3409 /// As [`Self::save`].
3410 pub async fn save_pool(
3411 &mut self,
3412 pool: &#root::sql::Pool,
3413 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3414 #dispatch_unset
3415 let _query = #root::core::UpdateQuery {
3416 model: <Self as #root::core::Model>::SCHEMA,
3417 set: ::std::vec![ #( #assignments ),* ],
3418 where_clause: #root::core::WhereExpr::Predicate(
3419 #root::core::Filter {
3420 column: #pk_column_lit,
3421 op: #root::core::Op::Eq,
3422 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3423 ::core::clone::Clone::clone(&self.#pk_ident)
3424 ),
3425 }
3426 ),
3427 };
3428 let _affected = #root::sql::update_pool(pool, &_query).await?;
3429 ::core::result::Result::Ok(_affected)
3430 }
3431
3432 /// Save (UPDATE) only the listed Rust-side fields,
3433 /// leaving every other column untouched. Issue #66 —
3434 /// Django's `Model.save(update_fields=[...])` shape.
3435 ///
3436 /// `fields` are Rust-side struct field names; the macro
3437 /// resolves each to its SQL column. Unknown field
3438 /// names return [`#root::core::QueryError::UnknownField`]
3439 /// wrapped in `ExecError::Query`. An empty list is a
3440 /// no-op (returns `Ok(())` and logs a `tracing::warn!`),
3441 /// matching Django's "nothing to do" semantic.
3442 ///
3443 /// Use this when:
3444 /// * you only mutated a couple of fields on a wide row
3445 /// (avoid re-writing every column on every save), or
3446 /// * two writers diverged after their initial read and
3447 /// you want to preserve the other writer's changes to
3448 /// columns you didn't touch.
3449 ///
3450 /// Auto-PK models with an unset PK return
3451 /// [`#root::core::QueryError::UnknownField`] with
3452 /// field name `<pk>` — `save_partial` is an
3453 /// UPDATE-only path. Call [`Self::insert_pool`]
3454 /// (or [`Self::save_pool`] which dispatches based on
3455 /// PK state) for the INSERT case.
3456 ///
3457 /// # Errors
3458 /// As [`Self::save_pool`], plus `UnknownField` for
3459 /// unknown / empty / Auto-Unset cases.
3460 pub async fn save_partial(
3461 &mut self,
3462 fields: &[&str],
3463 pool: &#root::sql::Pool,
3464 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3465 if fields.is_empty() {
3466 #root::__tracing::warn!(
3467 target: "rustango::save_partial",
3468 model = <Self as #root::core::Model>::SCHEMA.name,
3469 "save_partial called with empty field list — no-op"
3470 );
3471 return ::core::result::Result::Ok(0);
3472 }
3473 let _schema = <Self as #root::core::Model>::SCHEMA;
3474 // Validate field names against the schema.
3475 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3476 ::std::collections::HashSet::with_capacity(fields.len());
3477 for f in fields {
3478 match _schema.field(f) {
3479 ::core::option::Option::Some(fs) => {
3480 _wanted_cols.insert(fs.column);
3481 }
3482 ::core::option::Option::None => {
3483 return ::core::result::Result::Err(
3484 #root::sql::ExecError::Query(
3485 #root::core::QueryError::UnknownField {
3486 model: _schema.name,
3487 field: (*f).to_owned(),
3488 }
3489 )
3490 );
3491 }
3492 }
3493 }
3494 // Build the full assignment vec, then keep only the
3495 // assignments whose column is in `_wanted_cols`.
3496 let _full: ::std::vec::Vec<#root::core::Assignment> =
3497 ::std::vec![ #( #assignments ),* ];
3498 let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3499 .into_iter()
3500 .filter(|a| _wanted_cols.contains(a.column))
3501 .collect();
3502 if _filtered.is_empty() {
3503 // All field names valid, but they all map to
3504 // non-assignable slots (PK column, computed/
3505 // virtual fields, relations without an
3506 // assignment). Same no-op semantic as Django.
3507 #root::__tracing::warn!(
3508 target: "rustango::save_partial",
3509 model = _schema.name,
3510 "save_partial: every named field maps to a non-assignable column — no-op"
3511 );
3512 return ::core::result::Result::Ok(0);
3513 }
3514 let _query = #root::core::UpdateQuery {
3515 model: _schema,
3516 set: _filtered,
3517 where_clause: #root::core::WhereExpr::Predicate(
3518 #root::core::Filter {
3519 column: #pk_column_lit,
3520 op: #root::core::Op::Eq,
3521 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3522 ::core::clone::Clone::clone(&self.#pk_ident)
3523 ),
3524 }
3525 ),
3526 };
3527 let _affected = #root::sql::update_pool(pool, &_query).await?;
3528 ::core::result::Result::Ok(_affected)
3529 }
3530
3531 /// Typed-column counterpart of [`Self::save_partial`] —
3532 /// issue #67. `fields` is a tuple of [`Column`]
3533 /// constants whose `Model` matches `Self`; typos and
3534 /// model mismatches surface at *compile time*
3535 /// (`Author::name` inside a `Post::save_partial_typed`
3536 /// call is a type error, no runtime check).
3537 ///
3538 /// ```ignore
3539 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3540 /// ```
3541 ///
3542 /// Lowers to [`Self::save_partial`] under the hood — the
3543 /// tuple is reduced to a `&[&str]` slice of Rust-side
3544 /// field names and forwarded.
3545 ///
3546 /// [`Column`]: #root::core::Column
3547 ///
3548 /// # Errors
3549 /// As [`Self::save_partial`].
3550 pub async fn save_partial_typed<
3551 L: #root::core::TypedFieldList<Self>,
3552 >(
3553 &mut self,
3554 fields: L,
3555 pool: &#root::sql::Pool,
3556 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3557 let _names = fields.rust_field_names();
3558 let _refs: ::std::vec::Vec<&str> =
3559 _names.iter().copied().collect();
3560 self.save_partial(&_refs, pool).await
3561 }
3562 }
3563 }
3564 } else {
3565 quote!()
3566 };
3567
3568 // Audited `insert_pool` (overrides the placeholder set higher up
3569 // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3570 // audited models get insert_pool routing through
3571 // audit::insert_one_with_audit (per-backend tx wraps INSERT
3572 // + auto-PK readback + audit emit). Snapshot-style audit (the
3573 // PendingEntry's `changes` carries post-write field values).
3574 let pool_insert_method = if audited_fields.is_some() {
3575 if let Some(_) = primary_key {
3576 let pushes = if fields.has_auto {
3577 fields.insert_pushes.clone()
3578 } else {
3579 // For non-Auto-PK models, the macro normally builds
3580 // {columns, values} from fields.insert_columns +
3581 // fields.insert_values rather than insert_pushes.
3582 // Map those into the pushes shape.
3583 fields
3584 .insert_columns
3585 .iter()
3586 .zip(&fields.insert_values)
3587 .map(|(col, val)| {
3588 quote! {
3589 _columns.push(#col);
3590 _values.push(#val);
3591 }
3592 })
3593 .collect()
3594 };
3595 let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
3596 fields.returning_cols.clone()
3597 } else {
3598 // Non-Auto-PK: still need RETURNING something for the
3599 // audit helper's contract (it errors on empty
3600 // returning). Return the PK column so the audit row
3601 // can carry the assigned PK back. Some non-Auto PKs
3602 // are server-side-default (e.g. UUIDv4 default), so
3603 // RETURNING is genuinely useful.
3604 primary_key
3605 .map(|(_, col)| {
3606 let lit = col.as_str();
3607 vec![quote!(#lit)]
3608 })
3609 .unwrap_or_default()
3610 };
3611 let pairs = audit_pair_tokens.iter();
3612 let pk_str = audit_pk_to_string.clone();
3613 quote! {
3614 /// Insert this row against either backend with audit
3615 /// emission inside the same transaction. Bi-dialect
3616 /// counterpart of [`Self::insert`] for audited models.
3617 ///
3618 /// Snapshot-style audit (post-write field values).
3619 ///
3620 /// # Errors
3621 /// As [`Self::insert`].
3622 pub async fn insert_pool(
3623 &mut self,
3624 pool: &#root::sql::Pool,
3625 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3626 let mut _columns: ::std::vec::Vec<&'static str> =
3627 ::std::vec::Vec::new();
3628 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3629 ::std::vec::Vec::new();
3630 #( #pushes )*
3631 let _query = #root::core::InsertQuery {
3632 model: <Self as #root::core::Model>::SCHEMA,
3633 columns: _columns,
3634 values: _values,
3635 returning: ::std::vec![ #( #returning_cols ),* ],
3636 on_conflict: ::core::option::Option::None,
3637 };
3638 let _audit_entry = #root::audit::PendingEntry {
3639 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3640 entity_pk: #pk_str,
3641 operation: #root::audit::AuditOp::Create,
3642 source: #root::audit::current_source(),
3643 changes: #root::audit::snapshot_changes(&[
3644 #( #pairs ),*
3645 ]),
3646 };
3647 let _result = #root::audit::insert_one_with_audit(
3648 pool, &_query, &_audit_entry,
3649 ).await?;
3650 #root::sql::apply_auto_pk(_result, self)
3651 }
3652 }
3653 } else {
3654 quote!()
3655 }
3656 } else {
3657 // Keep the non-audited pool_insert_method we built earlier.
3658 pool_insert_method
3659 };
3660
3661 // Update audited save_pool: now that insert_pool is wired for
3662 // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3663 // insert_pool. Non-audited save_pool already does this.
3664 // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3665 // Replaces the snapshot-only emission with a per-backend transaction
3666 // body that:
3667 // 1. SELECTs the tracked columns by PK (typed Row::try_get per
3668 // column), capturing BEFORE values
3669 // 2. compiles the UPDATE via pool.dialect() and runs it on the tx
3670 // 3. builds AFTER pairs from &self
3671 // 4. diffs BEFORE/AFTER, emits one PendingEntry with
3672 // AuditOp::Update + diff_changes(...) on the same tx connection
3673 // 5. commits
3674 //
3675 // Per-backend arms inline the SQL string + placeholder shape, then
3676 // share the `audit_before_pair_tokens` decoder block (Row::try_get
3677 // is polymorphic over Row type — the same tokens work against
3678 // PgRow and MySqlRow as long as the field's Rust type implements
3679 // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3680 // primitives + chrono/uuid/serde_json::Value all do).
3681 let pool_save_method = if let Some(tracked) = audited_fields {
3682 if let Some((pk_ident, pk_col)) = primary_key {
3683 let pk_column_lit = pk_col.as_str();
3684 // Two iterators — quote!'s `#(#var)*` consumes the
3685 // iterator, and we need to splice the same after-pairs
3686 // sequence into both per-backend arms.
3687 let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
3688 let pk_str = audit_pk_to_string.clone();
3689 // Per-tracked-column BEFORE-pair token list. Each entry
3690 // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3691 // The Row alias resolves to PgRow / MySqlRow per call site,
3692 // so the same template generates both the PG and MySQL bodies.
3693 let mk_before_pairs =
3694 |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
3695 tracked
3696 .iter()
3697 .map(|c| {
3698 let column_lit = c.column.as_str();
3699 let value_ty = &c.value_ty;
3700 quote! {
3701 (
3702 #column_lit,
3703 match #getter::<#value_ty>(
3704 _audit_before_row, #column_lit,
3705 ) {
3706 ::core::result::Result::Ok(v) => {
3707 #root::__serde_json::to_value(&v)
3708 .unwrap_or(#root::__serde_json::Value::Null)
3709 }
3710 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3711 },
3712 )
3713 }
3714 })
3715 .collect()
3716 };
3717 let before_pairs_pg: Vec<proc_macro2::TokenStream> =
3718 mk_before_pairs(quote!(#root::sql::try_get_returning));
3719 let before_pairs_my: Vec<proc_macro2::TokenStream> =
3720 mk_before_pairs(quote!(#root::sql::try_get_returning_my));
3721 let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
3722 mk_before_pairs(quote!(#root::sql::try_get_returning_sqlite));
3723 let pg_select_cols: String = tracked
3724 .iter()
3725 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
3726 .collect::<Vec<_>>()
3727 .join(", ");
3728 let my_select_cols: String = tracked
3729 .iter()
3730 .map(|c| format!("`{}`", c.column.replace('`', "``")))
3731 .collect::<Vec<_>>()
3732 .join(", ");
3733 // SQLite uses double-quote identifier quoting (same as
3734 // Postgres in default config), so the column-list shape
3735 // matches PG.
3736 let sqlite_select_cols: String = pg_select_cols.clone();
3737 let pk_value_for_bind = if fields.pk_is_auto {
3738 quote!(self.#pk_ident.get().copied().unwrap_or_default())
3739 } else {
3740 quote!(::core::clone::Clone::clone(&self.#pk_ident))
3741 };
3742 let assignments = &fields.update_assignments;
3743 let unset_dispatch = if fields.has_auto {
3744 quote! {
3745 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3746 return self.insert_pool(pool).await.map(|()| 1u64);
3747 }
3748 }
3749 } else {
3750 quote!()
3751 };
3752 quote! {
3753 /// Save this row against either backend with audit
3754 /// emission (diff-style: BEFORE+AFTER) inside the
3755 /// same transaction. Auto::Unset PK routes to
3756 /// insert_pool. Bi-dialect counterpart of
3757 /// [`Self::save`] for audited models.
3758 ///
3759 /// The audit row's `changes` JSON contains one
3760 /// `{ "field": { "before": …, "after": … } }` entry
3761 /// per tracked column whose value actually changed
3762 /// — same shape as the existing &PgPool save() emits.
3763 ///
3764 /// # Errors
3765 /// As [`Self::save`].
3766 pub async fn save_pool(
3767 &mut self,
3768 pool: &#root::sql::Pool,
3769 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3770 #unset_dispatch
3771 let _query = #root::core::UpdateQuery {
3772 model: <Self as #root::core::Model>::SCHEMA,
3773 set: ::std::vec![ #( #assignments ),* ],
3774 where_clause: #root::core::WhereExpr::Predicate(
3775 #root::core::Filter {
3776 column: #pk_column_lit,
3777 op: #root::core::Op::Eq,
3778 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3779 ::core::clone::Clone::clone(&self.#pk_ident)
3780 ),
3781 }
3782 ),
3783 };
3784 let _after_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3785 ::std::vec![ #( #after_pairs_pg ),* ];
3786 #root::audit::save_one_with_diff(
3787 pool,
3788 &_query,
3789 #pk_column_lit,
3790 ::core::convert::Into::<#root::core::SqlValue>::into(
3791 #pk_value_for_bind,
3792 ),
3793 <Self as #root::core::Model>::SCHEMA.table,
3794 #pk_str,
3795 _after_pairs,
3796 #pg_select_cols,
3797 #my_select_cols,
3798 #sqlite_select_cols,
3799 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
3800 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
3801 |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
3802 ).await
3803 }
3804 }
3805 } else {
3806 quote!()
3807 }
3808 } else {
3809 pool_save_method
3810 };
3811
3812 // `delete_pool(&Pool)` — emitted for every model with a PK. Two
3813 // body shapes:
3814 // - non-audited: simple dispatch through `sql::delete_pool`
3815 // - audited: routes through `audit::delete_one_with_audit`,
3816 // which opens a per-backend transaction wrapping DELETE +
3817 // audit emit so the data write and audit row commit atomically.
3818 let pool_delete_method = {
3819 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
3820 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
3821 if let Some(pk_ident) = pk_ident_for_pool {
3822 if audited_fields.is_some() {
3823 let pairs = audit_pair_tokens.iter();
3824 let pk_str = audit_pk_to_string.clone();
3825 quote! {
3826 /// Delete this row against either backend with audit
3827 /// emission inside the same transaction. Bi-dialect
3828 /// counterpart of [`Self::delete`] for audited models.
3829 ///
3830 /// # Errors
3831 /// As [`Self::delete`].
3832 pub async fn delete_pool(
3833 &self,
3834 pool: &#root::sql::Pool,
3835 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3836 let _query = #root::core::DeleteQuery {
3837 model: <Self as #root::core::Model>::SCHEMA,
3838 where_clause: #root::core::WhereExpr::Predicate(
3839 #root::core::Filter {
3840 column: #pk_column_lit,
3841 op: #root::core::Op::Eq,
3842 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3843 ::core::clone::Clone::clone(&self.#pk_ident)
3844 ),
3845 }
3846 ),
3847 };
3848 let _audit_entry = #root::audit::PendingEntry {
3849 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3850 entity_pk: #pk_str,
3851 operation: #root::audit::AuditOp::Delete,
3852 source: #root::audit::current_source(),
3853 changes: #root::audit::snapshot_changes(&[
3854 #( #pairs ),*
3855 ]),
3856 };
3857 #root::audit::delete_one_with_audit(
3858 pool, &_query, &_audit_entry,
3859 ).await
3860 }
3861 }
3862 } else {
3863 quote! {
3864 /// Delete the row identified by this instance's primary key
3865 /// against either backend. Equivalent to [`Self::delete`] but
3866 /// takes [`#root::sql::Pool`] and dispatches per backend.
3867 ///
3868 /// # Errors
3869 /// As [`Self::delete`].
3870 pub async fn delete_pool(
3871 &self,
3872 pool: &#root::sql::Pool,
3873 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3874 let _query = #root::core::DeleteQuery {
3875 model: <Self as #root::core::Model>::SCHEMA,
3876 where_clause: #root::core::WhereExpr::Predicate(
3877 #root::core::Filter {
3878 column: #pk_column_lit,
3879 op: #root::core::Op::Eq,
3880 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3881 ::core::clone::Clone::clone(&self.#pk_ident)
3882 ),
3883 }
3884 ),
3885 };
3886 #root::sql::delete_pool(pool, &_query).await
3887 }
3888 }
3889 }
3890 } else {
3891 quote!()
3892 }
3893 };
3894
3895 // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
3896 // matching this instance's PK and overwrite the in-memory state
3897 // with the freshly-fetched columns. Django's `refresh_from_db`.
3898 // Issue #825. Only emitted when the model declares a PK; non-PK
3899 // models can't address a specific row.
3900 //
3901 // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
3902 // every field from `self`; resets the PK to `Auto::Unset` when
3903 // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
3904 // a fresh autoincrement value. Non-Auto PKs preserve the source
3905 // PK — the caller must overwrite before insert. Pure-Rust, no
3906 // I/O, no dialect surface.
3907 let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
3908 let other_field_clones: Vec<TokenStream2> = fields
3909 .column_entries
3910 .iter()
3911 .filter(|c| &c.ident != pk_ident)
3912 .map(|c| {
3913 let ident = &c.ident;
3914 quote! {
3915 #ident: ::core::clone::Clone::clone(&self.#ident)
3916 }
3917 })
3918 .collect();
3919 let pk_clone_token = if fields.pk_is_auto {
3920 quote! { #pk_ident: #root::sql::Auto::Unset }
3921 } else {
3922 quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
3923 };
3924 let replicate_doc = if fields.pk_is_auto {
3925 quote! {
3926 /// Eloquent-style `replicate()` — return a clone of this
3927 /// row with the primary key reset to [`Auto::Unset`] so
3928 /// the copy is ready for `insert_pool` / `save_pool` to
3929 /// allocate a fresh autoincrement value. Every other
3930 /// field is `Clone`d verbatim. Issue #825.
3931 ///
3932 /// `auto_now_add` / `auto_now` timestamp fields are
3933 /// **not** reset (Eloquent's `replicate` doesn't reset
3934 /// them either) — pass them through the normal insert
3935 /// path if you want fresh values, or assign them
3936 /// explicitly after the call.
3937 }
3938 } else {
3939 quote! {
3940 /// Eloquent-style `replicate()` — clone this row
3941 /// verbatim. Because the primary key is **not** an
3942 /// `Auto<T>`, the clone keeps the source PK; the
3943 /// caller must overwrite `copy.<pk>` before inserting
3944 /// to avoid a unique-key violation. Issue #825.
3945 }
3946 };
3947 // 2026-06-07 — field-name / shortcut collision guard.
3948 //
3949 // The macro emits both `pub const <field>: <field>_col = ...`
3950 // (per-field typed-column const, used by the typed-builder
3951 // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
3952 // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
3953 // model has a field named e.g. `count`, both items would land
3954 // in the same inherent impl with the same name, and the
3955 // compiler rejects the derive with "duplicate definitions".
3956 //
3957 // Drop the conflicting shortcut for that model. Callers can
3958 // still reach the same behavior via
3959 // `QuerySet::<Model>::default().count(&pool)`.
3960 let column_names: ::std::collections::HashSet<String> = fields
3961 .column_entries
3962 .iter()
3963 .map(|c| c.ident.to_string())
3964 .collect();
3965 let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
3966 if column_names.contains(name) {
3967 quote! {}
3968 } else {
3969 tokens
3970 }
3971 };
3972 let count_method = emit_if_no_field_collision(
3973 "count",
3974 quote! {
3975 /// Count rows of this model — `SELECT COUNT(*) FROM
3976 /// <table>`. Eloquent `Model::count()` parity.
3977 ///
3978 /// Skipped on models that already declare a field named
3979 /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
3980 /// in that case.
3981 ///
3982 /// # Errors
3983 /// As [`CounterPool::count`].
3984 ///
3985 /// [`CounterPool::count`]: rustango::sql::CounterPool::count
3986 pub async fn count(
3987 pool: &#root::sql::Pool,
3988 ) -> ::core::result::Result<i64, #root::sql::ExecError> {
3989 use #root::sql::CounterPool as _;
3990 #root::query::QuerySet::<Self>::default()
3991 .count(pool)
3992 .await
3993 }
3994 },
3995 );
3996 let value_method = emit_if_no_field_collision(
3997 "value",
3998 quote! {
3999 /// Pluck a single scalar from the first row.
4000 /// Eloquent `Model::query()->value($col)` parity.
4001 ///
4002 /// Skipped on models that already declare a field named
4003 /// `value`. Drop into
4004 /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4005 ///
4006 /// # Errors
4007 /// As `ValuesFlatQuerySet::first`.
4008 pub async fn value<U>(
4009 col: &str,
4010 pool: &#root::sql::Pool,
4011 ) -> ::core::result::Result<
4012 ::core::option::Option<U>,
4013 #root::sql::ExecError,
4014 >
4015 where
4016 U: #root::sql::MaybePgScalar
4017 + #root::sql::MaybeMyScalar
4018 + #root::sql::MaybeSqliteScalar
4019 + ::core::marker::Send
4020 + ::core::marker::Unpin,
4021 {
4022 let _col_static: &'static str = Self::__resolve_col(col)?;
4023 #root::query::QuerySet::<Self>::default()
4024 .values_list_flat(_col_static)
4025 .first::<U>(pool)
4026 .await
4027 }
4028 },
4029 );
4030 let sum_method = emit_if_no_field_collision(
4031 "sum",
4032 quote! {
4033 /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4034 /// Skipped on models that already declare a field named
4035 /// `sum`.
4036 ///
4037 /// # Errors
4038 /// As [`#root::sql::fetch_aggregate_pool`].
4039 pub async fn sum<U>(
4040 col: &str,
4041 pool: &#root::sql::Pool,
4042 ) -> ::core::result::Result<
4043 ::core::option::Option<U>,
4044 #root::sql::ExecError,
4045 >
4046 where
4047 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4048 + #root::sql::MaybeMyFromRow
4049 + #root::sql::MaybeSqliteFromRow
4050 + ::core::marker::Send
4051 + ::core::marker::Unpin,
4052 {
4053 Self::__aggregate_one_pool::<U>(
4054 col,
4055 |c| #root::core::AggregateExpr::Sum(c),
4056 pool,
4057 )
4058 .await
4059 }
4060 },
4061 );
4062 let avg_method = emit_if_no_field_collision(
4063 "avg",
4064 quote! {
4065 /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4066 /// models that already declare a field named `avg`.
4067 ///
4068 /// # Errors
4069 /// As [`#root::sql::fetch_aggregate_pool`].
4070 pub async fn avg<U>(
4071 col: &str,
4072 pool: &#root::sql::Pool,
4073 ) -> ::core::result::Result<
4074 ::core::option::Option<U>,
4075 #root::sql::ExecError,
4076 >
4077 where
4078 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4079 + #root::sql::MaybeMyFromRow
4080 + #root::sql::MaybeSqliteFromRow
4081 + ::core::marker::Send
4082 + ::core::marker::Unpin,
4083 {
4084 Self::__aggregate_one_pool::<U>(
4085 col,
4086 |c| #root::core::AggregateExpr::Avg(c),
4087 pool,
4088 )
4089 .await
4090 }
4091 },
4092 );
4093 let min_method = emit_if_no_field_collision(
4094 "min",
4095 quote! {
4096 /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4097 /// models that already declare a field named `min`.
4098 ///
4099 /// # Errors
4100 /// As [`#root::sql::fetch_aggregate_pool`].
4101 pub async fn min<U>(
4102 col: &str,
4103 pool: &#root::sql::Pool,
4104 ) -> ::core::result::Result<
4105 ::core::option::Option<U>,
4106 #root::sql::ExecError,
4107 >
4108 where
4109 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4110 + #root::sql::MaybeMyFromRow
4111 + #root::sql::MaybeSqliteFromRow
4112 + ::core::marker::Send
4113 + ::core::marker::Unpin,
4114 {
4115 Self::__aggregate_one_pool::<U>(
4116 col,
4117 |c| #root::core::AggregateExpr::Min(c),
4118 pool,
4119 )
4120 .await
4121 }
4122 },
4123 );
4124 let max_method = emit_if_no_field_collision(
4125 "max",
4126 quote! {
4127 /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4128 /// models that already declare a field named `max`.
4129 ///
4130 /// # Errors
4131 /// As [`#root::sql::fetch_aggregate_pool`].
4132 pub async fn max<U>(
4133 col: &str,
4134 pool: &#root::sql::Pool,
4135 ) -> ::core::result::Result<
4136 ::core::option::Option<U>,
4137 #root::sql::ExecError,
4138 >
4139 where
4140 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4141 + #root::sql::MaybeMyFromRow
4142 + #root::sql::MaybeSqliteFromRow
4143 + ::core::marker::Send
4144 + ::core::marker::Unpin,
4145 {
4146 Self::__aggregate_one_pool::<U>(
4147 col,
4148 |c| #root::core::AggregateExpr::Max(c),
4149 pool,
4150 )
4151 .await
4152 }
4153 },
4154 );
4155 let first_method = emit_if_no_field_collision(
4156 "first",
4157 quote! {
4158 /// First row of this model. Eloquent `Model::first()`.
4159 /// Skipped on models that already declare a field named
4160 /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4161 ///
4162 /// # Errors
4163 /// As `QuerySet::first`.
4164 pub async fn first(
4165 pool: &#root::sql::Pool,
4166 ) -> ::core::result::Result<
4167 ::core::option::Option<Self>,
4168 #root::sql::ExecError,
4169 > {
4170 #root::query::QuerySet::<Self>::default()
4171 .first(pool)
4172 .await
4173 }
4174 },
4175 );
4176 let last_method = emit_if_no_field_collision(
4177 "last",
4178 quote! {
4179 /// Last row of this model by primary-key DESC.
4180 /// Eloquent `Model::query()->latest('id')->first()`
4181 /// parity — fetches the highest-PK row without
4182 /// requiring the caller to spell the PK column.
4183 /// Returns `None` on an empty table.
4184 ///
4185 /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4186 /// Skipped on models that already declare a field
4187 /// named `last`.
4188 ///
4189 /// # Errors
4190 /// As `QuerySet::last`.
4191 pub async fn last(
4192 pool: &#root::sql::Pool,
4193 ) -> ::core::result::Result<
4194 ::core::option::Option<Self>,
4195 #root::sql::ExecError,
4196 > {
4197 #root::query::QuerySet::<Self>::default()
4198 .last(pool)
4199 .await
4200 }
4201 },
4202 );
4203 quote! {
4204 /// Re-SELECT this row by its primary key and overwrite
4205 /// every in-memory field with the freshly-fetched value.
4206 /// Django's [`Model.refresh_from_db`]. Issue #825.
4207 ///
4208 /// Use this when the row may have been modified by another
4209 /// process / connection / job since you read it — e.g. after
4210 /// a queued task callback, or to re-sync stale UI state
4211 /// before re-saving.
4212 ///
4213 /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4214 /// when the primary key no longer matches any row (e.g.
4215 /// the row was deleted concurrently).
4216 ///
4217 /// # Errors
4218 /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4219 /// the PK no longer exists.
4220 ///
4221 /// [`Model.refresh_from_db`]: https://docs.djangoproject.com/en/5.1/ref/models/instances/#django.db.models.Model.refresh_from_db
4222 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4223 pub async fn refresh_from_db(
4224 &mut self,
4225 pool: &#root::sql::Pool,
4226 ) -> ::core::result::Result<(), #root::sql::ExecError> {
4227 use #root::sql::FetcherPool as _;
4228 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4229 ::core::clone::Clone::clone(&self.#pk_ident),
4230 );
4231 let mut _rows: ::std::vec::Vec<Self> =
4232 #root::query::QuerySet::<Self>::default()
4233 .filter(::core::stringify!(#pk_ident), _pk_val)
4234 .limit(1)
4235 .fetch(pool)
4236 .await?;
4237 match _rows.into_iter().next() {
4238 ::core::option::Option::Some(_fresh) => {
4239 *self = _fresh;
4240 ::core::result::Result::Ok(())
4241 }
4242 ::core::option::Option::None => ::core::result::Result::Err(
4243 #root::sql::ExecError::Driver(
4244 #root::sql::sqlx::Error::RowNotFound,
4245 ),
4246 ),
4247 }
4248 }
4249
4250 /// Atomically increment the integer column `col` by
4251 /// `by` for this row. Equivalent to
4252 /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4253 /// Eloquent `Model::increment($col, $by)` / Django
4254 /// `Model.objects.filter(pk=…).update(col=F('col')+$by)`
4255 /// parity.
4256 ///
4257 /// **Doesn't mutate `self`** — the in-memory copy is now
4258 /// stale; call [`Self::refresh_from_db_pool`] /
4259 /// [`Self::fresh_pool`] to re-sync. Returns the rows-
4260 /// affected count (0 when the PK doesn't match any row,
4261 /// 1 on success).
4262 ///
4263 /// `col` is the Rust field name as a string; unknown
4264 /// fields surface as `UnknownField` at runtime. Negative
4265 /// `by` values atomically decrement (see also
4266 /// [`Self::decrement_pool`]).
4267 ///
4268 /// # Errors
4269 /// As [`UpdaterPool::execute_pool`].
4270 ///
4271 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4272 pub async fn increment(
4273 &self,
4274 col: &str,
4275 by: i64,
4276 pool: &#root::sql::Pool,
4277 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4278 Self::__increment_one(self, col, by, pool).await
4279 }
4280
4281 /// Sibling of [`Self::increment`] — atomically
4282 /// decrement this row's `col` by `by`. Eloquent
4283 /// `$model->decrement($col, $by)` parity. Equivalent to
4284 /// `self.increment(col, -by, &pool)`; the separate name
4285 /// keeps call sites readable.
4286 ///
4287 /// # Errors
4288 /// As [`Self::increment`].
4289 pub async fn decrement(
4290 &self,
4291 col: &str,
4292 by: i64,
4293 pool: &#root::sql::Pool,
4294 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4295 Self::__increment_one(self, col, -by, pool).await
4296 }
4297
4298 /// Bulk-increment: add `by` to `col` on every row of the
4299 /// table. Eloquent `Model::query()->increment($col, $by)`
4300 /// parity. Use for counters, score adjustments, view
4301 /// rollups.
4302 ///
4303 /// # Errors
4304 /// As [`UpdaterPool::execute_pool`].
4305 ///
4306 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4307 pub async fn increment_each(
4308 col: &str,
4309 by: i64,
4310 pool: &#root::sql::Pool,
4311 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4312 Self::__increment_all(col, by, pool).await
4313 }
4314
4315 /// Sibling of [`Self::increment_each`] — bulk-decrement.
4316 ///
4317 /// # Errors
4318 /// As [`Self::increment_each`].
4319 pub async fn decrement_each(
4320 col: &str,
4321 by: i64,
4322 pool: &#root::sql::Pool,
4323 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4324 Self::__increment_all(col, -by, pool).await
4325 }
4326
4327 /// Internal: forward to
4328 /// [`#root::sql::model_shortcuts::increment_one_pool`].
4329 /// One-line wrapper kept as a per-Model method so the
4330 /// macro's emitted `increment` / `decrement` instance
4331 /// calls don't have to thread `Self` through manually.
4332 #[doc(hidden)]
4333 pub async fn __increment_one(
4334 this: &Self,
4335 col: &str,
4336 by: i64,
4337 pool: &#root::sql::Pool,
4338 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4339 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4340 ::core::clone::Clone::clone(&this.#pk_ident),
4341 );
4342 #root::sql::model_shortcuts::increment_one_pool::<Self>(
4343 ::core::stringify!(#pk_ident),
4344 _pk_val,
4345 col,
4346 by,
4347 pool,
4348 )
4349 .await
4350 }
4351
4352 /// Internal: forward to
4353 /// [`#root::sql::model_shortcuts::increment_all_pool`].
4354 #[doc(hidden)]
4355 pub async fn __increment_all(
4356 col: &str,
4357 by: i64,
4358 pool: &#root::sql::Pool,
4359 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4360 #root::sql::model_shortcuts::increment_all_pool::<Self>(col, by, pool).await
4361 }
4362
4363 /// Internal: forward to
4364 /// [`#root::sql::model_shortcuts::resolve_col`].
4365 #[doc(hidden)]
4366 pub fn __resolve_col(
4367 col: &str,
4368 ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4369 #root::sql::model_shortcuts::resolve_col::<Self>(col)
4370 }
4371
4372 /// Internal: forward to
4373 /// [`#root::sql::model_shortcuts::add_signed_expr`].
4374 #[doc(hidden)]
4375 #[must_use]
4376 pub fn __add_signed_expr(
4377 col_static: &'static str,
4378 signed_by: i64,
4379 ) -> #root::core::Expr {
4380 #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4381 }
4382
4383 /// Re-SELECT this row by its primary key and return a
4384 /// **new** instance with the freshly-fetched fields.
4385 /// Eloquent `Model::fresh()` parity — non-mutating
4386 /// counterpart of [`Self::refresh_from_db_pool`].
4387 ///
4388 /// Returns `Ok(None)` when the row was deleted
4389 /// concurrently — vs [`Self::refresh_from_db_pool`]
4390 /// which surfaces that as `RowNotFound` because
4391 /// in-place mutation has nothing to write to.
4392 ///
4393 /// Useful when you want to compare the in-memory
4394 /// instance against the persisted state (audit-style
4395 /// diffs, conflict detection) without mutating the
4396 /// reference you already hold.
4397 ///
4398 /// # Errors
4399 /// As [`FetcherPool::fetch`].
4400 ///
4401 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4402 pub async fn fresh(
4403 &self,
4404 pool: &#root::sql::Pool,
4405 ) -> ::core::result::Result<
4406 ::core::option::Option<Self>,
4407 #root::sql::ExecError,
4408 > {
4409 use #root::sql::FetcherPool as _;
4410 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4411 ::core::clone::Clone::clone(&self.#pk_ident),
4412 );
4413 let _rows: ::std::vec::Vec<Self> =
4414 #root::query::QuerySet::<Self>::default()
4415 .filter(::core::stringify!(#pk_ident), _pk_val)
4416 .limit(1)
4417 .fetch(pool)
4418 .await?;
4419 ::core::result::Result::Ok(_rows.into_iter().next())
4420 }
4421
4422 #replicate_doc
4423 #[must_use]
4424 pub fn replicate(&self) -> Self {
4425 Self {
4426 #pk_clone_token,
4427 #( #other_field_clones, )*
4428 }
4429 }
4430
4431 #first_method
4432
4433 #last_method
4434
4435 /// Throwing counterpart of [`Self::first_pool`] —
4436 /// errors with `RowNotFound` when the table is empty.
4437 /// Eloquent `Model::firstOrFail()` parity.
4438 ///
4439 /// # Errors
4440 /// As [`Self::first_pool`]; additionally
4441 /// [`sqlx::Error::RowNotFound`] on empty tables.
4442 ///
4443 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4444 pub async fn first_or_fail(
4445 pool: &#root::sql::Pool,
4446 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4447 // Route through the queryset rather than `Self::first`,
4448 // which is suppressed on models with a field named
4449 // `first` (field/shortcut collision guard).
4450 match #root::query::QuerySet::<Self>::default().first(pool).await? {
4451 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4452 ::core::option::Option::None => ::core::result::Result::Err(
4453 #root::sql::ExecError::Driver(
4454 #root::sql::sqlx::Error::RowNotFound,
4455 ),
4456 ),
4457 }
4458 }
4459
4460 /// Single-column projection — `SELECT <col> FROM
4461 /// <table>`. Returns `Vec<U>` where each element is the
4462 /// decoded value of the column. Eloquent
4463 /// `Model::pluck($column)` / Django
4464 /// `Model.objects.values_list('col', flat=True)` parity.
4465 ///
4466 /// Thin wrapper over `QuerySet::<Self>::default()
4467 /// .values_list_flat(col).fetch::<U>(pool)`. `U` must
4468 /// be decodable from the column's SQL type on every
4469 /// dialect the binary targets (common picks: `i64` /
4470 /// `i32` / `String` / `bool` / `f64`).
4471 ///
4472 /// # Errors
4473 /// As `ValuesFlatQuerySet::fetch`.
4474 pub async fn pluck<U>(
4475 col: &'static str,
4476 pool: &#root::sql::Pool,
4477 ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4478 where
4479 U: #root::sql::MaybePgScalar
4480 + #root::sql::MaybeMyScalar
4481 + #root::sql::MaybeSqliteScalar
4482 + ::core::marker::Send
4483 + ::core::marker::Unpin,
4484 {
4485 #root::query::QuerySet::<Self>::default()
4486 .values_list_flat(col)
4487 .fetch::<U>(pool)
4488 .await
4489 }
4490
4491 /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4492 /// stream every row of this model in batches of `n`,
4493 /// invoking the callback once per batch. Stable PK-ASC
4494 /// ordering so the LIMIT/OFFSET pagination is
4495 /// deterministic across drivers.
4496 ///
4497 /// The callback is async — it can do further DB work
4498 /// (writes, related-row lookups, queue dispatch) per
4499 /// batch. Return `Err(...)` from the callback to abort
4500 /// the iteration early; the error bubbles up.
4501 ///
4502 /// **When to use**: bulk processing flows that can't
4503 /// fit the whole table in memory — sending newsletters,
4504 /// running data migrations, computing summary
4505 /// statistics. For small / known-bounded tables, plain
4506 /// `Self::all(&pool)` is simpler.
4507 ///
4508 /// **Caveat**: ascending OFFSET pagination is O(N²) on
4509 /// large tables. For multi-million-row scans prefer
4510 /// keyset-by-PK (the standard "WHERE id > last_seen"
4511 /// shape) over `chunk(...)`.
4512 ///
4513 /// Skipped on models without a primary key — chunking
4514 /// needs a stable order to avoid skipping / repeating
4515 /// rows across batches.
4516 pub async fn chunk<F, Fut>(
4517 n: i64,
4518 pool: &#root::sql::Pool,
4519 mut cb: F,
4520 ) -> ::core::result::Result<(), #root::sql::ExecError>
4521 where
4522 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4523 Fut: ::core::future::Future<
4524 Output = ::core::result::Result<(), #root::sql::ExecError>,
4525 >,
4526 {
4527 use #root::sql::FetcherPool as _;
4528 let pk_col = match Self::primary_key_column() {
4529 ::core::option::Option::Some(c) => c,
4530 ::core::option::Option::None => {
4531 return ::core::result::Result::Ok(());
4532 }
4533 };
4534 let mut offset: i64 = 0;
4535 loop {
4536 let rows: ::std::vec::Vec<Self> =
4537 #root::query::QuerySet::<Self>::default()
4538 .order_by(&[(pk_col, false)])
4539 .limit(n)
4540 .offset(offset)
4541 .fetch(pool)
4542 .await?;
4543 if rows.is_empty() {
4544 return ::core::result::Result::Ok(());
4545 }
4546 let len = rows.len() as i64;
4547 cb(rows).await?;
4548 if len < n {
4549 return ::core::result::Result::Ok(());
4550 }
4551 offset += n;
4552 }
4553 }
4554
4555 /// Eloquent `Model::chunkById($n, fn (...))` — same
4556 /// per-batch callback shape as [`Self::chunk`], but uses
4557 /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4558 /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4559 /// — the right choice for multi-million-row sweeps.
4560 ///
4561 /// Requires the primary key to be a signed integer type
4562 /// (`i64` / `i32`); the keyset comparison rides on
4563 /// `__rustango_pk_value()` lowering through
4564 /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4565 /// non-integer PKs (UUID, String) — those should use
4566 /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4567 /// keyset loop.
4568 ///
4569 /// Callback errors abort iteration; the error bubbles up
4570 /// unchanged. Empty table → callback invoked zero times.
4571 pub async fn chunk_by_id<F, Fut>(
4572 n: i64,
4573 pool: &#root::sql::Pool,
4574 mut cb: F,
4575 ) -> ::core::result::Result<(), #root::sql::ExecError>
4576 where
4577 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4578 Fut: ::core::future::Future<
4579 Output = ::core::result::Result<(), #root::sql::ExecError>,
4580 >,
4581 {
4582 use #root::sql::FetcherPool as _;
4583 let pk_col = match Self::primary_key_column() {
4584 ::core::option::Option::Some(c) => c,
4585 ::core::option::Option::None => {
4586 return ::core::result::Result::Ok(());
4587 }
4588 };
4589 // Track the largest PK seen so the next batch picks
4590 // up from there. `i64::MIN` as the sentinel — the
4591 // very first iteration's `> MIN` matches every row,
4592 // so the loop entry is uniform with subsequent
4593 // iterations.
4594 let mut last_seen: i64 = i64::MIN;
4595 loop {
4596 let key = ::std::format!("{}__gt", pk_col);
4597 let rows: ::std::vec::Vec<Self> =
4598 #root::query::QuerySet::<Self>::default()
4599 .filter(key.as_str(), last_seen)
4600 .order_by(&[(pk_col, false)])
4601 .limit(n)
4602 .fetch(pool)
4603 .await?;
4604 if rows.is_empty() {
4605 return ::core::result::Result::Ok(());
4606 }
4607 let len = rows.len() as i64;
4608 // Capture the last row's PK BEFORE moving rows
4609 // into the callback.
4610 let max_pk = match rows
4611 .last()
4612 .map(|r| r.__rustango_pk_value())
4613 {
4614 ::core::option::Option::Some(
4615 #root::core::SqlValue::I64(v),
4616 ) => v,
4617 ::core::option::Option::Some(
4618 #root::core::SqlValue::I32(v),
4619 ) => i64::from(v),
4620 _ => return ::core::result::Result::Ok(()),
4621 };
4622 cb(rows).await?;
4623 if len < n {
4624 return ::core::result::Result::Ok(());
4625 }
4626 last_seen = max_pk;
4627 }
4628 }
4629
4630 /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4631 /// per-row callback companion to [`Self::chunk`].
4632 /// Streams every row in keyset-paginated batches of
4633 /// `batch` size, calling `cb` once per row.
4634 ///
4635 /// Inherits the keyset-paginated scan of
4636 /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4637 /// non-integer PKs are silently a no-op).
4638 ///
4639 /// ```ignore
4640 /// Post::each(500, &pool, |p| async move {
4641 /// reindex(p).await?;
4642 /// Ok(())
4643 /// }).await?;
4644 /// ```
4645 ///
4646 /// Return `Err(...)` from the callback to abort
4647 /// iteration; the error bubbles up unchanged.
4648 pub async fn each<F, Fut>(
4649 batch: i64,
4650 pool: &#root::sql::Pool,
4651 mut cb: F,
4652 ) -> ::core::result::Result<(), #root::sql::ExecError>
4653 where
4654 F: ::core::ops::FnMut(Self) -> Fut,
4655 Fut: ::core::future::Future<
4656 Output = ::core::result::Result<(), #root::sql::ExecError>,
4657 >,
4658 {
4659 use #root::sql::FetcherPool as _;
4660 let pk_col = match Self::primary_key_column() {
4661 ::core::option::Option::Some(c) => c,
4662 ::core::option::Option::None => {
4663 return ::core::result::Result::Ok(());
4664 }
4665 };
4666 let mut last_seen: i64 = i64::MIN;
4667 loop {
4668 let key = ::std::format!("{}__gt", pk_col);
4669 let rows: ::std::vec::Vec<Self> =
4670 #root::query::QuerySet::<Self>::default()
4671 .filter(key.as_str(), last_seen)
4672 .order_by(&[(pk_col, false)])
4673 .limit(batch)
4674 .fetch(pool)
4675 .await?;
4676 if rows.is_empty() {
4677 return ::core::result::Result::Ok(());
4678 }
4679 let len = rows.len() as i64;
4680 let max_pk = match rows
4681 .last()
4682 .map(|r| r.__rustango_pk_value())
4683 {
4684 ::core::option::Option::Some(
4685 #root::core::SqlValue::I64(v),
4686 ) => v,
4687 ::core::option::Option::Some(
4688 #root::core::SqlValue::I32(v),
4689 ) => i64::from(v),
4690 _ => return ::core::result::Result::Ok(()),
4691 };
4692 for row in rows {
4693 cb(row).await?;
4694 }
4695 if len < batch {
4696 return ::core::result::Result::Ok(());
4697 }
4698 last_seen = max_pk;
4699 }
4700 }
4701
4702 /// Delete every row of this model — `TRUNCATE TABLE
4703 /// <table> RESTART IDENTITY CASCADE` on Postgres,
4704 /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4705 /// support `TRUNCATE` inside foreign-key constraints
4706 /// or — for SQLite — at all). Eloquent `Model::truncate()`
4707 /// / Django `Model.objects.all().delete()` parity.
4708 ///
4709 /// **Use only in tests / fixture-reset flows.** Production
4710 /// writes through this would silently bypass the
4711 /// `pre_delete` / `post_delete` signals (no per-row hooks
4712 /// fire on a TRUNCATE / bulk DELETE FROM) and lose every
4713 /// row's audit-log entry.
4714 ///
4715 /// # Errors
4716 /// As [`raw_execute_pool`].
4717 ///
4718 /// [`raw_execute_pool`]: rustango::sql::raw_execute_pool
4719 pub async fn truncate(
4720 pool: &#root::sql::Pool,
4721 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4722 let _table = <Self as #root::core::Model>::SCHEMA.table;
4723 let _dialect = pool.dialect();
4724 let _quoted = _dialect.quote_ident(_table);
4725 let _sql = if _dialect.name() == "postgres" {
4726 ::std::format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", _quoted)
4727 } else {
4728 ::std::format!("DELETE FROM {}", _quoted)
4729 };
4730 #root::sql::raw_execute_pool(pool, &_sql, ::std::vec::Vec::new()).await
4731 }
4732
4733 /// Bulk-delete every row whose primary key is in
4734 /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4735 /// Returns the affected row count.
4736 ///
4737 /// Eloquent `Model::destroy([1, 2, 3])` / Django
4738 /// `Model.objects.filter(pk__in=[...]).delete()` parity.
4739 /// Empty `pks` is a no-op (returns 0).
4740 ///
4741 /// Accepts any iterable whose elements are
4742 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4743 /// `[i64; N]`, etc.
4744 ///
4745 /// # Errors
4746 /// As `delete_pool`.
4747 pub async fn destroy<V>(
4748 pks: impl ::core::iter::IntoIterator<Item = V>,
4749 pool: &#root::sql::Pool,
4750 ) -> ::core::result::Result<u64, #root::sql::ExecError>
4751 where
4752 V: ::core::convert::Into<#root::core::SqlValue>,
4753 {
4754 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4755 pks.into_iter().map(::core::convert::Into::into).collect();
4756 if _values.is_empty() {
4757 return ::core::result::Result::Ok(0);
4758 }
4759 let _query = #root::core::DeleteQuery {
4760 model: <Self as #root::core::Model>::SCHEMA,
4761 where_clause: #root::core::WhereExpr::Predicate(
4762 #root::core::Filter {
4763 column: <Self as #root::core::Model>::SCHEMA
4764 .primary_key()
4765 .ok_or_else(|| {
4766 #root::sql::ExecError::Sql(
4767 #root::sql::SqlError::MissingPrimaryKey,
4768 )
4769 })?
4770 .column,
4771 op: #root::core::Op::In,
4772 value: #root::core::SqlValue::List(_values),
4773 },
4774 ),
4775 };
4776 #root::sql::delete_pool(pool, &_query).await
4777 }
4778
4779 /// Fetch every row where `<col> = <val>`. Eloquent
4780 /// `Model::where($col, $val)->get()` / Django
4781 /// `Model.objects.filter(col=val).all()` parity.
4782 ///
4783 /// Thin wrapper over `QuerySet::<Self>::default()
4784 /// .filter(col, val).fetch(pool)`. For one row,
4785 /// use [`Self::first_where_pool`]; for a chain that
4786 /// needs further `.filter()` / `.order_by()` /
4787 /// `.limit()`, drop down to `Self::query().filter(...)`
4788 /// directly.
4789 ///
4790 /// `val` accepts any value `Into<SqlValue>` so plain
4791 /// strings, ints, UUIDs, etc. all work.
4792 ///
4793 /// # Errors
4794 /// As [`FetcherPool::fetch`].
4795 ///
4796 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4797 pub async fn where_(
4798 col: &str,
4799 val: impl ::core::convert::Into<#root::core::SqlValue>,
4800 pool: &#root::sql::Pool,
4801 ) -> ::core::result::Result<
4802 ::std::vec::Vec<Self>,
4803 #root::sql::ExecError,
4804 > {
4805 use #root::sql::FetcherPool as _;
4806 #root::query::QuerySet::<Self>::default()
4807 .filter(col, val)
4808 .fetch(pool)
4809 .await
4810 }
4811
4812 /// Fetch every row where `<col> IN (vals)`. Eloquent
4813 /// `Model::whereIn($col, $vals)->get()` parity. Empty
4814 /// `vals` returns no rows (matches SQL's empty-IN
4815 /// semantics).
4816 ///
4817 /// `vals` accepts any iterable whose items are
4818 /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
4819 /// etc.
4820 ///
4821 /// # Errors
4822 /// As [`FetcherPool::fetch`].
4823 ///
4824 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4825 pub async fn where_in<V>(
4826 col: &str,
4827 vals: impl ::core::iter::IntoIterator<Item = V>,
4828 pool: &#root::sql::Pool,
4829 ) -> ::core::result::Result<
4830 ::std::vec::Vec<Self>,
4831 #root::sql::ExecError,
4832 >
4833 where
4834 V: ::core::convert::Into<#root::core::SqlValue>,
4835 {
4836 use #root::sql::FetcherPool as _;
4837 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4838 vals.into_iter().map(::core::convert::Into::into).collect();
4839 if _values.is_empty() {
4840 return ::core::result::Result::Ok(::std::vec::Vec::new());
4841 }
4842 let _key = ::std::format!("{}__in", col);
4843 #root::query::QuerySet::<Self>::default()
4844 .filter(&_key, #root::core::SqlValue::List(_values))
4845 .fetch(pool)
4846 .await
4847 }
4848
4849 /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
4850 /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
4851 /// `vals` returns every row (matches SQL's empty-NOT-IN
4852 /// semantics — vacuously true for every row).
4853 ///
4854 /// # Errors
4855 /// As [`FetcherPool::fetch`].
4856 ///
4857 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4858 pub async fn where_not_in<V>(
4859 col: &str,
4860 vals: impl ::core::iter::IntoIterator<Item = V>,
4861 pool: &#root::sql::Pool,
4862 ) -> ::core::result::Result<
4863 ::std::vec::Vec<Self>,
4864 #root::sql::ExecError,
4865 >
4866 where
4867 V: ::core::convert::Into<#root::core::SqlValue>,
4868 {
4869 use #root::sql::FetcherPool as _;
4870 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4871 vals.into_iter().map(::core::convert::Into::into).collect();
4872 if _values.is_empty() {
4873 return #root::query::QuerySet::<Self>::default()
4874 .fetch(pool)
4875 .await;
4876 }
4877 let _key = ::std::format!("{}__not_in", col);
4878 #root::query::QuerySet::<Self>::default()
4879 .filter(&_key, #root::core::SqlValue::List(_values))
4880 .fetch(pool)
4881 .await
4882 }
4883
4884 /// Fetch every row where `<col> IS NULL`. Eloquent
4885 /// `Model::whereNull($col)->get()` parity.
4886 ///
4887 /// # Errors
4888 /// As [`FetcherPool::fetch`].
4889 ///
4890 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4891 pub async fn where_null(
4892 col: &str,
4893 pool: &#root::sql::Pool,
4894 ) -> ::core::result::Result<
4895 ::std::vec::Vec<Self>,
4896 #root::sql::ExecError,
4897 > {
4898 use #root::sql::FetcherPool as _;
4899 let _key = ::std::format!("{}__isnull", col);
4900 #root::query::QuerySet::<Self>::default()
4901 .filter(&_key, #root::core::SqlValue::Bool(true))
4902 .fetch(pool)
4903 .await
4904 }
4905
4906 /// Fetch every row where `<col> IS NOT NULL`. Eloquent
4907 /// `Model::whereNotNull($col)->get()` parity.
4908 ///
4909 /// # Errors
4910 /// As [`FetcherPool::fetch`].
4911 ///
4912 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4913 pub async fn where_not_null(
4914 col: &str,
4915 pool: &#root::sql::Pool,
4916 ) -> ::core::result::Result<
4917 ::std::vec::Vec<Self>,
4918 #root::sql::ExecError,
4919 > {
4920 use #root::sql::FetcherPool as _;
4921 let _key = ::std::format!("{}__isnull", col);
4922 #root::query::QuerySet::<Self>::default()
4923 .filter(&_key, #root::core::SqlValue::Bool(false))
4924 .fetch(pool)
4925 .await
4926 }
4927
4928 /// Fetch up to `n` rows in random order. Eloquent
4929 /// `Model::inRandomOrder()->limit($n)->get()` /
4930 /// `Model::query()->inRandomOrder()->get()->take($n)`
4931 /// parity. **Performance caveat**: random ordering
4932 /// forces a full table scan + per-row random key sort;
4933 /// the optimizer cannot use an index. Prefer a
4934 /// `pk >= rand_offset LIMIT N` walk for huge tables.
4935 ///
4936 /// # Errors
4937 /// As [`FetcherPool::fetch`].
4938 ///
4939 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4940 pub async fn random_n(
4941 n: i64,
4942 pool: &#root::sql::Pool,
4943 ) -> ::core::result::Result<
4944 ::std::vec::Vec<Self>,
4945 #root::sql::ExecError,
4946 > {
4947 use #root::sql::FetcherPool as _;
4948 #root::query::QuerySet::<Self>::default()
4949 .order_random()
4950 .limit(n)
4951 .fetch(pool)
4952 .await
4953 }
4954
4955 /// Fetch one row in random order. Eloquent
4956 /// `Model::inRandomOrder()->first()` parity. Same
4957 /// performance caveat as [`Self::random_n_pool`].
4958 ///
4959 /// # Errors
4960 /// As [`FetcherPool::fetch`].
4961 ///
4962 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4963 pub async fn random(
4964 pool: &#root::sql::Pool,
4965 ) -> ::core::result::Result<
4966 ::core::option::Option<Self>,
4967 #root::sql::ExecError,
4968 > {
4969 ::core::result::Result::Ok(
4970 Self::random_n(1, pool).await?.into_iter().next(),
4971 )
4972 }
4973
4974 /// Fetch every row ordered ASC by `field`. Eloquent
4975 /// `Model::oldest($field)->get()` parity — the multi-row
4976 /// counterpart of [`Self::earliest_pool`].
4977 ///
4978 /// # Errors
4979 /// As [`FetcherPool::fetch`].
4980 ///
4981 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4982 pub async fn oldest(
4983 field: &str,
4984 pool: &#root::sql::Pool,
4985 ) -> ::core::result::Result<
4986 ::std::vec::Vec<Self>,
4987 #root::sql::ExecError,
4988 > {
4989 use #root::sql::FetcherPool as _;
4990 #root::query::QuerySet::<Self>::default()
4991 .order_by(&[(field, false)])
4992 .fetch(pool)
4993 .await
4994 }
4995
4996 /// Fetch every row ordered DESC by `field`. Eloquent
4997 /// `Model::latest($field)->get()` parity — the multi-row
4998 /// counterpart of [`Self::latest_pool`].
4999 ///
5000 /// # Errors
5001 /// As [`FetcherPool::fetch`].
5002 ///
5003 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5004 pub async fn newest(
5005 field: &str,
5006 pool: &#root::sql::Pool,
5007 ) -> ::core::result::Result<
5008 ::std::vec::Vec<Self>,
5009 #root::sql::ExecError,
5010 > {
5011 use #root::sql::FetcherPool as _;
5012 #root::query::QuerySet::<Self>::default()
5013 .order_by(&[(field, true)])
5014 .fetch(pool)
5015 .await
5016 }
5017
5018 /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5019 /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5020 /// Routes through the existing `__year` lookup suffix
5021 /// (issue #829).
5022 ///
5023 /// # Errors
5024 /// As [`FetcherPool::fetch`].
5025 ///
5026 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5027 pub async fn where_year(
5028 col: &str,
5029 year: i64,
5030 pool: &#root::sql::Pool,
5031 ) -> ::core::result::Result<
5032 ::std::vec::Vec<Self>,
5033 #root::sql::ExecError,
5034 > {
5035 use #root::sql::FetcherPool as _;
5036 let _key = ::std::format!("{}__year", col);
5037 #root::query::QuerySet::<Self>::default()
5038 .filter(&_key, #root::core::SqlValue::I64(year))
5039 .fetch(pool)
5040 .await
5041 }
5042
5043 /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5044 /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5045 /// `month` is 1–12.
5046 ///
5047 /// # Errors
5048 /// As [`FetcherPool::fetch`].
5049 ///
5050 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5051 pub async fn where_month(
5052 col: &str,
5053 month: i64,
5054 pool: &#root::sql::Pool,
5055 ) -> ::core::result::Result<
5056 ::std::vec::Vec<Self>,
5057 #root::sql::ExecError,
5058 > {
5059 use #root::sql::FetcherPool as _;
5060 let _key = ::std::format!("{}__month", col);
5061 #root::query::QuerySet::<Self>::default()
5062 .filter(&_key, #root::core::SqlValue::I64(month))
5063 .fetch(pool)
5064 .await
5065 }
5066
5067 /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5068 /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5069 /// `day` is 1–31.
5070 ///
5071 /// # Errors
5072 /// As [`FetcherPool::fetch`].
5073 ///
5074 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5075 pub async fn where_day(
5076 col: &str,
5077 day: i64,
5078 pool: &#root::sql::Pool,
5079 ) -> ::core::result::Result<
5080 ::std::vec::Vec<Self>,
5081 #root::sql::ExecError,
5082 > {
5083 use #root::sql::FetcherPool as _;
5084 let _key = ::std::format!("{}__day", col);
5085 #root::query::QuerySet::<Self>::default()
5086 .filter(&_key, #root::core::SqlValue::I64(day))
5087 .fetch(pool)
5088 .await
5089 }
5090
5091 /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5092 /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5093 /// `hour` is 0–23.
5094 ///
5095 /// # Errors
5096 /// As [`FetcherPool::fetch`].
5097 ///
5098 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5099 pub async fn where_hour(
5100 col: &str,
5101 hour: i64,
5102 pool: &#root::sql::Pool,
5103 ) -> ::core::result::Result<
5104 ::std::vec::Vec<Self>,
5105 #root::sql::ExecError,
5106 > {
5107 use #root::sql::FetcherPool as _;
5108 let _key = ::std::format!("{}__hour", col);
5109 #root::query::QuerySet::<Self>::default()
5110 .filter(&_key, #root::core::SqlValue::I64(hour))
5111 .fetch(pool)
5112 .await
5113 }
5114
5115 /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5116 /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5117 /// `minute` is 0–59.
5118 ///
5119 /// # Errors
5120 /// As [`FetcherPool::fetch`].
5121 ///
5122 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5123 pub async fn where_minute(
5124 col: &str,
5125 minute: i64,
5126 pool: &#root::sql::Pool,
5127 ) -> ::core::result::Result<
5128 ::std::vec::Vec<Self>,
5129 #root::sql::ExecError,
5130 > {
5131 use #root::sql::FetcherPool as _;
5132 let _key = ::std::format!("{}__minute", col);
5133 #root::query::QuerySet::<Self>::default()
5134 .filter(&_key, #root::core::SqlValue::I64(minute))
5135 .fetch(pool)
5136 .await
5137 }
5138
5139 /// Fetch every row where `<col> LIKE <pattern>` —
5140 /// caller-supplied pattern (must include `%` / `_`
5141 /// wildcards manually). Eloquent `Model::whereLike`
5142 /// parity.
5143 ///
5144 /// # Errors
5145 /// As [`FetcherPool::fetch`].
5146 ///
5147 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5148 pub async fn where_like(
5149 col: &str,
5150 pattern: impl ::core::convert::Into<::std::string::String>,
5151 pool: &#root::sql::Pool,
5152 ) -> ::core::result::Result<
5153 ::std::vec::Vec<Self>,
5154 #root::sql::ExecError,
5155 > {
5156 use #root::sql::FetcherPool as _;
5157 let _key = ::std::format!("{}__like", col);
5158 #root::query::QuerySet::<Self>::default()
5159 .filter(
5160 &_key,
5161 #root::core::SqlValue::String(pattern.into()),
5162 )
5163 .fetch(pool)
5164 .await
5165 }
5166
5167 /// Fetch every row where `<col> ILIKE <pattern>` —
5168 /// case-insensitive LIKE (PG native, MySQL/SQLite
5169 /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5170 ///
5171 /// # Errors
5172 /// As [`FetcherPool::fetch`].
5173 ///
5174 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5175 pub async fn where_ilike(
5176 col: &str,
5177 pattern: impl ::core::convert::Into<::std::string::String>,
5178 pool: &#root::sql::Pool,
5179 ) -> ::core::result::Result<
5180 ::std::vec::Vec<Self>,
5181 #root::sql::ExecError,
5182 > {
5183 use #root::sql::FetcherPool as _;
5184 let _key = ::std::format!("{}__ilike", col);
5185 #root::query::QuerySet::<Self>::default()
5186 .filter(
5187 &_key,
5188 #root::core::SqlValue::String(pattern.into()),
5189 )
5190 .fetch(pool)
5191 .await
5192 }
5193
5194 /// Fetch every row where `<col>` starts with `prefix`
5195 /// (auto-appends `%`). Django `__startswith` / Eloquent
5196 /// `whereLike("col", "$prefix%")` parity.
5197 ///
5198 /// # Errors
5199 /// As [`FetcherPool::fetch`].
5200 ///
5201 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5202 pub async fn where_starts_with(
5203 col: &str,
5204 prefix: impl ::core::convert::Into<::std::string::String>,
5205 pool: &#root::sql::Pool,
5206 ) -> ::core::result::Result<
5207 ::std::vec::Vec<Self>,
5208 #root::sql::ExecError,
5209 > {
5210 use #root::sql::FetcherPool as _;
5211 let _key = ::std::format!("{}__startswith", col);
5212 #root::query::QuerySet::<Self>::default()
5213 .filter(
5214 &_key,
5215 #root::core::SqlValue::String(prefix.into()),
5216 )
5217 .fetch(pool)
5218 .await
5219 }
5220
5221 /// Fetch every row where `<col>` ends with `suffix`
5222 /// (auto-prepends `%`). Django `__endswith` / Eloquent
5223 /// `whereLike("col", "%$suffix")` parity.
5224 ///
5225 /// # Errors
5226 /// As [`FetcherPool::fetch`].
5227 ///
5228 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5229 pub async fn where_ends_with(
5230 col: &str,
5231 suffix: impl ::core::convert::Into<::std::string::String>,
5232 pool: &#root::sql::Pool,
5233 ) -> ::core::result::Result<
5234 ::std::vec::Vec<Self>,
5235 #root::sql::ExecError,
5236 > {
5237 use #root::sql::FetcherPool as _;
5238 let _key = ::std::format!("{}__endswith", col);
5239 #root::query::QuerySet::<Self>::default()
5240 .filter(
5241 &_key,
5242 #root::core::SqlValue::String(suffix.into()),
5243 )
5244 .fetch(pool)
5245 .await
5246 }
5247
5248 /// Fetch every row where `<col>` contains `substr`
5249 /// (auto-wraps with `%`). Django `__contains` /
5250 /// Eloquent `whereLike("col", "%$substr%")` parity.
5251 ///
5252 /// # Errors
5253 /// As [`FetcherPool::fetch`].
5254 ///
5255 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5256 pub async fn where_contains(
5257 col: &str,
5258 substr: impl ::core::convert::Into<::std::string::String>,
5259 pool: &#root::sql::Pool,
5260 ) -> ::core::result::Result<
5261 ::std::vec::Vec<Self>,
5262 #root::sql::ExecError,
5263 > {
5264 use #root::sql::FetcherPool as _;
5265 let _key = ::std::format!("{}__contains", col);
5266 #root::query::QuerySet::<Self>::default()
5267 .filter(
5268 &_key,
5269 #root::core::SqlValue::String(substr.into()),
5270 )
5271 .fetch(pool)
5272 .await
5273 }
5274
5275 /// Fetch every row where `<col> > val`. Eloquent
5276 /// `Model::where($col, ">", $val)->get()` parity.
5277 ///
5278 /// # Errors
5279 /// As [`FetcherPool::fetch`].
5280 ///
5281 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5282 pub async fn where_gt(
5283 col: &str,
5284 val: impl ::core::convert::Into<#root::core::SqlValue>,
5285 pool: &#root::sql::Pool,
5286 ) -> ::core::result::Result<
5287 ::std::vec::Vec<Self>,
5288 #root::sql::ExecError,
5289 > {
5290 use #root::sql::FetcherPool as _;
5291 let _key = ::std::format!("{}__gt", col);
5292 #root::query::QuerySet::<Self>::default()
5293 .filter(&_key, val)
5294 .fetch(pool)
5295 .await
5296 }
5297
5298 /// Fetch every row where `<col> >= val`. Eloquent
5299 /// `Model::where($col, ">=", $val)->get()` parity.
5300 ///
5301 /// # Errors
5302 /// As [`FetcherPool::fetch`].
5303 ///
5304 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5305 pub async fn where_gte(
5306 col: &str,
5307 val: impl ::core::convert::Into<#root::core::SqlValue>,
5308 pool: &#root::sql::Pool,
5309 ) -> ::core::result::Result<
5310 ::std::vec::Vec<Self>,
5311 #root::sql::ExecError,
5312 > {
5313 use #root::sql::FetcherPool as _;
5314 let _key = ::std::format!("{}__gte", col);
5315 #root::query::QuerySet::<Self>::default()
5316 .filter(&_key, val)
5317 .fetch(pool)
5318 .await
5319 }
5320
5321 /// Fetch every row where `<col> < val`. Eloquent
5322 /// `Model::where($col, "<", $val)->get()` parity.
5323 ///
5324 /// # Errors
5325 /// As [`FetcherPool::fetch`].
5326 ///
5327 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5328 pub async fn where_lt(
5329 col: &str,
5330 val: impl ::core::convert::Into<#root::core::SqlValue>,
5331 pool: &#root::sql::Pool,
5332 ) -> ::core::result::Result<
5333 ::std::vec::Vec<Self>,
5334 #root::sql::ExecError,
5335 > {
5336 use #root::sql::FetcherPool as _;
5337 let _key = ::std::format!("{}__lt", col);
5338 #root::query::QuerySet::<Self>::default()
5339 .filter(&_key, val)
5340 .fetch(pool)
5341 .await
5342 }
5343
5344 /// Fetch every row where `<col> <= val`. Eloquent
5345 /// `Model::where($col, "<=", $val)->get()` parity.
5346 ///
5347 /// # Errors
5348 /// As [`FetcherPool::fetch`].
5349 ///
5350 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5351 pub async fn where_lte(
5352 col: &str,
5353 val: impl ::core::convert::Into<#root::core::SqlValue>,
5354 pool: &#root::sql::Pool,
5355 ) -> ::core::result::Result<
5356 ::std::vec::Vec<Self>,
5357 #root::sql::ExecError,
5358 > {
5359 use #root::sql::FetcherPool as _;
5360 let _key = ::std::format!("{}__lte", col);
5361 #root::query::QuerySet::<Self>::default()
5362 .filter(&_key, val)
5363 .fetch(pool)
5364 .await
5365 }
5366
5367 /// Fetch every row where `<col> <> val`. Eloquent
5368 /// `Model::where($col, "!=", $val)->get()` parity.
5369 ///
5370 /// # Errors
5371 /// As [`FetcherPool::fetch`].
5372 ///
5373 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5374 pub async fn where_ne(
5375 col: &str,
5376 val: impl ::core::convert::Into<#root::core::SqlValue>,
5377 pool: &#root::sql::Pool,
5378 ) -> ::core::result::Result<
5379 ::std::vec::Vec<Self>,
5380 #root::sql::ExecError,
5381 > {
5382 use #root::sql::FetcherPool as _;
5383 let _key = ::std::format!("{}__ne", col);
5384 #root::query::QuerySet::<Self>::default()
5385 .filter(&_key, val)
5386 .fetch(pool)
5387 .await
5388 }
5389
5390 /// Fetch every row matching `<col> = val` for ANY of the
5391 /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5392 /// parity. Empty `cols` returns no rows.
5393 ///
5394 /// Resolves each `&str` column to its SCHEMA-registered
5395 /// `&'static str` once and builds a single OR-composed
5396 /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5397 ///
5398 /// # Errors
5399 /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5400 /// when any column is not declared on the model.
5401 ///
5402 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5403 pub async fn where_any(
5404 cols: &[&str],
5405 val: impl ::core::convert::Into<#root::core::SqlValue>,
5406 pool: &#root::sql::Pool,
5407 ) -> ::core::result::Result<
5408 ::std::vec::Vec<Self>,
5409 #root::sql::ExecError,
5410 > {
5411 Self::__where_multi(cols, val, false, pool).await
5412 }
5413
5414 /// Fetch every row matching `<col> = val` for ALL listed
5415 /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5416 /// Empty `cols` returns every row (vacuous AND).
5417 ///
5418 /// # Errors
5419 /// As [`Self::where_any`].
5420 pub async fn where_all(
5421 cols: &[&str],
5422 val: impl ::core::convert::Into<#root::core::SqlValue>,
5423 pool: &#root::sql::Pool,
5424 ) -> ::core::result::Result<
5425 ::std::vec::Vec<Self>,
5426 #root::sql::ExecError,
5427 > {
5428 Self::__where_multi(cols, val, true, pool).await
5429 }
5430
5431 /// Internal: build a Q expression composing `cols` via
5432 /// AND (`all`) or OR (`!all`), then fetch. Backs
5433 /// `where_any` / `where_all`.
5434 #[doc(hidden)]
5435 pub async fn __where_multi(
5436 cols: &[&str],
5437 val: impl ::core::convert::Into<#root::core::SqlValue>,
5438 all: bool,
5439 pool: &#root::sql::Pool,
5440 ) -> ::core::result::Result<
5441 ::std::vec::Vec<Self>,
5442 #root::sql::ExecError,
5443 > {
5444 #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5445 .await
5446 }
5447
5448 /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5449 /// parity / Django `Model.objects.all()[:n]`. PK-ordered
5450 /// is NOT guaranteed without an explicit `order_by` —
5451 /// drop into `Self::query()` for that.
5452 ///
5453 /// # Errors
5454 /// As [`FetcherPool::fetch`].
5455 ///
5456 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5457 pub async fn take(
5458 n: i64,
5459 pool: &#root::sql::Pool,
5460 ) -> ::core::result::Result<
5461 ::std::vec::Vec<Self>,
5462 #root::sql::ExecError,
5463 > {
5464 use #root::sql::FetcherPool as _;
5465 #root::query::QuerySet::<Self>::default()
5466 .limit(n)
5467 .fetch(pool)
5468 .await
5469 }
5470
5471 /// Fetch the page-th window of `per_page` rows
5472 /// (1-indexed). Eloquent
5473 /// `Model::query()->forPage($page, $perPage)->get()`
5474 /// parity. The DB scans an `OFFSET (page - 1) * per_page
5475 /// LIMIT per_page`; for large offsets this is O(N) —
5476 /// prefer keyset pagination via PK on hot paths.
5477 ///
5478 /// # Errors
5479 /// As [`FetcherPool::fetch`].
5480 ///
5481 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5482 pub async fn for_page(
5483 page: i64,
5484 per_page: i64,
5485 pool: &#root::sql::Pool,
5486 ) -> ::core::result::Result<
5487 ::std::vec::Vec<Self>,
5488 #root::sql::ExecError,
5489 > {
5490 use #root::sql::FetcherPool as _;
5491 let _offset = if page > 1 { (page - 1) * per_page } else { 0 };
5492 #root::query::QuerySet::<Self>::default()
5493 .limit(per_page)
5494 .offset(_offset)
5495 .fetch(pool)
5496 .await
5497 }
5498
5499 /// Eloquent `Model::paginate($per_page, $page)` — fetch
5500 /// one page of rows AND the total row count in one
5501 /// call. Returns `(rows, total)`. Two queries: a
5502 /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5503 /// for the total.
5504 ///
5505 /// Useful for paginated UIs that need both the visible
5506 /// rows AND a "Page X of Y" / total-count widget. For
5507 /// large tables prefer keyset pagination + cached count;
5508 /// every call to `paginate` re-counts the full table.
5509 ///
5510 /// Same 1-indexed `page` convention as [`Self::for_page`].
5511 ///
5512 /// # Errors
5513 /// As [`Self::for_page`] and [`Self::count`].
5514 pub async fn paginate(
5515 page: i64,
5516 per_page: i64,
5517 pool: &#root::sql::Pool,
5518 ) -> ::core::result::Result<
5519 (::std::vec::Vec<Self>, i64),
5520 #root::sql::ExecError,
5521 > {
5522 // Route through the queryset rather than `Self::count`:
5523 // the `count` inherent method is suppressed on models
5524 // that declare a field named `count` (field/shortcut
5525 // collision guard), and `paginate` must still compile
5526 // for those models.
5527 let total = {
5528 use #root::sql::CounterPool as _;
5529 #root::query::QuerySet::<Self>::default()
5530 .count(pool)
5531 .await?
5532 };
5533 let rows = Self::for_page(page, per_page, pool).await?;
5534 ::core::result::Result::Ok((rows, total))
5535 }
5536
5537 /// Bulk-update — set `set_col = set_val` on every row
5538 /// matching `where_col = where_val`. Returns affected row
5539 /// count. Eloquent
5540 /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5541 /// parity.
5542 ///
5543 /// For multi-column updates drop into the queryset
5544 /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5545 ///
5546 /// # Errors
5547 /// As [`UpdaterPool::execute_pool`].
5548 ///
5549 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5550 pub async fn update_where(
5551 where_col: &str,
5552 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5553 set_col: &str,
5554 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5555 pool: &#root::sql::Pool,
5556 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5557 use #root::sql::UpdaterPool as _;
5558 #root::query::QuerySet::<Self>::default()
5559 .filter(where_col, where_val)
5560 .update()
5561 .set(set_col, set_val)
5562 .execute_pool(pool)
5563 .await
5564 }
5565
5566 /// Bulk-delete — remove every row matching
5567 /// `where_col = where_val`. Returns affected row count.
5568 /// Eloquent
5569 /// `Model::where($where_col, $where_val)->delete()` parity.
5570 ///
5571 /// For more complex filters drop into the queryset
5572 /// builder + `Self::query().filter(...).delete().execute_pool(&pool)`.
5573 ///
5574 /// # Errors
5575 /// As [`#root::sql::delete_pool`].
5576 pub async fn delete_where(
5577 where_col: &str,
5578 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5579 pool: &#root::sql::Pool,
5580 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5581 let _query = #root::core::DeleteQuery {
5582 model: <Self as #root::core::Model>::SCHEMA,
5583 where_clause: #root::core::WhereExpr::Predicate(
5584 #root::core::Filter {
5585 column: <Self as #root::core::Model>::SCHEMA
5586 .field(where_col)
5587 .ok_or_else(|| {
5588 #root::sql::ExecError::Query(
5589 #root::core::QueryError::UnknownField {
5590 model: <Self as #root::core::Model>::SCHEMA.name,
5591 field: ::std::string::ToString::to_string(where_col),
5592 },
5593 )
5594 })?
5595 .column,
5596 op: #root::core::Op::Eq,
5597 value: ::core::convert::Into::into(where_val),
5598 },
5599 ),
5600 };
5601 #root::sql::delete_pool(pool, &_query).await
5602 }
5603
5604 /// Bulk-update — set `set_col = set_val` on EVERY row of
5605 /// this model's table. **No WHERE clause** — use with
5606 /// care. Eloquent `Model::query()->update([$col => $val])`
5607 /// parity.
5608 ///
5609 /// Use for backfills, one-shot reset flows, etc. The
5610 /// returned count is rows affected.
5611 ///
5612 /// # Errors
5613 /// As [`UpdaterPool::execute_pool`].
5614 ///
5615 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5616 pub async fn update_all(
5617 set_col: &str,
5618 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5619 pool: &#root::sql::Pool,
5620 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5621 use #root::sql::UpdaterPool as _;
5622 #root::query::QuerySet::<Self>::default()
5623 .update()
5624 .set(set_col, set_val)
5625 .execute_pool(pool)
5626 .await
5627 }
5628
5629 /// Fetch every row where `<col> NOT LIKE <pattern>`.
5630 /// Eloquent `Model::whereNotLike` parity. Pattern is
5631 /// passed verbatim — caller controls `%` / `_`.
5632 ///
5633 /// # Errors
5634 /// As [`FetcherPool::fetch`].
5635 ///
5636 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5637 pub async fn where_not_like(
5638 col: &str,
5639 pattern: impl ::core::convert::Into<::std::string::String>,
5640 pool: &#root::sql::Pool,
5641 ) -> ::core::result::Result<
5642 ::std::vec::Vec<Self>,
5643 #root::sql::ExecError,
5644 > {
5645 use #root::sql::FetcherPool as _;
5646 let _key = ::std::format!("{}__not_like", col);
5647 #root::query::QuerySet::<Self>::default()
5648 .filter(
5649 &_key,
5650 #root::core::SqlValue::String(pattern.into()),
5651 )
5652 .fetch(pool)
5653 .await
5654 }
5655
5656 /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5657 /// case-insensitive `NOT LIKE` (PG native, MySQL /
5658 /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5659 ///
5660 /// # Errors
5661 /// As [`FetcherPool::fetch`].
5662 ///
5663 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5664 pub async fn where_not_ilike(
5665 col: &str,
5666 pattern: impl ::core::convert::Into<::std::string::String>,
5667 pool: &#root::sql::Pool,
5668 ) -> ::core::result::Result<
5669 ::std::vec::Vec<Self>,
5670 #root::sql::ExecError,
5671 > {
5672 use #root::sql::FetcherPool as _;
5673 let _key = ::std::format!("{}__not_ilike", col);
5674 #root::query::QuerySet::<Self>::default()
5675 .filter(
5676 &_key,
5677 #root::core::SqlValue::String(pattern.into()),
5678 )
5679 .fetch(pool)
5680 .await
5681 }
5682
5683 /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5684 /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5685 /// parity.
5686 ///
5687 /// # Errors
5688 /// As [`FetcherPool::fetch`].
5689 ///
5690 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5691 pub async fn where_not_between(
5692 col: &str,
5693 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5694 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5695 pool: &#root::sql::Pool,
5696 ) -> ::core::result::Result<
5697 ::std::vec::Vec<Self>,
5698 #root::sql::ExecError,
5699 > {
5700 use #root::sql::FetcherPool as _;
5701 let _key = ::std::format!("{}__not_between", col);
5702 let _vals = #root::core::SqlValue::List(::std::vec![
5703 ::core::convert::Into::into(lo),
5704 ::core::convert::Into::into(hi),
5705 ]);
5706 #root::query::QuerySet::<Self>::default()
5707 .filter(&_key, _vals)
5708 .fetch(pool)
5709 .await
5710 }
5711
5712 /// Returns the SQL table name for this model. Eloquent
5713 /// `$model->getTable()` parity.
5714 #[must_use]
5715 pub fn table_name() -> &'static str {
5716 <Self as #root::core::Model>::SCHEMA.table
5717 }
5718
5719 /// Returns the SQL column name of this model's primary
5720 /// key, or `None` when the model has no
5721 /// `#[rustango(primary_key)]`. Eloquent
5722 /// `$model->getKeyName()` parity.
5723 #[must_use]
5724 pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5725 <Self as #root::core::Model>::SCHEMA
5726 .primary_key()
5727 .map(|f| f.column)
5728 }
5729
5730 /// Fetch every row where `<col> BETWEEN lo AND hi`
5731 /// (inclusive on both ends — same as SQL). Eloquent
5732 /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5733 ///
5734 /// # Errors
5735 /// As [`FetcherPool::fetch`].
5736 ///
5737 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5738 pub async fn where_between(
5739 col: &str,
5740 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5741 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5742 pool: &#root::sql::Pool,
5743 ) -> ::core::result::Result<
5744 ::std::vec::Vec<Self>,
5745 #root::sql::ExecError,
5746 > {
5747 use #root::sql::FetcherPool as _;
5748 let _key = ::std::format!("{}__between", col);
5749 let _vals = #root::core::SqlValue::List(::std::vec![
5750 ::core::convert::Into::into(lo),
5751 ::core::convert::Into::into(hi),
5752 ]);
5753 #root::query::QuerySet::<Self>::default()
5754 .filter(&_key, _vals)
5755 .fetch(pool)
5756 .await
5757 }
5758
5759 /// Fetch the first row where `<col> = <val>`. Returns
5760 /// `Ok(None)` when no row matches. Eloquent
5761 /// `Model::firstWhere($col, $val)` / Django
5762 /// `Model.objects.filter(col=val).first()` parity.
5763 ///
5764 /// Thin wrapper over `QuerySet::<Self>::default()
5765 /// .filter(col, val).first(pool)`. Use this when you
5766 /// want one row identified by a non-PK column (e.g.
5767 /// `User::first_where_pool("email", "x@y.com", &pool)`).
5768 ///
5769 /// `val` accepts any value `Into<SqlValue>` so plain
5770 /// strings, ints, UUIDs, etc. all work.
5771 ///
5772 /// # Errors
5773 /// As `QuerySet::first`.
5774 pub async fn first_where(
5775 col: &str,
5776 val: impl ::core::convert::Into<#root::core::SqlValue>,
5777 pool: &#root::sql::Pool,
5778 ) -> ::core::result::Result<
5779 ::core::option::Option<Self>,
5780 #root::sql::ExecError,
5781 > {
5782 #root::query::QuerySet::<Self>::default()
5783 .filter(col, val)
5784 .first(pool)
5785 .await
5786 }
5787
5788 #value_method
5789
5790 /// Fetch the row with the largest `field` value —
5791 /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
5792 /// `Ok(None)` for an empty table. Eloquent
5793 /// `Model::latest($field)->first()` / Django
5794 /// `Model.objects.latest(field)` (non-throwing) parity.
5795 /// Thin wrapper over `QuerySet::<Self>::default()
5796 /// .latest(field, pool)`.
5797 ///
5798 /// **Field name** is the Rust field ident as a string
5799 /// (not the SQL column). Unknown fields surface as
5800 /// `ExecError::Query(QueryError::UnknownField)` at
5801 /// compile time.
5802 ///
5803 /// # Errors
5804 /// As `QuerySet::latest`.
5805 pub async fn latest(
5806 field: &str,
5807 pool: &#root::sql::Pool,
5808 ) -> ::core::result::Result<
5809 ::core::option::Option<Self>,
5810 #root::sql::ExecError,
5811 > {
5812 #root::query::QuerySet::<Self>::default()
5813 .latest(field, pool)
5814 .await
5815 }
5816
5817 /// Sibling of [`Self::latest_pool`] — fetches the row
5818 /// with the smallest `field` value (`ORDER BY <field>
5819 /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
5820 /// ->first()` / Django `Model.objects.earliest(field)`
5821 /// parity.
5822 ///
5823 /// # Errors
5824 /// As [`Self::latest_pool`].
5825 pub async fn earliest(
5826 field: &str,
5827 pool: &#root::sql::Pool,
5828 ) -> ::core::result::Result<
5829 ::core::option::Option<Self>,
5830 #root::sql::ExecError,
5831 > {
5832 #root::query::QuerySet::<Self>::default()
5833 .earliest(field, pool)
5834 .await
5835 }
5836
5837 #count_method
5838
5839 /// `true` when the table contains at least one row.
5840 /// Eloquent `Model::query()->exists()` / Django
5841 /// `Model.objects.exists()` parity. Thin wrapper over
5842 /// `QuerySet::<Self>::default().exists(pool)`.
5843 ///
5844 /// # Errors
5845 /// As [`ExistsPool::exists`].
5846 ///
5847 /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
5848 pub async fn exists(
5849 pool: &#root::sql::Pool,
5850 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5851 use #root::sql::ExistsPool as _;
5852 #root::query::QuerySet::<Self>::default()
5853 .exists(pool)
5854 .await
5855 }
5856
5857 /// Inverse of [`Self::exists`] — returns `true` when
5858 /// the table has zero rows. Eloquent
5859 /// `Model::doesntExist()` parity.
5860 ///
5861 /// # Errors
5862 /// As [`Self::exists`].
5863 pub async fn doesnt_exist(
5864 pool: &#root::sql::Pool,
5865 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5866 Self::exists(pool).await.map(|e| !e)
5867 }
5868
5869 /// Eloquent `Model::query()->whereKey($pk)->exists()` —
5870 /// `true` when a row with primary key `pk` exists in
5871 /// the table. Sugar over
5872 /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
5873 ///
5874 /// Differs from [`Self::exists`] (which checks "any row
5875 /// in the table") by checking a specific PK existence.
5876 /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
5877 /// because the row is never materialized — the SQL is
5878 /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
5879 ///
5880 /// # Errors
5881 /// As [`#root::sql::ExistsPool::contains_pk`].
5882 pub async fn contains_pk(
5883 pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
5884 pool: &#root::sql::Pool,
5885 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5886 use #root::sql::ExistsPool as _;
5887 #root::query::QuerySet::<Self>::default()
5888 .contains_pk(pool, pk)
5889 .await
5890 }
5891
5892 #sum_method
5893 #avg_method
5894 #min_method
5895 #max_method
5896
5897 /// Internal: forward to
5898 /// [`#root::sql::model_shortcuts::aggregate_one_pool`].
5899 /// Backs `sum` / `avg` / `min` / `max`.
5900 #[doc(hidden)]
5901 pub async fn __aggregate_one_pool<U>(
5902 col: &str,
5903 build: fn(&'static str) -> #root::core::AggregateExpr,
5904 pool: &#root::sql::Pool,
5905 ) -> ::core::result::Result<
5906 ::core::option::Option<U>,
5907 #root::sql::ExecError,
5908 >
5909 where
5910 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
5911 + #root::sql::MaybeMyFromRow
5912 + #root::sql::MaybeSqliteFromRow
5913 + ::core::marker::Send
5914 + ::core::marker::Unpin,
5915 {
5916 #root::sql::model_shortcuts::aggregate_one_pool::<Self, U>(col, build, pool)
5917 .await
5918 }
5919
5920 /// Fetch every row of this model from `pool`. Eloquent
5921 /// `Model::all()` parity — a thin wrapper over
5922 /// `QuerySet::<Self>::default().fetch(pool)`.
5923 ///
5924 /// **Use with care on large tables** — there's no
5925 /// pagination or limit; the entire table is materialized
5926 /// into memory. For anything beyond fixture / lookup
5927 /// tables, page through `QuerySet::<Self>::default()
5928 /// .order_by(...).limit(N).offset(M).fetch(pool)`
5929 /// or stream via `.iterator(chunk_size)`.
5930 ///
5931 /// # Errors
5932 /// As [`FetcherPool::fetch`].
5933 ///
5934 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5935 pub async fn all(
5936 pool: &#root::sql::Pool,
5937 ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
5938 {
5939 use #root::sql::FetcherPool as _;
5940 #root::query::QuerySet::<Self>::default()
5941 .fetch(pool)
5942 .await
5943 }
5944
5945 /// Look up every row whose primary key is in `pks`.
5946 /// Returns the matching rows in **inventory** order — NOT
5947 /// the order of `pks`. Empty `pks` returns an empty
5948 /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
5949 /// with a list) / Django `Model.objects.filter(pk__in=[...])`
5950 /// parity.
5951 ///
5952 /// Thin wrapper over `QuerySet::<Self>::default()
5953 /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
5954 /// Caller-supplied PKs that don't match a row are
5955 /// silently skipped (the returned vec is shorter than
5956 /// the input list). For an order-preserving / "fail
5957 /// when any missing" variant, build the queryset
5958 /// explicitly with `in_bulk` instead.
5959 ///
5960 /// Accepts any iterable whose elements are
5961 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
5962 /// `Vec<Uuid>`, etc.
5963 ///
5964 /// # Errors
5965 /// As [`FetcherPool::fetch`].
5966 ///
5967 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5968 pub async fn find_many<V>(
5969 pks: impl ::core::iter::IntoIterator<Item = V>,
5970 pool: &#root::sql::Pool,
5971 ) -> ::core::result::Result<
5972 ::std::vec::Vec<Self>,
5973 #root::sql::ExecError,
5974 >
5975 where
5976 V: ::core::convert::Into<#root::core::SqlValue>,
5977 {
5978 use #root::sql::FetcherPool as _;
5979 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5980 pks.into_iter().map(::core::convert::Into::into).collect();
5981 if _values.is_empty() {
5982 return ::core::result::Result::Ok(::std::vec::Vec::new());
5983 }
5984 let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
5985 #root::query::QuerySet::<Self>::default()
5986 .filter(&_key, #root::core::SqlValue::List(_values))
5987 .fetch(pool)
5988 .await
5989 }
5990
5991 /// Look up the row whose primary key equals `pk`. Returns
5992 /// `Ok(None)` when no row matches; this is the
5993 /// non-throwing counterpart of Django's `.get(pk=…)`
5994 /// (which raises `DoesNotExist`). Eloquent `Model::find`
5995 /// shape — accepts any value `Into<SqlValue>`.
5996 ///
5997 /// One-liner shortcut for the common
5998 /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
5999 /// .limit(1).fetch(pool).await?.into_iter().next()`
6000 /// dance.
6001 ///
6002 /// # Errors
6003 /// As [`FetcherPool::fetch`].
6004 ///
6005 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6006 pub async fn find(
6007 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6008 pool: &#root::sql::Pool,
6009 ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6010 {
6011 use #root::sql::FetcherPool as _;
6012 let _pk_val: #root::core::SqlValue = pk.into();
6013 let mut _rows: ::std::vec::Vec<Self> =
6014 #root::query::QuerySet::<Self>::default()
6015 .filter(::core::stringify!(#pk_ident), _pk_val)
6016 .limit(1)
6017 .fetch(pool)
6018 .await?;
6019 ::core::result::Result::Ok(_rows.into_iter().next())
6020 }
6021
6022 /// Look up the row whose primary key equals `pk`. Errors
6023 /// when no row matches — the throwing counterpart of
6024 /// [`Self::find_pool`]. Eloquent `Model::findOrFail` /
6025 /// Django `Model.objects.get(pk=…)` (which raises
6026 /// `DoesNotExist`) parity.
6027 ///
6028 /// Translates the miss into
6029 /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6030 /// so callers can `?`-bubble straight through the typical
6031 /// `ExecError` error chain.
6032 ///
6033 /// # Errors
6034 /// As [`Self::find_pool`]; additionally
6035 /// [`sqlx::Error::RowNotFound`] when no row matches.
6036 ///
6037 /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6038 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6039 pub async fn find_or_fail(
6040 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6041 pool: &#root::sql::Pool,
6042 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6043 match Self::find(pk, pool).await? {
6044 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6045 ::core::option::Option::None => ::core::result::Result::Err(
6046 #root::sql::ExecError::Driver(
6047 #root::sql::sqlx::Error::RowNotFound,
6048 ),
6049 ),
6050 }
6051 }
6052
6053 /// Look up multiple rows by primary key, **failing** if
6054 /// any requested PK is missing. Eloquent
6055 /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6056 /// inventory order (NOT request order — same as
6057 /// [`Self::find_many`]).
6058 ///
6059 /// Empty `pks` returns an empty `Vec` (no rows requested
6060 /// → nothing to fail on).
6061 ///
6062 /// Differs from [`Self::find_many`] in that this method
6063 /// requires every PK to resolve. If even one is missing,
6064 /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6065 /// in `ExecError::Driver`). Rows are deduped at the SQL
6066 /// layer, so passing the same PK twice in `pks` counts as
6067 /// one expected row.
6068 ///
6069 /// # Errors
6070 /// As [`Self::find_many`], plus `RowNotFound` when the
6071 /// returned row count is less than the count of distinct
6072 /// requested PKs.
6073 pub async fn find_many_or_fail<V>(
6074 pks: impl ::core::iter::IntoIterator<Item = V>,
6075 pool: &#root::sql::Pool,
6076 ) -> ::core::result::Result<
6077 ::std::vec::Vec<Self>,
6078 #root::sql::ExecError,
6079 >
6080 where
6081 V: ::core::convert::Into<#root::core::SqlValue>,
6082 {
6083 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6084 pks.into_iter().map(::core::convert::Into::into).collect();
6085 if _values.is_empty() {
6086 return ::core::result::Result::Ok(::std::vec::Vec::new());
6087 }
6088 // Dedup the requested PK list before counting so
6089 // duplicate-PK requests don't false-fail (the SQL
6090 // `IN (...)` clause naturally dedups too).
6091 let mut _seen: ::std::collections::HashSet<
6092 ::std::string::String,
6093 > = ::std::collections::HashSet::new();
6094 for v in &_values {
6095 _seen.insert(v.to_display_string());
6096 }
6097 let _expected = _seen.len();
6098 let _rows = Self::find_many(_values, pool).await?;
6099 if _rows.len() < _expected {
6100 return ::core::result::Result::Err(
6101 #root::sql::ExecError::Driver(
6102 #root::sql::sqlx::Error::RowNotFound,
6103 ),
6104 );
6105 }
6106 ::core::result::Result::Ok(_rows)
6107 }
6108
6109 /// Find by primary key or run `fallback` to produce a
6110 /// default row to return. Eloquent
6111 /// `Model::findOr($pk, fn() => …)` parity.
6112 ///
6113 /// Unlike [`Self::find_or_fail_pool`] (which raises on
6114 /// miss), this is the "give me something sensible"
6115 /// branch: typical use is "fetch the user's row, else
6116 /// fall back to an anonymous/guest stub".
6117 ///
6118 /// `fallback` runs only when no row matches — the DB
6119 /// round-trip happens unconditionally.
6120 ///
6121 /// # Errors
6122 /// As [`Self::find_pool`].
6123 pub async fn find_or<F>(
6124 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6125 pool: &#root::sql::Pool,
6126 fallback: F,
6127 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6128 where
6129 F: ::core::ops::FnOnce() -> Self,
6130 {
6131 ::core::result::Result::Ok(
6132 Self::find(pk, pool).await?.unwrap_or_else(fallback),
6133 )
6134 }
6135
6136 /// Same as [`Self::find_or`] but also returns a `bool`
6137 /// indicating whether the row was found in the DB
6138 /// (`true`) or freshly built from `fallback` (`false`).
6139 /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6140 /// in PHP the returned model also exposes `->exists`;
6141 /// here that's surfaced as the second tuple element.
6142 ///
6143 /// Useful for edit-or-create form handlers where the
6144 /// caller needs to know whether to PATCH or POST when
6145 /// the user submits the form.
6146 ///
6147 /// # Errors
6148 /// As [`Self::find`].
6149 pub async fn find_or_new<F>(
6150 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6151 pool: &#root::sql::Pool,
6152 fallback: F,
6153 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6154 where
6155 F: ::core::ops::FnOnce() -> Self,
6156 {
6157 match Self::find(pk, pool).await? {
6158 ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6159 ::core::option::Option::None => {
6160 ::core::result::Result::Ok((fallback(), false))
6161 }
6162 }
6163 }
6164
6165 /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6166 /// [`Self::find_or_new`] but **persists** the new row
6167 /// when the PK isn't found. Returns `(row, exists: bool)`
6168 /// — `exists=true` when the PK was found,
6169 /// `false` when a fresh row was inserted.
6170 ///
6171 /// ```ignore
6172 /// let (post, found) = Post::find_or_insert(
6173 /// pk,
6174 /// &pool,
6175 /// || Post { id: Auto::default(), title: "new".into() },
6176 /// ).await?;
6177 /// // `found` true → returned existing row;
6178 /// // `found` false → fallback was inserted, `post.id` populated.
6179 /// ```
6180 ///
6181 /// **Caveat**: two concurrent calls that both miss the
6182 /// find can both INSERT, violating uniqueness. Wrap in a
6183 /// transaction or rely on a UNIQUE constraint + handle
6184 /// the conflict error if you need race-free semantics.
6185 ///
6186 /// # Errors
6187 /// As [`Self::find`] and [`Self::save_pool`].
6188 pub async fn find_or_insert<F>(
6189 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6190 pool: &#root::sql::Pool,
6191 fallback: F,
6192 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6193 where
6194 F: ::core::ops::FnOnce() -> Self,
6195 {
6196 if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6197 return ::core::result::Result::Ok((_row, true));
6198 }
6199 let mut _new = fallback();
6200 _new.save_pool(pool).await?;
6201 ::core::result::Result::Ok((_new, false))
6202 }
6203
6204 /// Fetch the first row of the table, or run `fallback`
6205 /// when the table is empty. Eloquent
6206 /// `Model::firstOr(fn() => …)` parity.
6207 ///
6208 /// # Errors
6209 /// As [`Self::first_pool`].
6210 pub async fn first_or<F>(
6211 pool: &#root::sql::Pool,
6212 fallback: F,
6213 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6214 where
6215 F: ::core::ops::FnOnce() -> Self,
6216 {
6217 // Route through the queryset rather than `Self::first`,
6218 // which is suppressed on models with a field named
6219 // `first` (field/shortcut collision guard).
6220 ::core::result::Result::Ok(
6221 #root::query::QuerySet::<Self>::default()
6222 .first(pool)
6223 .await?
6224 .unwrap_or_else(fallback),
6225 )
6226 }
6227
6228 /// Fetch exactly one row matching `<col> = <val>`. Errors
6229 /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6230 /// or more than one matches
6231 /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6232 /// Eloquent `Model::sole($col, $val)` parity.
6233 ///
6234 /// # Errors
6235 /// As [`Self::where_pool`] plus the explicit
6236 /// `RowNotFound` / `MultipleRowsReturned` cases above.
6237 pub async fn sole(
6238 col: &str,
6239 val: impl ::core::convert::Into<#root::core::SqlValue>,
6240 pool: &#root::sql::Pool,
6241 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6242 let mut _rows = Self::where_(col, val, pool).await?;
6243 match _rows.len() {
6244 0 => ::core::result::Result::Err(
6245 #root::sql::ExecError::Driver(
6246 #root::sql::sqlx::Error::RowNotFound,
6247 ),
6248 ),
6249 1 => ::core::result::Result::Ok(_rows.remove(0)),
6250 n => ::core::result::Result::Err(
6251 #root::sql::ExecError::MultipleRowsReturned {
6252 op: "sole",
6253 table: <Self as #root::core::Model>::SCHEMA.name,
6254 count: n,
6255 },
6256 ),
6257 }
6258 }
6259 }
6260 } else {
6261 quote!()
6262 };
6263
6264 // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6265 // the non-audited `_pool` methods but execute against an open
6266 // `PoolTx` so the writes participate in the caller's transaction.
6267 // Auditing inside TX is deferred; these always use the plain
6268 // executor primitives regardless of whether the model is audited.
6269 let tx_insert_method = if fields.has_auto {
6270 let pushes = &fields.insert_pushes;
6271 let returning_cols = &fields.returning_cols;
6272 quote! {
6273 /// Insert this row inside an open transaction, populating
6274 /// any `Auto<T>` PK from the auto-assigned value. Works
6275 /// against any backend that `tx` wraps.
6276 ///
6277 /// # Errors
6278 /// As [`Self::insert_pool`].
6279 pub async fn insert_tx(
6280 &mut self,
6281 tx: &mut #root::sql::PoolTx<'_>,
6282 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6283 let mut _columns: ::std::vec::Vec<&'static str> =
6284 ::std::vec::Vec::new();
6285 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6286 ::std::vec::Vec::new();
6287 #( #pushes )*
6288 let _query = #root::core::InsertQuery {
6289 model: <Self as #root::core::Model>::SCHEMA,
6290 columns: _columns,
6291 values: _values,
6292 returning: ::std::vec![ #( #returning_cols ),* ],
6293 on_conflict: ::core::option::Option::None,
6294 };
6295 let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6296 #root::sql::apply_auto_pk(_result, self)
6297 }
6298 }
6299 } else {
6300 let insert_columns = &fields.insert_columns;
6301 let insert_values = &fields.insert_values;
6302 quote! {
6303 /// Insert this row inside an open transaction.
6304 ///
6305 /// # Errors
6306 /// As [`Self::insert_pool`].
6307 pub async fn insert_tx(
6308 &self,
6309 tx: &mut #root::sql::PoolTx<'_>,
6310 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6311 let _query = #root::core::InsertQuery {
6312 model: <Self as #root::core::Model>::SCHEMA,
6313 columns: ::std::vec![ #( #insert_columns ),* ],
6314 values: ::std::vec![ #( #insert_values ),* ],
6315 returning: ::std::vec::Vec::new(),
6316 on_conflict: ::core::option::Option::None,
6317 };
6318 #root::sql::insert_tx(tx, &_query).await
6319 }
6320 }
6321 };
6322
6323 let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6324 let pk_column_lit = pk_col.as_str();
6325 let assignments = &fields.update_assignments;
6326 let dispatch_unset = if fields.pk_is_auto {
6327 quote! {
6328 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6329 return self.insert_tx(tx).await.map(|()| 1u64);
6330 }
6331 }
6332 } else {
6333 quote!()
6334 };
6335 quote! {
6336 /// Save this row inside an open transaction. `INSERT` when
6337 /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6338 /// PK. Works against any backend that `tx` wraps.
6339 ///
6340 /// # Errors
6341 /// As [`Self::save_pool`].
6342 pub async fn save_tx(
6343 &mut self,
6344 tx: &mut #root::sql::PoolTx<'_>,
6345 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6346 #dispatch_unset
6347 let _query = #root::core::UpdateQuery {
6348 model: <Self as #root::core::Model>::SCHEMA,
6349 set: ::std::vec![ #( #assignments ),* ],
6350 where_clause: #root::core::WhereExpr::Predicate(
6351 #root::core::Filter {
6352 column: #pk_column_lit,
6353 op: #root::core::Op::Eq,
6354 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6355 ::core::clone::Clone::clone(&self.#pk_ident)
6356 ),
6357 }
6358 ),
6359 };
6360 let _affected = #root::sql::update_tx(tx, &_query).await?;
6361 ::core::result::Result::Ok(_affected)
6362 }
6363 }
6364 } else {
6365 quote!()
6366 };
6367
6368 let tx_delete_method = {
6369 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6370 let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6371 if let Some(pk_ident) = pk_ident_for_tx {
6372 quote! {
6373 /// Delete the row identified by this instance's PK
6374 /// inside an open transaction. Works against any backend
6375 /// that `tx` wraps.
6376 ///
6377 /// # Errors
6378 /// As [`Self::delete_pool`].
6379 pub async fn delete_tx(
6380 &self,
6381 tx: &mut #root::sql::PoolTx<'_>,
6382 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6383 let _query = #root::core::DeleteQuery {
6384 model: <Self as #root::core::Model>::SCHEMA,
6385 where_clause: #root::core::WhereExpr::Predicate(
6386 #root::core::Filter {
6387 column: #pk_column_lit,
6388 op: #root::core::Op::Eq,
6389 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6390 ::core::clone::Clone::clone(&self.#pk_ident)
6391 ),
6392 }
6393 ),
6394 };
6395 #root::sql::delete_tx(tx, &_query).await
6396 }
6397 }
6398 } else {
6399 quote!()
6400 }
6401 };
6402
6403 // Update emission captures both BEFORE and AFTER state — runs an
6404 // extra SELECT against `_executor` BEFORE the UPDATE, captures
6405 // each tracked field's prior value, then after the UPDATE diffs
6406 // against the in-memory `&self`. `diff_changes` drops unchanged
6407 // columns so the JSON only contains the actual delta.
6408 //
6409 // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6410 // and binds `_audit_before_pairs`; `audit_update_post` runs
6411 // after the UPDATE and emits the PendingEntry.
6412 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6413 audited_fields
6414 {
6415 if tracked.is_empty() {
6416 (quote!(), quote!())
6417 } else {
6418 let select_cols: String = tracked
6419 .iter()
6420 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
6421 .collect::<Vec<_>>()
6422 .join(", ");
6423 let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6424 let select_cols_lit = select_cols;
6425 let pk_column_lit_for_select = pk_column_for_select;
6426 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6427 if fields.pk_is_auto {
6428 quote!(self.#pk_ident.get().copied().unwrap_or_default())
6429 } else {
6430 quote!(::core::clone::Clone::clone(&self.#pk_ident))
6431 }
6432 } else {
6433 quote!(0_i64)
6434 };
6435 let before_pairs = tracked.iter().map(|c| {
6436 let column_lit = c.column.as_str();
6437 let value_ty = &c.value_ty;
6438 quote! {
6439 (
6440 #column_lit,
6441 match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6442 &_audit_before_row, #column_lit,
6443 ) {
6444 ::core::result::Result::Ok(v) => {
6445 #root::__serde_json::to_value(&v)
6446 .unwrap_or(#root::__serde_json::Value::Null)
6447 }
6448 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6449 },
6450 )
6451 }
6452 });
6453 let after_pairs = tracked.iter().map(|c| {
6454 let column_lit = c.column.as_str();
6455 let ident = &c.ident;
6456 quote! {
6457 (
6458 #column_lit,
6459 #root::__serde_json::to_value(&self.#ident)
6460 .unwrap_or(#root::__serde_json::Value::Null),
6461 )
6462 }
6463 });
6464 let pk_str = audit_pk_to_string.clone();
6465 let pre = quote! {
6466 let _audit_select_sql = ::std::format!(
6467 r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
6468 #select_cols_lit,
6469 <Self as #root::core::Model>::SCHEMA.table,
6470 #pk_column_lit_for_select,
6471 );
6472 let _audit_before_pairs:
6473 ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6474 match #root::sql::sqlx::query(&_audit_select_sql)
6475 .bind(#pk_value_for_bind)
6476 .fetch_optional(&mut *_executor)
6477 .await
6478 {
6479 ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
6480 ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
6481 }
6482 _ => ::core::option::Option::None,
6483 };
6484 };
6485 let post = quote! {
6486 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6487 let _audit_after:
6488 ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
6489 ::std::vec![ #( #after_pairs ),* ];
6490 let _audit_entry = #root::audit::PendingEntry {
6491 entity_table: <Self as #root::core::Model>::SCHEMA.table,
6492 entity_pk: #pk_str,
6493 operation: #root::audit::AuditOp::Update,
6494 source: #root::audit::current_source(),
6495 changes: #root::audit::diff_changes(
6496 &_audit_before,
6497 &_audit_after,
6498 ),
6499 };
6500 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6501 }
6502 };
6503 (pre, post)
6504 }
6505 } else {
6506 (quote!(), quote!())
6507 };
6508
6509 // Bulk-insert audit: capture every row's tracked fields after the
6510 // RETURNING populates each PK, then push one batched INSERT INTO
6511 // audit_log via `emit_many`. One round-trip regardless of N rows.
6512 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6513 let row_pk_str = if let Some((pk_ident, _)) = primary_key {
6514 if fields.pk_is_auto {
6515 quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
6516 } else {
6517 quote!(::std::format!("{}", &_row.#pk_ident))
6518 }
6519 } else {
6520 quote!(::std::string::String::new())
6521 };
6522 let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
6523 let column_lit = c.column.as_str();
6524 let ident = &c.ident;
6525 quote! {
6526 (
6527 #column_lit,
6528 #root::__serde_json::to_value(&_row.#ident)
6529 .unwrap_or(#root::__serde_json::Value::Null),
6530 )
6531 }
6532 });
6533 quote! {
6534 let _audit_source = #root::audit::current_source();
6535 let mut _audit_entries:
6536 ::std::vec::Vec<#root::audit::PendingEntry> =
6537 ::std::vec::Vec::with_capacity(rows.len());
6538 for _row in rows.iter() {
6539 _audit_entries.push(#root::audit::PendingEntry {
6540 entity_table: <Self as #root::core::Model>::SCHEMA.table,
6541 entity_pk: #row_pk_str,
6542 operation: #root::audit::AuditOp::Create,
6543 source: _audit_source.clone(),
6544 changes: #root::audit::snapshot_changes(&[
6545 #( #row_pairs ),*
6546 ]),
6547 });
6548 }
6549 #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6550 }
6551 } else {
6552 quote!()
6553 };
6554
6555 let save_method = if fields.pk_is_auto {
6556 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6557 let pk_column_lit = pk_column.as_str();
6558 let assignments = &fields.update_assignments;
6559 let upsert_cols = &fields.upsert_update_columns;
6560 let upsert_pushes = &fields.insert_pushes;
6561 let upsert_returning = &fields.returning_cols;
6562 let upsert_auto_assigns = &fields.auto_assigns;
6563 // Conflict target: prefer the first declared `unique_together`
6564 // when it exists. Plain `Auto<T>` PKs are server-assigned via
6565 // `BIGSERIAL` and never collide on insert, so a PK-only target
6566 // would silently turn `upsert()` into "always-insert" for
6567 // surrogate-PK models with composite UNIQUE constraints — see
6568 // `RolePermission` / `UserRole` / `UserPermission` in the
6569 // tenancy permission engine. When no `unique_together` is
6570 // declared we keep the PK target (the original behaviour).
6571 let upsert_target_columns: Vec<String> = indexes
6572 .iter()
6573 .find(|i| i.unique && !i.columns.is_empty())
6574 .map(|i| i.columns.clone())
6575 .unwrap_or_else(|| vec![pk_column.clone()]);
6576 let upsert_target_lits = upsert_target_columns
6577 .iter()
6578 .map(String::as_str)
6579 .collect::<Vec<_>>();
6580 let conflict_clause = if fields.upsert_update_columns.is_empty() {
6581 quote!(#root::core::ConflictClause::DoNothing)
6582 } else {
6583 quote!(#root::core::ConflictClause::DoUpdate {
6584 target: ::std::vec![ #( #upsert_target_lits ),* ],
6585 update_columns: ::std::vec![ #( #upsert_cols ),* ],
6586 })
6587 };
6588 Some(quote! {
6589 /// Insert this row if its `Auto<T>` primary key is
6590 /// `Unset`, otherwise update the existing row matching the
6591 /// PK. Mirrors Django's `save()` — caller doesn't need to
6592 /// pick `insert` vs the bulk-update path manually.
6593 ///
6594 /// On the insert branch, populates the PK from `RETURNING`
6595 /// (same behavior as `insert`). On the update branch,
6596 /// writes every non-PK column back; if no row matches the
6597 /// PK, returns `Ok(())` silently.
6598 ///
6599 /// Only generated when the primary key is declared as
6600 /// `Auto<T>`. Models with a manually-managed PK must use
6601 /// `insert` or the QuerySet update builder.
6602 ///
6603 /// # Errors
6604 /// Returns [`#root::sql::ExecError`] for SQL-writing
6605 /// or driver failures.
6606 #[cfg(feature = "postgres")]
6607 pub async fn save(
6608 &mut self,
6609 pool: &#root::sql::sqlx::PgPool,
6610 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6611 #pool_to_save_on
6612 }
6613
6614 /// Like [`Self::save`] but accepts any sqlx executor —
6615 /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6616 /// escape hatch for tenant-scoped writes: schema-mode
6617 /// tenants share the registry pool but rely on a per-
6618 /// checkout `SET search_path`, so passing `&PgPool` would
6619 /// silently hit the wrong schema. Acquire a connection
6620 /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6621 ///
6622 /// # Errors
6623 /// As [`Self::save`].
6624 #[cfg(feature = "postgres")]
6625 pub async fn save_on #executor_generics (
6626 &mut self,
6627 #executor_param,
6628 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6629 #executor_where
6630 {
6631 // #1029 — INSERT writes exactly one row → 1; UPDATE returns
6632 // the rows-affected count (0 when the PK no longer exists,
6633 // the Django 6.0 `Model.NotUpdated` signal).
6634 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6635 return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
6636 }
6637 #audit_update_pre
6638 let _query = #root::core::UpdateQuery {
6639 model: <Self as #root::core::Model>::SCHEMA,
6640 set: ::std::vec![ #( #assignments ),* ],
6641 where_clause: #root::core::WhereExpr::Predicate(
6642 #root::core::Filter {
6643 column: #pk_column_lit,
6644 op: #root::core::Op::Eq,
6645 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6646 ::core::clone::Clone::clone(&self.#pk_ident)
6647 ),
6648 }
6649 ),
6650 };
6651 let _affected = #root::sql::__macro_internals::update_on(
6652 #executor_passes_to_data_write,
6653 &_query,
6654 ).await?;
6655 #audit_update_post
6656 ::core::result::Result::Ok(_affected)
6657 }
6658
6659 /// Per-call override for the audit source. Runs
6660 /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6661 /// scope so the resulting audit entry records `source`
6662 /// instead of the task-local default. Useful for seed
6663 /// scripts and one-off CLI tools that don't sit inside an
6664 /// admin handler. The override applies only to this call;
6665 /// no global state changes.
6666 ///
6667 /// # Errors
6668 /// As [`Self::save_on`].
6669 #[cfg(feature = "postgres")]
6670 pub async fn save_on_with #executor_generics (
6671 &mut self,
6672 #executor_param,
6673 source: #root::audit::AuditSource,
6674 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6675 #executor_where
6676 {
6677 #root::audit::with_source(source, self.save_on(_executor)).await
6678 }
6679
6680 /// Insert this row or update it in-place if the primary key already
6681 /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6682 ///
6683 /// With `Auto::Unset` PK the server assigns a new key and no conflict
6684 /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6685 /// inserted if absent or all non-PK columns are overwritten if present.
6686 ///
6687 /// # Errors
6688 /// As [`Self::insert_on`].
6689 #[cfg(feature = "postgres")]
6690 pub async fn upsert(
6691 &mut self,
6692 pool: &#root::sql::sqlx::PgPool,
6693 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6694 #pool_to_upsert_on
6695 }
6696
6697 /// Like [`Self::upsert`] but accepts any sqlx executor.
6698 /// See [`Self::save_on`] for tenancy-scoped rationale.
6699 ///
6700 /// # Errors
6701 /// As [`Self::upsert`].
6702 #[cfg(feature = "postgres")]
6703 pub async fn upsert_on #executor_generics (
6704 &mut self,
6705 #executor_param,
6706 ) -> ::core::result::Result<(), #root::sql::ExecError>
6707 #executor_where
6708 {
6709 let mut _columns: ::std::vec::Vec<&'static str> =
6710 ::std::vec::Vec::new();
6711 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6712 ::std::vec::Vec::new();
6713 #( #upsert_pushes )*
6714 let query = #root::core::InsertQuery {
6715 model: <Self as #root::core::Model>::SCHEMA,
6716 columns: _columns,
6717 values: _values,
6718 returning: ::std::vec![ #( #upsert_returning ),* ],
6719 on_conflict: ::core::option::Option::Some(#conflict_clause),
6720 };
6721 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
6722 #executor_passes_to_data_write,
6723 &query,
6724 ).await?;
6725 let _returning_row = &_returning_row_v;
6726 #( #upsert_auto_assigns )*
6727 ::core::result::Result::Ok(())
6728 }
6729 })
6730 } else {
6731 None
6732 };
6733
6734 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6735 let pk_column_lit = pk_column.as_str();
6736 // Optional `soft_delete_on` / `restore_on` companions when the
6737 // model has a `#[rustango(soft_delete)]` column. They land
6738 // alongside the regular `delete_on` so callers have both
6739 // options — a hard delete (audit-tracked as a real DELETE) and
6740 // a logical delete (audit-tracked as an UPDATE setting the
6741 // deleted_at column to NOW()).
6742 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6743 let col_lit = col;
6744 let sd_field_ident = fields
6745 .soft_delete_field_ident
6746 .clone()
6747 .expect("soft_delete_column without ident");
6748 quote! {
6749 /// Soft-delete this row by setting its
6750 /// `#[rustango(soft_delete)]` column to `NOW()`.
6751 /// Mirrors Django's `SoftDeleteModel.delete()` shape:
6752 /// the row stays in the table; query helpers can
6753 /// filter it out by checking the column for `IS NOT
6754 /// NULL`.
6755 ///
6756 /// # Errors
6757 /// As [`Self::delete`].
6758 #[cfg(feature = "postgres")]
6759 pub async fn soft_delete_on #executor_generics (
6760 &self,
6761 #executor_param,
6762 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6763 #executor_where
6764 {
6765 let _query = #root::core::UpdateQuery {
6766 model: <Self as #root::core::Model>::SCHEMA,
6767 set: ::std::vec![
6768 #root::core::Assignment {
6769 column: #col_lit,
6770 value: ::core::convert::Into::<#root::core::Expr>::into(
6771 ::core::convert::Into::<#root::core::SqlValue>::into(
6772 #root::__chrono::Utc::now()
6773 )
6774 ),
6775 },
6776 ],
6777 where_clause: #root::core::WhereExpr::Predicate(
6778 #root::core::Filter {
6779 column: #pk_column_lit,
6780 op: #root::core::Op::Eq,
6781 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6782 ::core::clone::Clone::clone(&self.#pk_ident)
6783 ),
6784 }
6785 ),
6786 };
6787 let _affected = #root::sql::__macro_internals::update_on(
6788 #executor_passes_to_data_write,
6789 &_query,
6790 ).await?;
6791 #audit_softdelete_emit
6792 ::core::result::Result::Ok(_affected)
6793 }
6794
6795 /// Inverse of [`Self::soft_delete_on`] — clears the
6796 /// soft-delete column back to NULL so the row is
6797 /// considered live again.
6798 ///
6799 /// # Errors
6800 /// As [`Self::delete`].
6801 #[cfg(feature = "postgres")]
6802 pub async fn restore_on #executor_generics (
6803 &self,
6804 #executor_param,
6805 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6806 #executor_where
6807 {
6808 let _query = #root::core::UpdateQuery {
6809 model: <Self as #root::core::Model>::SCHEMA,
6810 set: ::std::vec![
6811 #root::core::Assignment {
6812 column: #col_lit,
6813 value: ::core::convert::Into::<#root::core::Expr>::into(
6814 #root::core::SqlValue::Null
6815 ),
6816 },
6817 ],
6818 where_clause: #root::core::WhereExpr::Predicate(
6819 #root::core::Filter {
6820 column: #pk_column_lit,
6821 op: #root::core::Op::Eq,
6822 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6823 ::core::clone::Clone::clone(&self.#pk_ident)
6824 ),
6825 }
6826 ),
6827 };
6828 let _affected = #root::sql::__macro_internals::update_on(
6829 #executor_passes_to_data_write,
6830 &_query,
6831 ).await?;
6832 #audit_restore_emit
6833 ::core::result::Result::Ok(_affected)
6834 }
6835
6836 /// Tri-dialect counterpart of [`Self::soft_delete_on`]
6837 /// — takes [`#root::sql::Pool`] and dispatches per
6838 /// backend. Eloquent `Model::delete()` semantics on
6839 /// soft-delete-enabled models (closes #821 partial).
6840 ///
6841 /// Sets the `#[rustango(soft_delete)]` column to
6842 /// `NOW()` on every backend. Query helpers
6843 /// (`QuerySet::active()` / `only_trashed()`,
6844 /// `soft_delete::active_filter` /
6845 /// `compose_with_active`) filter trashed rows out by
6846 /// reading `IS NULL` on the same column.
6847 ///
6848 /// # Errors
6849 /// As [`#root::sql::update_pool`].
6850 pub async fn soft_delete(
6851 &self,
6852 pool: &#root::sql::Pool,
6853 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6854 let _query = #root::core::UpdateQuery {
6855 model: <Self as #root::core::Model>::SCHEMA,
6856 set: ::std::vec![
6857 #root::core::Assignment {
6858 column: #col_lit,
6859 value: ::core::convert::Into::<#root::core::Expr>::into(
6860 ::core::convert::Into::<#root::core::SqlValue>::into(
6861 #root::__chrono::Utc::now()
6862 )
6863 ),
6864 },
6865 ],
6866 where_clause: #root::core::WhereExpr::Predicate(
6867 #root::core::Filter {
6868 column: #pk_column_lit,
6869 op: #root::core::Op::Eq,
6870 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6871 ::core::clone::Clone::clone(&self.#pk_ident)
6872 ),
6873 }
6874 ),
6875 };
6876 #root::sql::update_pool(pool, &_query).await
6877 }
6878
6879 /// Tri-dialect counterpart of [`Self::restore_on`].
6880 /// Clears the `#[rustango(soft_delete)]` column back
6881 /// to `NULL`, marking the row live again. Eloquent
6882 /// `Model::restore()` parity.
6883 ///
6884 /// # Errors
6885 /// As [`#root::sql::update_pool`].
6886 pub async fn restore(
6887 &self,
6888 pool: &#root::sql::Pool,
6889 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6890 let _query = #root::core::UpdateQuery {
6891 model: <Self as #root::core::Model>::SCHEMA,
6892 set: ::std::vec![
6893 #root::core::Assignment {
6894 column: #col_lit,
6895 value: ::core::convert::Into::<#root::core::Expr>::into(
6896 #root::core::SqlValue::Null
6897 ),
6898 },
6899 ],
6900 where_clause: #root::core::WhereExpr::Predicate(
6901 #root::core::Filter {
6902 column: #pk_column_lit,
6903 op: #root::core::Op::Eq,
6904 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6905 ::core::clone::Clone::clone(&self.#pk_ident)
6906 ),
6907 }
6908 ),
6909 };
6910 #root::sql::update_pool(pool, &_query).await
6911 }
6912
6913 /// Hard-delete this row, ignoring the soft-delete
6914 /// column. Eloquent `Model::forceDelete()` parity —
6915 /// the escape hatch when you need to actually purge
6916 /// data (GDPR, fixture cleanup, etc.).
6917 ///
6918 /// Equivalent to [`Self::delete_pool`] (the framework's
6919 /// non-soft delete) — exposed under the Eloquent name
6920 /// for muscle-memory + so soft-delete-enabled models
6921 /// have all three operations (soft / restore / force)
6922 /// in one place.
6923 ///
6924 /// # Errors
6925 /// As [`Self::delete_pool`].
6926 pub async fn force_delete(
6927 &self,
6928 pool: &#root::sql::Pool,
6929 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6930 Self::delete_pool(self, pool).await
6931 }
6932
6933 /// Returns `true` when this row is soft-deleted (its
6934 /// `#[rustango(soft_delete)]` column is currently
6935 /// set — Eloquent `$model->trashed()` parity).
6936 ///
6937 /// Pure in-memory predicate over `&self`; does not
6938 /// hit the database. Useful in admin/template code
6939 /// like `{% if post.trashed() %}…{% endif %}` and in
6940 /// guard clauses on restore/force-delete flows.
6941 pub fn trashed(&self) -> bool {
6942 ::core::option::Option::is_some(&self.#sd_field_ident)
6943 }
6944
6945 /// Fetch every row whose `#[rustango(soft_delete)]`
6946 /// column is `NULL` (a.k.a. the live, non-trashed
6947 /// rows). Eloquent's default `Model::all()` behavior on
6948 /// a soft-delete model (Eloquent auto-scopes trashed
6949 /// rows out; rustango doesn't have auto-scoping yet —
6950 /// see issue #820 — so this is the explicit shortcut).
6951 ///
6952 /// One-liner over `QuerySet::<Self>::default()
6953 /// .active().fetch(pool)`. Closes #821 partial.
6954 ///
6955 /// # Errors
6956 /// As [`#root::sql::FetcherPool::fetch`].
6957 pub async fn active(
6958 pool: &#root::sql::Pool,
6959 ) -> ::core::result::Result<
6960 ::std::vec::Vec<Self>,
6961 #root::sql::ExecError,
6962 > {
6963 use #root::sql::FetcherPool as _;
6964 #root::query::QuerySet::<Self>::default()
6965 .active()
6966 .fetch(pool)
6967 .await
6968 }
6969
6970 /// Fetch ONLY soft-deleted rows. Eloquent
6971 /// `Model::onlyTrashed()->get()` parity — drives the
6972 /// admin "Trash" page, restore flows, GDPR purge
6973 /// scans, etc.
6974 ///
6975 /// One-liner over `QuerySet::<Self>::default()
6976 /// .only_trashed().fetch(pool)`. Closes #821
6977 /// partial.
6978 ///
6979 /// # Errors
6980 /// As [`#root::sql::FetcherPool::fetch`].
6981 pub async fn only_trashed(
6982 pool: &#root::sql::Pool,
6983 ) -> ::core::result::Result<
6984 ::std::vec::Vec<Self>,
6985 #root::sql::ExecError,
6986 > {
6987 use #root::sql::FetcherPool as _;
6988 #root::query::QuerySet::<Self>::default()
6989 .only_trashed()
6990 .fetch(pool)
6991 .await
6992 }
6993
6994 /// Fetch every row, both live and soft-deleted.
6995 /// Eloquent `Model::withTrashed()->get()` parity.
6996 ///
6997 /// Today every queryset already includes trashed rows
6998 /// (rustango has no global-scope tracking yet — issue
6999 /// #820), so this is functionally equivalent to
7000 /// [`Self::all_pool`]. Exposed as a named shortcut so
7001 /// soft-delete-aware code reads `Model::with_trashed_pool`
7002 /// rather than `Model::all_pool` — keeps intent visible
7003 /// in callers and stays correct when auto-scoping lands.
7004 ///
7005 /// Closes #821 partial.
7006 ///
7007 /// # Errors
7008 /// As [`#root::sql::FetcherPool::fetch`].
7009 pub async fn with_trashed(
7010 pool: &#root::sql::Pool,
7011 ) -> ::core::result::Result<
7012 ::std::vec::Vec<Self>,
7013 #root::sql::ExecError,
7014 > {
7015 use #root::sql::FetcherPool as _;
7016 #root::query::QuerySet::<Self>::default()
7017 .with_trashed()
7018 .fetch(pool)
7019 .await
7020 }
7021
7022 }
7023 } else {
7024 quote!()
7025 };
7026 quote! {
7027 /// Delete the row identified by this instance's primary key.
7028 ///
7029 /// Returns the number of rows affected (0 or 1).
7030 ///
7031 /// # Errors
7032 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7033 /// driver failures.
7034 #[cfg(feature = "postgres")]
7035 pub async fn delete(
7036 &self,
7037 pool: &#root::sql::sqlx::PgPool,
7038 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7039 #pool_to_delete_on
7040 }
7041
7042 /// Like [`Self::delete`] but accepts any sqlx executor —
7043 /// for tenant-scoped deletes against an explicitly-acquired
7044 /// connection. See [`Self::save_on`] for the rationale.
7045 ///
7046 /// # Errors
7047 /// As [`Self::delete`].
7048 #[cfg(feature = "postgres")]
7049 pub async fn delete_on #executor_generics (
7050 &self,
7051 #executor_param,
7052 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7053 #executor_where
7054 {
7055 let query = #root::core::DeleteQuery {
7056 model: <Self as #root::core::Model>::SCHEMA,
7057 where_clause: #root::core::WhereExpr::Predicate(
7058 #root::core::Filter {
7059 column: #pk_column_lit,
7060 op: #root::core::Op::Eq,
7061 value: ::core::convert::Into::<#root::core::SqlValue>::into(
7062 ::core::clone::Clone::clone(&self.#pk_ident)
7063 ),
7064 }
7065 ),
7066 };
7067 let _affected = #root::sql::__macro_internals::delete_on(
7068 #executor_passes_to_data_write,
7069 &query,
7070 ).await?;
7071 #audit_delete_emit
7072 ::core::result::Result::Ok(_affected)
7073 }
7074
7075 /// Per-call audit-source override for [`Self::delete_on`].
7076 /// See [`Self::save_on_with`] for shape rationale.
7077 ///
7078 /// # Errors
7079 /// As [`Self::delete_on`].
7080 #[cfg(feature = "postgres")]
7081 pub async fn delete_on_with #executor_generics (
7082 &self,
7083 #executor_param,
7084 source: #root::audit::AuditSource,
7085 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7086 #executor_where
7087 {
7088 #root::audit::with_source(source, self.delete_on(_executor)).await
7089 }
7090 #pool_delete_method
7091 #pool_insert_method
7092 #pool_save_method
7093 #refresh_replicate_methods
7094 #tx_delete_method
7095 #tx_insert_method
7096 #tx_save_method
7097 #soft_delete_methods
7098
7099 /// Returns `true` when `other` represents the same DB
7100 /// row as `self` — i.e. their primary keys compare
7101 /// equal. Eloquent `$model->is($other)` parity.
7102 ///
7103 /// Because both arguments are typed `&Self`, the
7104 /// model/table check is automatic — `Post::is` cannot
7105 /// be invoked against a `Comment` at compile time. Only
7106 /// the PK has to be compared at runtime.
7107 pub fn is(&self, other: &Self) -> bool {
7108 self.#pk_ident == other.#pk_ident
7109 }
7110
7111 /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7112 /// parity.
7113 pub fn is_not(&self, other: &Self) -> bool {
7114 self.#pk_ident != other.#pk_ident
7115 }
7116
7117 /// Returns this row's primary-key value as an
7118 /// [`#root::core::SqlValue`]. Eloquent
7119 /// `$model->getKey()` parity.
7120 ///
7121 /// Useful when you need to thread the PK through a
7122 /// generic `Into<SqlValue>`-bound API without knowing the
7123 /// concrete PK type (`i64` vs `Uuid` vs `String`).
7124 #[must_use]
7125 pub fn get_key(&self) -> #root::core::SqlValue {
7126 ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7127 }
7128
7129 }
7130 });
7131
7132 let insert_method = if fields.has_auto {
7133 let pushes = &fields.insert_pushes;
7134 let returning_cols = &fields.returning_cols;
7135 let auto_assigns = &fields.auto_assigns;
7136 quote! {
7137 /// Insert this row into its table. Skips columns whose
7138 /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7139 /// sequence fills them in, then reads each `Auto` column
7140 /// back via `RETURNING` and stores it on `self`.
7141 ///
7142 /// # Errors
7143 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7144 /// driver failures.
7145 #[cfg(feature = "postgres")]
7146 pub async fn insert(
7147 &mut self,
7148 pool: &#root::sql::sqlx::PgPool,
7149 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7150 #pool_to_insert_on
7151 }
7152
7153 /// Like [`Self::insert`] but accepts any sqlx executor.
7154 /// See [`Self::save_on`] for tenancy-scoped rationale.
7155 ///
7156 /// # Errors
7157 /// As [`Self::insert`].
7158 #[cfg(feature = "postgres")]
7159 pub async fn insert_on #executor_generics (
7160 &mut self,
7161 #executor_param,
7162 ) -> ::core::result::Result<(), #root::sql::ExecError>
7163 #executor_where
7164 {
7165 let mut _columns: ::std::vec::Vec<&'static str> =
7166 ::std::vec::Vec::new();
7167 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7168 ::std::vec::Vec::new();
7169 #( #pushes )*
7170 let query = #root::core::InsertQuery {
7171 model: <Self as #root::core::Model>::SCHEMA,
7172 columns: _columns,
7173 values: _values,
7174 returning: ::std::vec![ #( #returning_cols ),* ],
7175 on_conflict: ::core::option::Option::None,
7176 };
7177 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7178 #executor_passes_to_data_write,
7179 &query,
7180 ).await?;
7181 let _returning_row = &_returning_row_v;
7182 #( #auto_assigns )*
7183 #audit_insert_emit
7184 ::core::result::Result::Ok(())
7185 }
7186
7187 /// Per-call audit-source override for [`Self::insert_on`].
7188 /// See [`Self::save_on_with`] for shape rationale.
7189 ///
7190 /// # Errors
7191 /// As [`Self::insert_on`].
7192 #[cfg(feature = "postgres")]
7193 pub async fn insert_on_with #executor_generics (
7194 &mut self,
7195 #executor_param,
7196 source: #root::audit::AuditSource,
7197 ) -> ::core::result::Result<(), #root::sql::ExecError>
7198 #executor_where
7199 {
7200 #root::audit::with_source(source, self.insert_on(_executor)).await
7201 }
7202 }
7203 } else {
7204 let insert_columns = &fields.insert_columns;
7205 let insert_values = &fields.insert_values;
7206 quote! {
7207 /// Insert this row into its table.
7208 ///
7209 /// # Errors
7210 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7211 /// driver failures.
7212 #[cfg(feature = "postgres")]
7213 pub async fn insert(
7214 &self,
7215 pool: &#root::sql::sqlx::PgPool,
7216 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7217 self.insert_on(pool).await
7218 }
7219
7220 /// Like [`Self::insert`] but accepts any sqlx executor.
7221 /// See [`Self::save_on`] for tenancy-scoped rationale.
7222 ///
7223 /// # Errors
7224 /// As [`Self::insert`].
7225 #[cfg(feature = "postgres")]
7226 pub async fn insert_on<'_c, _E>(
7227 &self,
7228 _executor: _E,
7229 ) -> ::core::result::Result<(), #root::sql::ExecError>
7230 where
7231 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7232 {
7233 let query = #root::core::InsertQuery {
7234 model: <Self as #root::core::Model>::SCHEMA,
7235 columns: ::std::vec![ #( #insert_columns ),* ],
7236 values: ::std::vec![ #( #insert_values ),* ],
7237 returning: ::std::vec::Vec::new(),
7238 on_conflict: ::core::option::Option::None,
7239 };
7240 #root::sql::__macro_internals::insert_on(_executor, &query).await
7241 }
7242 }
7243 };
7244
7245 let bulk_insert_method = if fields.has_auto {
7246 let cols_no_auto = &fields.bulk_columns_no_auto;
7247 let cols_all = &fields.bulk_columns_all;
7248 let pushes_no_auto = &fields.bulk_pushes_no_auto;
7249 let pushes_all = &fields.bulk_pushes_all;
7250 let returning_cols = &fields.returning_cols;
7251 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7252 let uniformity = &fields.bulk_auto_uniformity;
7253 let first_auto_ident = fields
7254 .first_auto_ident
7255 .as_ref()
7256 .expect("has_auto implies first_auto_ident is Some");
7257 quote! {
7258 /// Bulk-insert `rows` in a single round-trip. Every row's
7259 /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7260 /// (sequence fills them in) or uniformly `Auto::Set(_)`
7261 /// (caller-supplied values). Mixed Set/Unset is rejected
7262 /// — call `insert` per row for that case.
7263 ///
7264 /// Empty slice is a no-op. Each row's `Auto` fields are
7265 /// populated from the `RETURNING` clause in input order
7266 /// before this returns.
7267 ///
7268 /// # Errors
7269 /// Returns [`#root::sql::ExecError`] for validation,
7270 /// SQL-writing, mixed-Auto rejection, or driver failures.
7271 #[cfg(feature = "postgres")]
7272 pub async fn bulk_insert(
7273 rows: &mut [Self],
7274 pool: &#root::sql::sqlx::PgPool,
7275 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7276 #pool_to_bulk_insert_on
7277 }
7278
7279 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7280 /// See [`Self::save_on`] for tenancy-scoped rationale.
7281 ///
7282 /// # Errors
7283 /// As [`Self::bulk_insert`].
7284 #[cfg(feature = "postgres")]
7285 pub async fn bulk_insert_on #executor_generics (
7286 rows: &mut [Self],
7287 #executor_param,
7288 ) -> ::core::result::Result<(), #root::sql::ExecError>
7289 #executor_where
7290 {
7291 if rows.is_empty() {
7292 return ::core::result::Result::Ok(());
7293 }
7294 let _first_unset = matches!(
7295 rows[0].#first_auto_ident,
7296 #root::sql::Auto::Unset
7297 );
7298 #( #uniformity )*
7299
7300 let mut _all_rows: ::std::vec::Vec<
7301 ::std::vec::Vec<#root::core::SqlValue>,
7302 > = ::std::vec::Vec::with_capacity(rows.len());
7303 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7304 for _row in rows.iter() {
7305 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7306 ::std::vec::Vec::new();
7307 #( #pushes_no_auto )*
7308 _all_rows.push(_row_vals);
7309 }
7310 ::std::vec![ #( #cols_no_auto ),* ]
7311 } else {
7312 for _row in rows.iter() {
7313 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7314 ::std::vec::Vec::new();
7315 #( #pushes_all )*
7316 _all_rows.push(_row_vals);
7317 }
7318 ::std::vec![ #( #cols_all ),* ]
7319 };
7320
7321 let _query = #root::core::BulkInsertQuery {
7322 model: <Self as #root::core::Model>::SCHEMA,
7323 columns: _columns,
7324 rows: _all_rows,
7325 returning: ::std::vec![ #( #returning_cols ),* ],
7326 on_conflict: ::core::option::Option::None,
7327 };
7328 let _returned = #root::sql::__macro_internals::bulk_insert_on(
7329 #executor_passes_to_data_write,
7330 &_query,
7331 ).await?;
7332 if _returned.len() != rows.len() {
7333 return ::core::result::Result::Err(
7334 #root::sql::ExecError::Sql(
7335 #root::sql::SqlError::BulkInsertReturningMismatch {
7336 expected: rows.len(),
7337 actual: _returned.len(),
7338 }
7339 )
7340 );
7341 }
7342 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7343 #auto_assigns_for_row
7344 }
7345 #audit_bulk_insert_emit
7346 ::core::result::Result::Ok(())
7347 }
7348 }
7349 } else {
7350 let cols_all = &fields.bulk_columns_all;
7351 let pushes_all = &fields.bulk_pushes_all;
7352 quote! {
7353 /// Bulk-insert `rows` in a single round-trip. Every row's
7354 /// fields are written verbatim — there are no `Auto<T>`
7355 /// fields on this model.
7356 ///
7357 /// Empty slice is a no-op.
7358 ///
7359 /// # Errors
7360 /// Returns [`#root::sql::ExecError`] for validation,
7361 /// SQL-writing, or driver failures.
7362 #[cfg(feature = "postgres")]
7363 pub async fn bulk_insert(
7364 rows: &[Self],
7365 pool: &#root::sql::sqlx::PgPool,
7366 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7367 Self::bulk_insert_on(rows, pool).await
7368 }
7369
7370 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7371 /// See [`Self::save_on`] for tenancy-scoped rationale.
7372 ///
7373 /// # Errors
7374 /// As [`Self::bulk_insert`].
7375 #[cfg(feature = "postgres")]
7376 pub async fn bulk_insert_on<'_c, _E>(
7377 rows: &[Self],
7378 _executor: _E,
7379 ) -> ::core::result::Result<(), #root::sql::ExecError>
7380 where
7381 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7382 {
7383 if rows.is_empty() {
7384 return ::core::result::Result::Ok(());
7385 }
7386 let mut _all_rows: ::std::vec::Vec<
7387 ::std::vec::Vec<#root::core::SqlValue>,
7388 > = ::std::vec::Vec::with_capacity(rows.len());
7389 for _row in rows.iter() {
7390 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7391 ::std::vec::Vec::new();
7392 #( #pushes_all )*
7393 _all_rows.push(_row_vals);
7394 }
7395 let _query = #root::core::BulkInsertQuery {
7396 model: <Self as #root::core::Model>::SCHEMA,
7397 columns: ::std::vec![ #( #cols_all ),* ],
7398 rows: _all_rows,
7399 returning: ::std::vec::Vec::new(),
7400 on_conflict: ::core::option::Option::None,
7401 };
7402 let _ = #root::sql::__macro_internals::bulk_insert_on(_executor, &_query).await?;
7403 ::core::result::Result::Ok(())
7404 }
7405 }
7406 };
7407
7408 // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7409 // (no postgres-feature gate); routes through the existing
7410 // `bulk_insert_pool` + per-dialect conflict writer.
7411 //
7412 // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7413 // sequence picks the PK for fresh inserts; the UPDATE branch never
7414 // touches the Auto column.
7415 let bulk_upsert_pool_method = {
7416 // Pick the "no Auto" columns when the model has Auto fields,
7417 // else every column.
7418 let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7419 (
7420 fields.bulk_columns_no_auto.clone(),
7421 fields.bulk_pushes_no_auto.clone(),
7422 )
7423 } else {
7424 (
7425 fields.bulk_columns_all.clone(),
7426 fields.bulk_pushes_all.clone(),
7427 )
7428 };
7429 quote! {
7430 /// Tri-dialect `bulk_create(update_conflicts=True)` — Django's
7431 /// canonical "import a batch idempotently" shape. Issue #267
7432 /// / T1.5.
7433 ///
7434 /// Per-row values are extracted and lowered into a
7435 /// [`#root::core::BulkInsertQuery`] with
7436 /// `on_conflict = DoUpdate { target, update_columns }`. The
7437 /// writer dispatches per-dialect:
7438 /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7439 /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)` (target ignored — MySQL matches every UNIQUE index)
7440 ///
7441 /// `target` names the column(s) whose unique constraint
7442 /// defines the conflict (typically a `unique` or
7443 /// `unique_together` natural-key column, NOT the `Auto<T>`
7444 /// PK). `update_cols` names the columns to overwrite on
7445 /// conflict — every other column is left untouched on the
7446 /// existing row.
7447 ///
7448 /// Auto-PK rows must all have `Auto::Unset` (the sequence
7449 /// picks the PK on insert; the update path never touches
7450 /// the Auto column). Auto-set rows trigger a hard error.
7451 /// Empty slice is a no-op.
7452 ///
7453 /// # Errors
7454 /// Returns [`#root::sql::ExecError`] for validation,
7455 /// SQL-writing, or driver failures.
7456 pub async fn bulk_upsert_pool(
7457 rows: &[Self],
7458 target: &[&'static str],
7459 update_cols: &[&'static str],
7460 pool: &#root::sql::Pool,
7461 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7462 if rows.is_empty() {
7463 return ::core::result::Result::Ok(());
7464 }
7465 let mut _all_rows: ::std::vec::Vec<
7466 ::std::vec::Vec<#root::core::SqlValue>,
7467 > = ::std::vec::Vec::with_capacity(rows.len());
7468 for _row in rows.iter() {
7469 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7470 ::std::vec::Vec::new();
7471 #( #upsert_pushes )*
7472 _all_rows.push(_row_vals);
7473 }
7474 let _query = #root::core::BulkInsertQuery {
7475 model: <Self as #root::core::Model>::SCHEMA,
7476 columns: ::std::vec![ #( #upsert_cols ),* ],
7477 rows: _all_rows,
7478 returning: ::std::vec::Vec::new(),
7479 on_conflict: ::core::option::Option::Some(
7480 #root::core::ConflictClause::DoUpdate {
7481 target: target.to_vec(),
7482 update_columns: update_cols.to_vec(),
7483 }
7484 ),
7485 };
7486 #root::sql::bulk_insert_pool(pool, &_query).await
7487 }
7488
7489 /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7490 /// skip rows that would violate a unique constraint. Issue
7491 /// #267 / T1.5. Same per-dialect dispatch as
7492 /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7493 /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7494 /// <pivot> = <pivot>` (MySQL no-op write).
7495 ///
7496 /// # Errors
7497 /// As [`Self::bulk_upsert_pool`].
7498 pub async fn bulk_insert_or_ignore_pool(
7499 rows: &[Self],
7500 pool: &#root::sql::Pool,
7501 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7502 if rows.is_empty() {
7503 return ::core::result::Result::Ok(());
7504 }
7505 let mut _all_rows: ::std::vec::Vec<
7506 ::std::vec::Vec<#root::core::SqlValue>,
7507 > = ::std::vec::Vec::with_capacity(rows.len());
7508 for _row in rows.iter() {
7509 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7510 ::std::vec::Vec::new();
7511 #( #upsert_pushes )*
7512 _all_rows.push(_row_vals);
7513 }
7514 let _query = #root::core::BulkInsertQuery {
7515 model: <Self as #root::core::Model>::SCHEMA,
7516 columns: ::std::vec![ #( #upsert_cols ),* ],
7517 rows: _all_rows,
7518 returning: ::std::vec::Vec::new(),
7519 on_conflict: ::core::option::Option::Some(
7520 #root::core::ConflictClause::DoNothing
7521 ),
7522 };
7523 #root::sql::bulk_insert_pool(pool, &_query).await
7524 }
7525 }
7526 };
7527
7528 // Ergonomic `Model::bulk_update(objs, fields)` — Django's
7529 // `QuerySet.bulk_update`. The SQL/IR/executor stack
7530 // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7531 // `write_bulk_update_*` writers) already existed; what was missing
7532 // was the per-model constructor that maps `&[Self]` + a runtime
7533 // column list into rows of `[pk, col_vals…]` so callers don't
7534 // hand-build the IR. Emitted only when the model has a primary key
7535 // (the PK is the join key and can't itself be updated).
7536 let bulk_update_method = match &fields.primary_key {
7537 None => quote! {},
7538 Some((pk_ident, pk_col)) => {
7539 // One pair of match arms per non-PK column: a validation arm
7540 // resolving the runtime name to its `&'static str` column,
7541 // and a value arm pushing that field off the row.
7542 let mut col_arms: Vec<TokenStream2> = Vec::new();
7543 let mut val_arms: Vec<TokenStream2> = Vec::new();
7544 for entry in &fields.column_entries {
7545 if &entry.column == pk_col {
7546 continue;
7547 }
7548 let col = &entry.column;
7549 let ident = &entry.ident;
7550 col_arms.push(quote! { #col => #col, });
7551 val_arms.push(quote! {
7552 #col => _row_vals.push(
7553 ::core::convert::Into::<#root::core::SqlValue>::into(
7554 ::core::clone::Clone::clone(&_o.#ident)
7555 )
7556 ),
7557 });
7558 }
7559 quote! {
7560 /// Django's `QuerySet.bulk_update(objs, fields)` — write
7561 /// per-row-different values for the named `fields` across
7562 /// every object in `objs` in a single statement, matched
7563 /// by primary key.
7564 ///
7565 /// `fields` names the **columns** to update. The primary
7566 /// key identifies each row and cannot itself be updated
7567 /// (pass it and you get
7568 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7569 /// Empty `objs` or `fields` is a no-op returning `0`.
7570 /// Objects whose PK matches no row are simply not updated.
7571 /// Returns the number of rows affected.
7572 ///
7573 /// Tri-dialect: lowers to one
7574 /// [`#root::core::BulkUpdateQuery`] and dispatches
7575 /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7576 /// a CTE + correlated subquery on SQLite, an inner
7577 /// `JOIN (VALUES …)` on MySQL.
7578 ///
7579 /// # Errors
7580 /// [`#root::core::QueryError::UnknownField`] for a name
7581 /// that isn't a column on this model,
7582 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7583 /// `fields` names the PK, or [`#root::sql::ExecError`] for
7584 /// SQL-writing / driver failures.
7585 pub async fn bulk_update(
7586 objs: &[Self],
7587 fields: &[&str],
7588 pool: &#root::sql::Pool,
7589 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7590 if objs.is_empty() || fields.is_empty() {
7591 return ::core::result::Result::Ok(0);
7592 }
7593 let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7594 let mut _update_columns: ::std::vec::Vec<&'static str> =
7595 ::std::vec::Vec::with_capacity(fields.len());
7596 for &_f in fields {
7597 let _col: &'static str = match _f {
7598 #pk_col => {
7599 return ::core::result::Result::Err(
7600 ::core::convert::Into::into(
7601 #root::core::QueryError::BulkUpdatePrimaryKey {
7602 model: _model_name,
7603 field: ::std::string::ToString::to_string(_f),
7604 }
7605 )
7606 );
7607 }
7608 #( #col_arms )*
7609 _ => {
7610 return ::core::result::Result::Err(
7611 ::core::convert::Into::into(
7612 #root::core::QueryError::UnknownField {
7613 model: _model_name,
7614 field: ::std::string::ToString::to_string(_f),
7615 }
7616 )
7617 );
7618 }
7619 };
7620 _update_columns.push(_col);
7621 }
7622 let mut _rows: ::std::vec::Vec<
7623 ::std::vec::Vec<#root::core::SqlValue>,
7624 > = ::std::vec::Vec::with_capacity(objs.len());
7625 for _o in objs.iter() {
7626 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7627 ::std::vec::Vec::with_capacity(fields.len() + 1);
7628 // PK first — the writers expect `[pk, …update cols]`.
7629 _row_vals.push(
7630 ::core::convert::Into::<#root::core::SqlValue>::into(
7631 ::core::clone::Clone::clone(&_o.#pk_ident)
7632 )
7633 );
7634 for &_f in fields {
7635 match _f {
7636 #( #val_arms )*
7637 // Unreachable: every name was validated
7638 // against the same arm set above.
7639 _ => {}
7640 }
7641 }
7642 _rows.push(_row_vals);
7643 }
7644 let _query = #root::core::BulkUpdateQuery {
7645 model: <Self as #root::core::Model>::SCHEMA,
7646 update_columns: _update_columns,
7647 rows: _rows,
7648 };
7649 #root::sql::bulk_update_pool(pool, &_query).await
7650 }
7651 }
7652 }
7653 };
7654
7655 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7656 quote! {
7657 /// Hidden runtime accessor for the primary-key value as a
7658 /// [`SqlValue`]. Used by reverse-relation helpers
7659 /// (`<parent>::<child>_set`) emitted from sibling models'
7660 /// FK fields. Not part of the public API.
7661 #[doc(hidden)]
7662 pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7663 ::core::convert::Into::<#root::core::SqlValue>::into(
7664 ::core::clone::Clone::clone(&self.#pk_ident)
7665 )
7666 }
7667 }
7668 });
7669
7670 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7671 quote! {
7672 impl #root::sql::HasPkValue for #struct_name {
7673 fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7674 ::core::convert::Into::<#root::core::SqlValue>::into(
7675 ::core::clone::Clone::clone(&self.#pk_ident)
7676 )
7677 }
7678 }
7679 }
7680 });
7681
7682 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7683
7684 // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7685 // dispatch to the right per-backend body without the macro emitting
7686 // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7687 // so audited models with non-Auto PKs (which still go through
7688 // `insert_one_with_audit` → `apply_auto_pk`) link.
7689 let assign_auto_pk_pool_impl = {
7690 let auto_assigns = &fields.auto_assigns;
7691 // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7692 // so the body is structurally identical to `auto_assigns` —
7693 // only the helper name swaps from `try_get_returning` to
7694 // `try_get_returning_sqlite` so the closure typechecks against
7695 // a `SqliteRow` instead of a `PgRow`.
7696 let auto_assigns_sqlite: Vec<TokenStream2> = fields
7697 .auto_field_idents
7698 .iter()
7699 .map(|(ident, column)| {
7700 quote! {
7701 self.#ident = #root::sql::try_get_returning_sqlite(
7702 _returning_row, #column
7703 )?;
7704 }
7705 })
7706 .collect();
7707 // #1028 — decode each `generated_as` column from the same
7708 // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7709 // own type drives the decode (no `Auto<T>` wrapper).
7710 let generated_assigns: Vec<TokenStream2> = fields
7711 .generated_field_idents
7712 .iter()
7713 .map(|(ident, column)| {
7714 quote! {
7715 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7716 }
7717 })
7718 .collect();
7719 let generated_assigns_sqlite: Vec<TokenStream2> = fields
7720 .generated_field_idents
7721 .iter()
7722 .map(|(ident, column)| {
7723 quote! {
7724 self.#ident = #root::sql::try_get_returning_sqlite(
7725 _returning_row, #column
7726 )?;
7727 }
7728 })
7729 .collect();
7730 let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
7731 // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7732 // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7733 // Auto<Uuid>/etc. fail to link against MySQL (intended —
7734 // those models can't use AUTO_INCREMENT). The trait is only
7735 // touched on the MySQL arm at runtime, so PG-only consumers
7736 // never see the bound failure.
7737 //
7738 // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7739 // Auto<i64> PK + auto_now_add timestamp) errored hard at
7740 // runtime with "multi-column RETURNING". MySQL has no
7741 // multi-column RETURNING semantic and a follow-up SELECT
7742 // would need cross-trait plumbing. Pragmatic shape: succeed
7743 // with the FIRST Auto field populated from LAST_INSERT_ID();
7744 // any other Auto fields stay `Auto::Unset`. Callers that
7745 // need the DB-defaulted timestamp / UUID can re-fetch the
7746 // row by PK after `save_pool`. Fixes the cookbook chapter
7747 // 12 dialect divergence.
7748 let value_ty = fields
7749 .first_auto_value_ty
7750 .as_ref()
7751 .expect("first_auto_value_ty set whenever first_auto_ident is");
7752 quote! {
7753 let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7754 ::rustango_from_mysql_auto_id(_id)?;
7755 self.#first = #root::sql::Auto::Set(_converted);
7756 ::core::result::Result::Ok(())
7757 }
7758 } else {
7759 quote! {
7760 let _ = _id;
7761 ::core::result::Result::Ok(())
7762 }
7763 };
7764 quote! {
7765 impl #root::sql::AssignAutoPkPool for #struct_name {
7766 fn __rustango_assign_from_pg_row(
7767 &mut self,
7768 _returning_row: &#root::sql::PgReturningRow,
7769 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7770 #( #auto_assigns )*
7771 #( #generated_assigns )*
7772 ::core::result::Result::Ok(())
7773 }
7774 fn __rustango_assign_from_mysql_id(
7775 &mut self,
7776 _id: i64,
7777 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7778 #mysql_body
7779 }
7780 fn __rustango_assign_from_sqlite_row(
7781 &mut self,
7782 _returning_row: &#root::sql::SqliteReturningRow,
7783 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7784 #( #auto_assigns_sqlite )*
7785 #( #generated_assigns_sqlite )*
7786 ::core::result::Result::Ok(())
7787 }
7788 }
7789 }
7790 };
7791
7792 let from_aliased_row_inits = &fields.from_aliased_row_inits;
7793 let aliased_row_helper = quote! {
7794 /// Decode a row's aliased target columns (produced by
7795 /// `select_related`'s LEFT JOIN) into a fresh instance of
7796 /// this model. Reads each column via
7797 /// `format!("{prefix}__{col}")`, matching the alias the
7798 /// SELECT writer emitted. Slice 9.0d.
7799 #[doc(hidden)]
7800 #[cfg(feature = "postgres")]
7801 pub fn __rustango_from_aliased_row(
7802 row: &#root::sql::sqlx::postgres::PgRow,
7803 prefix: &str,
7804 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7805 ::core::result::Result::Ok(Self {
7806 #( #from_aliased_row_inits ),*
7807 })
7808 }
7809 };
7810 // v0.23.0-batch8 — MySQL counterpart, gated through the
7811 // cfg-aware macro_rules so PG-only builds expand to nothing.
7812 let aliased_row_helper_my = quote! {
7813 #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7814 #( #from_aliased_row_inits ),*
7815 });
7816 };
7817
7818 // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7819 // pattern + cfg gate on the `sqlite` feature.
7820 let aliased_row_helper_sqlite = quote! {
7821 #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7822 #( #from_aliased_row_inits ),*
7823 });
7824 };
7825
7826 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7827 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7828 let load_related_impl_sqlite =
7829 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7830
7831 // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7832 // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7833 // default `Self::objects()`. Each accessor returns a fresh
7834 // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7835 // methods the user defined.
7836 let extra_manager_fns: Vec<TokenStream2> = manager_fns
7837 .iter()
7838 .map(|fn_ident| {
7839 let model_name_str = struct_name.to_string();
7840 let fn_name_str = fn_ident.to_string();
7841 let doc = format!(
7842 "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7843 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7844 equivalent to `Self::objects()`. Chains with any \
7845 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7846 extension methods."
7847 );
7848 quote! {
7849 #[doc = #doc]
7850 #[must_use]
7851 pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7852 #root::query::QuerySet::new()
7853 }
7854 }
7855 })
7856 .collect();
7857
7858 quote! {
7859 impl #struct_name {
7860 /// Start a new `QuerySet` over this model. Django shape.
7861 #[must_use]
7862 pub fn objects() -> #root::query::QuerySet<#struct_name> {
7863 #root::query::QuerySet::new()
7864 }
7865
7866 /// Eloquent-shape alias of [`Self::objects`]. Returns
7867 /// a fresh `QuerySet<Self>` ready for `.filter()` /
7868 /// `.where_()` / etc. Matches Laravel muscle-memory:
7869 ///
7870 /// ```ignore
7871 /// // Eloquent: Post::query()->where('published', true)
7872 /// // Django: Post.objects.filter(published=True)
7873 /// // rustango: Post::query().filter("published", true)
7874 /// // or: Post::objects().filter("published", true)
7875 /// ```
7876 ///
7877 /// Both names point at the same underlying constructor;
7878 /// neither is preferred.
7879 #[must_use]
7880 pub fn query() -> #root::query::QuerySet<#struct_name> {
7881 #root::query::QuerySet::new()
7882 }
7883
7884 #( #extra_manager_fns )*
7885
7886 #insert_method
7887
7888 #bulk_insert_method
7889
7890 #bulk_upsert_pool_method
7891
7892 #bulk_update_method
7893
7894 #save_method
7895
7896 #pk_methods
7897
7898 #pk_value_helper
7899
7900 #aliased_row_helper
7901
7902 #column_consts
7903 }
7904
7905 #aliased_row_helper_my
7906
7907 #aliased_row_helper_sqlite
7908
7909 #load_related_impl
7910
7911 #load_related_impl_my
7912
7913 #load_related_impl_sqlite
7914
7915 #has_pk_value_impl
7916
7917 #fk_pk_access_impl
7918
7919 #assign_auto_pk_pool_impl
7920 }
7921}
7922
7923/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
7924/// `auto_assigns` but reading from `_returning_row` and writing to
7925/// `_row_mut` instead of `self`.
7926fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
7927 let root = rustango_root();
7928 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
7929 let col_lit = column.as_str();
7930 quote! {
7931 _row_mut.#ident = #root::sql::sqlx::Row::try_get(
7932 _returning_row,
7933 #col_lit,
7934 )?;
7935 }
7936 });
7937 quote! { #( #lines )* }
7938}
7939
7940/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
7941fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
7942 let lines = entries.iter().map(|e| {
7943 let ident = &e.ident;
7944 let col_ty = column_type_ident(ident);
7945 quote! {
7946 #[allow(non_upper_case_globals)]
7947 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
7948 }
7949 });
7950 quote! { #(#lines)* }
7951}
7952
7953/// Emit a hidden per-model module carrying one zero-sized type per field,
7954/// each with a `Column` impl pointing back at the model.
7955fn column_module_tokens(
7956 module_ident: &syn::Ident,
7957 struct_name: &syn::Ident,
7958 entries: &[ColumnEntry],
7959) -> TokenStream2 {
7960 let root = rustango_root();
7961 let items = entries.iter().map(|e| {
7962 let col_ty = column_type_ident(&e.ident);
7963 let value_ty = &e.value_ty;
7964 let name = &e.name;
7965 let column = &e.column;
7966 let field_type_tokens = &e.field_type_tokens;
7967 quote! {
7968 #[derive(::core::clone::Clone, ::core::marker::Copy)]
7969 pub struct #col_ty;
7970
7971 impl #root::core::Column for #col_ty {
7972 type Model = super::#struct_name;
7973 type Value = #value_ty;
7974 const NAME: &'static str = #name;
7975 const COLUMN: &'static str = #column;
7976 const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
7977 }
7978 }
7979 });
7980 quote! {
7981 #[doc(hidden)]
7982 #[allow(non_camel_case_types, non_snake_case)]
7983 pub mod #module_ident {
7984 // Re-import the parent scope so field types referencing
7985 // sibling models (e.g. `ForeignKey<Author>`) resolve
7986 // inside this submodule. Without this we'd hit
7987 // `proc_macro_derive_resolution_fallback` warnings.
7988 #[allow(unused_imports)]
7989 use super::*;
7990 #(#items)*
7991 }
7992 }
7993}
7994
7995fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
7996 syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
7997}
7998
7999fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8000 syn::Ident::new(
8001 &format!("__rustango_cols_{struct_name}"),
8002 struct_name.span(),
8003 )
8004}
8005
8006fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
8007 let root = rustango_root();
8008 // The Postgres impl is always emitted — every rustango build pulls in
8009 // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8010 // routed through `#root::__impl_my_from_row!`, a cfg-gated
8011 // macro_rules whose body collapses to nothing when rustango is built
8012 // without the `mysql` feature. No user-facing feature shim required.
8013 //
8014 // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8015 // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8016 // back into a comma-separated list inside square brackets. Since each
8017 // entry is already in `field: expr` shape, the existing tokens slot in.
8018 quote! {
8019 #[cfg(feature = "postgres")]
8020 impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8021 for #struct_name
8022 {
8023 fn from_row(
8024 row: &'r #root::sql::sqlx::postgres::PgRow,
8025 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8026 ::core::result::Result::Ok(Self {
8027 #( #from_row_inits ),*
8028 })
8029 }
8030 }
8031
8032 #root::__impl_my_from_row!(#struct_name, |row| {
8033 #( #from_row_inits ),*
8034 });
8035
8036 #root::__impl_sqlite_from_row!(#struct_name, |row| {
8037 #( #from_row_inits ),*
8038 });
8039 }
8040}
8041
8042struct ContainerAttrs {
8043 table: Option<String>,
8044 display: Option<(String, proc_macro2::Span)>,
8045 /// Explicit Django-style app label from `#[rustango(app = "blog")]`.
8046 /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8047 /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8048 /// at runtime — this attribute is the override for cases where
8049 /// the inference is wrong (e.g. a model that conceptually belongs
8050 /// to one app but is physically in another module).
8051 app: Option<String>,
8052 /// Django ModelAdmin-shape per-model knobs from
8053 /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8054 /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8055 /// admin code falls back to `AdminConfig::DEFAULT`.
8056 admin: Option<AdminAttrs>,
8057 /// Per-model audit configuration from `#[rustango(audit(...))]`.
8058 /// `None` when the model isn't audited — write paths emit no
8059 /// audit entries. When present, single-row writes capture
8060 /// before/after for the listed fields and bulk writes batch
8061 /// snapshots into one INSERT into `rustango_audit_log`.
8062 audit: Option<AuditAttrs>,
8063 /// `true` when `#[rustango(permissions)]` is present. Signals that
8064 /// `auto_create_permissions` should seed the four CRUD codenames for
8065 /// this model.
8066 permissions: bool,
8067 /// Many-to-many relations declared via
8068 /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8069 /// src = "post_id", dst = "tag_id"))]`.
8070 m2m: Vec<M2MAttr>,
8071 /// Polymorphic M2M relations declared via
8072 /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8073 /// pk_column = "taggable_id", ct_column = "taggable_type",
8074 /// related_column = "tag_id"))]` (issue #818).
8075 generic_m2m: Vec<GenericM2MAttr>,
8076 /// Composite indexes declared via
8077 /// `#[rustango(index("col1, col2"))]` or
8078 /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8079 /// Single-column indexes from `#[rustango(index)]` on fields are
8080 /// accumulated here during field collection.
8081 indexes: Vec<IndexAttr>,
8082 /// Table-level CHECK constraints declared via
8083 /// `#[rustango(check(name = "…", expr = "…"))]`.
8084 checks: Vec<CheckAttr>,
8085 /// Table-level PG `EXCLUDE` constraints declared via
8086 /// `#[rustango(exclude(name = "…", using = "gist", elements =
8087 /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8088 /// migration writer renders them on Postgres and skips with a
8089 /// warning on MySQL/SQLite. Issue #319.
8090 excludes: Vec<ExcludeAttr>,
8091 /// Composite (multi-column) FKs declared via
8092 /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8093 /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8094 composite_fks: Vec<CompositeFkAttr>,
8095 /// Generic ("any model") FKs declared via
8096 /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8097 /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8098 generic_fks: Vec<GenericFkAttr>,
8099 /// Where this model lives in a tenancy deployment, declared via
8100 /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8101 /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8102 /// to partition diff output between registry-scoped and
8103 /// tenant-scoped migration files.
8104 scope: Option<String>,
8105 /// Custom-Manager extension-trait name from
8106 /// `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8107 /// When set, the macro emits an empty `pub trait <name>: Sized {}`
8108 /// adjacent to the model so users can write
8109 /// `impl FooManagerExt for QuerySet<Foo> { fn published(self) -> Self ... }`
8110 /// and discover the convention from the model definition.
8111 manager_ext: Option<syn::Ident>,
8112 /// Extra QuerySet accessor names from
8113 /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8114 /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8115 /// the default `Self::objects()`. Multiple attributes allowed.
8116 manager_fns: Vec<syn::Ident>,
8117 /// Default ordering declared via `#[rustango(default_order =
8118 /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8119 /// `(column_name, desc_flag, span_for_error_reporting)` — the
8120 /// `-` prefix means descending; the `+` prefix or no prefix means
8121 /// ascending.
8122 default_order: Vec<(String, bool, proc_macro2::Span)>,
8123 /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8124 /// Routes the emitted schema's `is_view = true` so the migration
8125 /// snapshot skips this model (its underlying SQL view is operator-
8126 /// managed, not rustango-managed).
8127 is_view: bool,
8128 /// Django-shape `Meta.managed` from `#[rustango(managed = false)]`.
8129 /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8130 /// the migration snapshot skips this model so `makemigrations` /
8131 /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8132 /// TABLE` against it (operator-managed schema).
8133 managed: bool,
8134 /// Django-shape `Meta.base_manager_name` from
8135 /// `#[rustango(base_manager_name = "...")]`. Threaded into
8136 /// `ModelSchema::base_manager_name`. Declarative-only today.
8137 base_manager_name: Option<String>,
8138 /// Django-shape `Meta.order_with_respect_to = "parent_fk"` from
8139 /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8140 /// field this model's instances are ordered relative to.
8141 /// Declarative-only today; threaded onto
8142 /// `ModelSchema::order_with_respect_to`.
8143 order_with_respect_to: Option<String>,
8144 /// Django-shape `Meta.proxy = True` from `#[rustango(proxy)]` /
8145 /// `#[rustango(proxy = true)]`. Marks the model as a proxy that
8146 /// shares its DB table with another struct. Threaded into
8147 /// `ModelSchema::proxy` so future codegen can skip table-owning
8148 /// behavior for proxies.
8149 proxy: bool,
8150 /// Django-shape `Meta.required_db_features` from
8151 /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8152 /// Each comma-separated capability token surfaces on
8153 /// `ModelSchema::required_db_features` so `manage check --deploy`
8154 /// can warn when the active dialect lacks one.
8155 required_db_features: Vec<String>,
8156 /// Django-shape `Meta.required_db_vendor` from
8157 /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8158 /// Normalized to the dialect name `manage check --deploy`
8159 /// compares against `Settings.database.backend`. Aliases
8160 /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8161 /// stored under the canonical name.
8162 required_db_vendor: Option<String>,
8163 /// Django-shape `Meta.default_related_name` from
8164 /// `#[rustango(default_related_name = "...")]`. Threaded into
8165 /// `ModelSchema::default_related_name`. Reverse-relation accessor
8166 /// name to use when an FK / M2M field doesn't override it.
8167 /// Today rustango doesn't auto-emit reverse managers; the
8168 /// metadata is the foundation for that work.
8169 default_related_name: Option<String>,
8170 /// Django-shape `Meta.db_table_comment` (4.2+) from
8171 /// `#[rustango(db_table_comment = "...")]`. Threaded into
8172 /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8173 /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8174 /// inline `COMMENT='...'`, SQLite: no-op).
8175 db_table_comment: Option<String>,
8176 /// Django-shape `Meta.get_latest_by` from
8177 /// `#[rustango(get_latest_by = "created_at")]` /
8178 /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8179 /// `(column, descending)` where `descending = true` when the
8180 /// attribute value starts with `-`. Threaded into
8181 /// `ModelSchema::get_latest_by`.
8182 get_latest_by: Option<(String, bool)>,
8183 /// Django-shape `Meta.permissions = [(codename, name), ...]`
8184 /// from `#[rustango(extra_permissions = "approve:Can approve,
8185 /// archive:Can archive")]`. Comma-separated `codename:label`
8186 /// pairs. Threaded into `ModelSchema::extra_permissions`.
8187 extra_permissions: Vec<(String, String)>,
8188 /// Django-shape `Meta.default_permissions` — which CRUD codenames
8189 /// (`"add"` / `"change"` / `"delete"` / `"view"`) the framework
8190 /// auto-creates. Empty `Vec` (default) means **all four** — matches
8191 /// Django's behavior when the operator omits the option. Set via
8192 /// `#[rustango(default_permissions = "view,change")]` to opt out.
8193 /// Validated at parse time; unknown actions fail with a span-pointing
8194 /// error.
8195 default_permissions: Vec<String>,
8196 /// `#[rustango(verbose_name = "blog post")]` — Django-shape
8197 /// human-readable singular label for the model. Threaded into
8198 /// `ModelSchema::verbose_name` so admin section headers /
8199 /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8200 /// over the Rust struct identifier.
8201 verbose_name: Option<String>,
8202 /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8203 /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8204 /// When unset, `display_label_plural()` auto-suffixes `s`.
8205 verbose_name_plural: Option<String>,
8206 /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8207 /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8208 /// a name (used by `QuerySet::without_global_scope`) with a
8209 /// `fn() -> WhereExpr` path that the macro emits into
8210 /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8211 global_scopes: Vec<GlobalScopeAttr>,
8212 /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8213 /// `#[rustango(through(name, far, far_fk_column, intermediate,
8214 /// intermediate_fk_column, intermediate_pk_column))]` — issue
8215 /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8216 /// entry emits an inherent `<name>_through(&self)` accessor that
8217 /// returns a `QuerySet<Far>` filtered via a correlated subquery
8218 /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8219 /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8220 through_relations: Vec<ThroughAttr>,
8221 /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8222 /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8223 /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8224 /// Each entry emits two associated functions on the parent —
8225 /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8226 /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8227 /// over a correlated subquery against the child table. Users
8228 /// drop the result into `QuerySet::where_raw(...)`.
8229 reverse_has_relations: Vec<ReverseHasAttr>,
8230 /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8231 /// issue #830. Each emits a `Model::generic_reverse_relations()`
8232 /// entry so the relation-existence family resolves polymorphic
8233 /// children by name.
8234 generic_has_relations: Vec<GenericHasAttr>,
8235}
8236
8237/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8238/// declaration. Each entry becomes one `core::GlobalScope` in the
8239/// emitted schema literal; `apply` resolves at macro-expand time
8240/// against the consumer's scope so the function must be in scope at
8241/// the use site. Issue #820.
8242struct GlobalScopeAttr {
8243 name: String,
8244 apply: syn::Path,
8245}
8246
8247/// Parsed `#[rustango(through(...))]` declaration. Issue
8248/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8249/// `hasManyThrough` / `hasOneThrough` parity.
8250///
8251/// `Country hasManyThrough Post via User` declares as:
8252///
8253/// ```ignore
8254/// #[rustango(through(
8255/// name = "posts",
8256/// far = "Post",
8257/// far_fk_column = "author_id",
8258/// intermediate = "User",
8259/// intermediate_fk_column = "country_id",
8260/// ))]
8261/// struct Country { ... }
8262/// ```
8263///
8264/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8265/// which returns a queryset filtered via a correlated subquery —
8266/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8267/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8268/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8269/// because the subquery lives inside a `WhereExpr::InSubquery` node
8270/// and the outer queryset's pending list stays empty.
8271///
8272/// All four required arguments use **SQL column / table names**
8273/// (not Rust field names) to sidestep the multi-hop-filter substrate
8274/// gap. Once that substrate lands, a higher-level Rust-field-name
8275/// shorthand can be added without breaking this surface.
8276struct ThroughAttr {
8277 /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8278 name: String,
8279 /// Far model type identifier. `far = "Post"` → returns
8280 /// `QuerySet<Post>`. Resolved verbatim against the scope where
8281 /// the derive expands.
8282 far: syn::Ident,
8283 /// SQL column on the far model's table that references the
8284 /// intermediate model's primary key. For `Post`'s
8285 /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8286 /// default `<field>_id` convention) or whatever the user
8287 /// declared via `#[rustango(db_column = "...")]`.
8288 far_fk_column: String,
8289 /// Intermediate model type identifier. Needed to look up its
8290 /// `SCHEMA` so the subquery's `FROM` clause points at the
8291 /// intermediate table. `intermediate = "User"`.
8292 intermediate: syn::Ident,
8293 /// SQL column on the intermediate's table that references the
8294 /// source (this) model's primary key. For `User`'s
8295 /// `country: ForeignKey<Country>` the column is `"country_id"`.
8296 intermediate_fk_column: String,
8297 /// SQL primary-key column on the intermediate's table — the
8298 /// column the subquery projects. Optional; defaults to `"id"`
8299 /// (rustango's default PK column name). Override when the
8300 /// intermediate declares a custom PK column.
8301 intermediate_pk_column: String,
8302}
8303
8304/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8305/// child_fk_column = "..."))]` declaration. Issue
8306/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8307/// `whereHas` / `whereDoesntHave` parity.
8308///
8309/// `Post hasMany Comment` declares as:
8310///
8311/// ```ignore
8312/// #[rustango(reverse_has(
8313/// name = "comments",
8314/// child = "Comment",
8315/// child_fk_column = "post_id",
8316/// ))]
8317/// struct Post { ... }
8318/// ```
8319///
8320/// The macro emits two associated functions on `Post`:
8321///
8322/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8323/// `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8324/// <outer>.<self_pk_column>)`.
8325/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8326/// but `NOT EXISTS`, the `whereDoesntHave` analog.
8327///
8328/// User code:
8329///
8330/// ```ignore
8331/// // Posts with at least one comment:
8332/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8333/// // Posts with no comments:
8334/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8335/// ```
8336///
8337/// As with #817, all column / table identifiers are **SQL names**
8338/// (not Rust field names) so the substrate is independent of the
8339/// outstanding multi-hop filter resolver gap. The emitted
8340/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8341/// SQL-emit time via the writer's scope stack.
8342struct ReverseHasAttr {
8343 /// Accessor name. `name = "comments"` → emits
8344 /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8345 name: String,
8346 /// Child model type identifier. `child = "Comment"` — needed to
8347 /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8348 /// points at the child table.
8349 child: syn::Ident,
8350 /// SQL column on the child's table that references this model's
8351 /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8352 /// column is `"post_id"`.
8353 child_fk_column: String,
8354 /// SQL primary-key column on **this** model's table — the column
8355 /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8356 /// (rustango's default PK column name).
8357 self_pk_column: String,
8358}
8359
8360/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8361/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8362/// relation-existence family (issue #830). The child is a polymorphic,
8363/// content-type-discriminated model; emits a `Model::
8364/// generic_reverse_relations()` entry the queryset resolves by name.
8365struct GenericHasAttr {
8366 /// Accessor name, e.g. `name = "tags"`.
8367 name: String,
8368 /// Child model type identifier (`child = "Tag"`) — looked up for its
8369 /// `SCHEMA` so the subquery's `FROM` points at the child table.
8370 child: syn::Ident,
8371 /// Column on the child table holding the parent's content-type id.
8372 /// Optional; defaults to `"content_type_id"`.
8373 ct_column: String,
8374 /// Column on the child table holding the parent's PK value.
8375 /// Optional; defaults to `"object_pk"`.
8376 pk_column: String,
8377 /// SQL primary-key column on **this** (parent) model's table.
8378 /// Optional; defaults to `"id"`.
8379 self_pk_column: String,
8380}
8381
8382/// Parsed form of one index declaration (field-level or container-level).
8383struct IndexAttr {
8384 /// Index name; auto-derived when `None` at parse time.
8385 name: Option<String>,
8386 /// Column names in the index.
8387 columns: Vec<String>,
8388 /// `true` for `CREATE UNIQUE INDEX`.
8389 unique: bool,
8390 /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8391 /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8392 /// `"btree"` when the attribute is absent — the DDL writer omits
8393 /// the `USING` clause and the backend uses its own default
8394 /// (btree on every supported dialect).
8395 method: String,
8396 /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8397 /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8398 /// condition = "...", name = "..."))]`. `None` for plain indexes.
8399 where_clause: Option<String>,
8400 /// Django `Index(fields=..., include=[...])` covering-index
8401 /// columns (PG 11+ `INCLUDE (...)` clause). Empty `Vec` (the
8402 /// default) means "no covering columns".
8403 include: Vec<String>,
8404}
8405
8406/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8407struct CheckAttr {
8408 name: String,
8409 expr: String,
8410}
8411
8412/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8413/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8414/// PG-only — surfaced on every backend in the macro emit; the migration
8415/// writer skips the op on MySQL/SQLite. Issue #319.
8416struct ExcludeAttr {
8417 /// Constraint name (free-form Rust identifier).
8418 name: String,
8419 /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8420 using: String,
8421 /// Comma-separated `(column, operator)` pairs, in declaration
8422 /// order. Parsed from `"col WITH op, col WITH op"`.
8423 elements: Vec<(String, String)>,
8424 /// Optional `WHERE` predicate (raw SQL).
8425 where_clause: Option<String>,
8426}
8427
8428/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8429/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8430/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8431/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8432/// the model, not the field.
8433struct CompositeFkAttr {
8434 /// Logical relation name (free-form Rust identifier).
8435 name: String,
8436 /// SQL table name of the target.
8437 to: String,
8438 /// Source-side column names, in declaration order.
8439 from: Vec<String>,
8440 /// Target-side column names, same length / order as `from`.
8441 on: Vec<String>,
8442}
8443
8444/// Parsed form of one `#[rustango(generic_fk(name = "target",
8445/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8446/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8447/// generic ("any model") FKs live on the model, not the field.
8448struct GenericFkAttr {
8449 /// Logical relation name (free-form Rust identifier).
8450 name: String,
8451 /// Source-side column carrying the `content_type_id` value.
8452 ct_column: String,
8453 /// Source-side column carrying the target row's primary key.
8454 pk_column: String,
8455}
8456
8457/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8458struct M2MAttr {
8459 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8460 name: String,
8461 /// Target table (e.g. `"app_tags"`).
8462 to: String,
8463 /// Junction table (e.g. `"post_tags"`).
8464 through: String,
8465 /// Source FK column in the junction table (e.g. `"post_id"`).
8466 src: String,
8467 /// Destination FK column in the junction table (e.g. `"tag_id"`).
8468 dst: String,
8469 /// Whether the migration writer should auto-create the junction
8470 /// table. Default `true`. Set `auto_create = false` (#324) when
8471 /// the operator declares the through table as its own
8472 /// `#[derive(Model)]` struct with extra columns.
8473 auto_create: bool,
8474}
8475
8476/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8477/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8478/// junction carries a ContentType discriminator so unrelated models
8479/// share one pivot + related set.
8480struct GenericM2MAttr {
8481 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8482 name: String,
8483 /// Polymorphic junction table (e.g. `"taggables"`).
8484 through: String,
8485 /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8486 pk_column: String,
8487 /// Junction column holding the owning model's `content_type_id`
8488 /// discriminator (e.g. `"taggable_type"`).
8489 ct_column: String,
8490 /// Junction column holding the related model PK (e.g. `"tag_id"`).
8491 related_column: String,
8492}
8493
8494/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8495/// "user"))]`. `track` is a comma-separated list of field names whose
8496/// before/after values land in the JSONB `changes` column. `source`
8497/// is informational only — it pins a default source when the model
8498/// is written outside any `audit::with_source(...)` scope (rare).
8499#[derive(Default)]
8500struct AuditAttrs {
8501 /// Field names to capture in the `changes` JSONB. Validated
8502 /// against declared scalar fields at compile time. Empty means
8503 /// "track every scalar field" — Django's audit-everything default.
8504 track: Option<(Vec<String>, proc_macro2::Span)>,
8505}
8506
8507/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8508/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8509/// comma-separated strings; we validate each ident against the model's
8510/// declared fields at compile time.
8511#[derive(Default)]
8512struct AdminAttrs {
8513 list_display: Option<(Vec<String>, proc_macro2::Span)>,
8514 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8515 list_per_page: Option<usize>,
8516 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8517 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8518 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8519 /// Bulk action names. No field-validation against model fields —
8520 /// these are action handlers, not column references.
8521 actions: Option<(Vec<String>, proc_macro2::Span)>,
8522 /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8523 /// sections, comma-separated fields per section, optional
8524 /// `Title:` prefix. Empty title omits the `<legend>`.
8525 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8526 /// `admin(list_display_links = "title")` — Django-shape. Names
8527 /// from `list_display` whose cells should link to detail/edit.
8528 /// Issue #350.
8529 list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8530 /// `admin(search_help_text = "...")` — Django-shape. Short
8531 /// caption rendered beside the admin list view's search box.
8532 /// Issue #353.
8533 search_help_text: Option<String>,
8534 /// `admin(actions_on_top = false)` — Django-shape. Hides the
8535 /// action-bar above the table. Default `true`. Issue #354.
8536 actions_on_top: Option<bool>,
8537 /// `admin(actions_on_bottom = true)` — Django-shape. Renders an
8538 /// additional action-bar below the table. Default `false`.
8539 /// Issue #354.
8540 actions_on_bottom: Option<bool>,
8541 /// `admin(date_hierarchy = "created_at")` — Django-shape. Name of
8542 /// a date / datetime field whose values render as a clickable
8543 /// year / month / day drill-down strip above the list table.
8544 /// Empty / unset disables the strip. Issue #355.
8545 date_hierarchy: Option<String>,
8546 /// `admin(prepopulated_fields = "slug:title")` — Django-shape.
8547 /// Each entry is `target:source[+source2]`; multiple entries are
8548 /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8549 /// The admin change-form emits JS that slugifies the source values
8550 /// into the target field on every keystroke. Issue #356.
8551 prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8552 /// `admin(raw_id_fields = "parent, owner")` — Django-shape. Names
8553 /// of FK fields whose change-form widget renders a lookup link
8554 /// next to the input. Issue #357.
8555 raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8556 /// `admin(autocomplete_fields = "author_id")` — Django-shape.
8557 /// Names of FK fields whose change-form widget renders an
8558 /// Ajax-driven typeahead populated from a `__autocomplete`
8559 /// endpoint on the target model. Issue #358.
8560 autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8561 /// `admin(list_select_related = "all" | "none" | "author, …")`
8562 /// — Django-shape. Tunes the admin list view's FK auto-JOIN
8563 /// policy. Default `"all"` matches rustango's join-everything
8564 /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8565 list_select_related: Option<String>,
8566 /// `admin(formfield_overrides = "field:widget, field2:widget2")` —
8567 /// Django-shape. Each entry is `field_name:widget_name`; multiple
8568 /// entries comma-separated. Empty / unset → no overrides. The
8569 /// list of widget names supported is documented on
8570 /// `AdminConfig::formfield_overrides`. Issue #359.
8571 formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8572}
8573
8574fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8575 let mut out = ContainerAttrs {
8576 table: None,
8577 display: None,
8578 app: None,
8579 admin: None,
8580 audit: None,
8581 // Default `permissions = true` so every `#[derive(Model)]`
8582 // gets the four CRUD codenames seeded by `auto_create_permissions`
8583 // and is visible to non-superusers in the tenant admin without
8584 // manual per-model annotation. Models that intentionally don't
8585 // want permission rows (registry-internal types, framework
8586 // tables operators shouldn't manage directly) opt out via
8587 // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8588 // out-of-the-box admin invisibility regression (#62).
8589 permissions: true,
8590 m2m: Vec::new(),
8591 generic_m2m: Vec::new(),
8592 indexes: Vec::new(),
8593 checks: Vec::new(),
8594 excludes: Vec::new(),
8595 composite_fks: Vec::new(),
8596 generic_fks: Vec::new(),
8597 scope: None,
8598 manager_ext: None,
8599 manager_fns: Vec::new(),
8600 default_order: Vec::new(),
8601 is_view: false,
8602 managed: true,
8603 verbose_name: None,
8604 verbose_name_plural: None,
8605 base_manager_name: None,
8606 order_with_respect_to: None,
8607 proxy: false,
8608 required_db_features: Vec::new(),
8609 required_db_vendor: None,
8610 default_related_name: None,
8611 db_table_comment: None,
8612 get_latest_by: None,
8613 extra_permissions: Vec::new(),
8614 default_permissions: Vec::new(),
8615 global_scopes: Vec::new(),
8616 through_relations: Vec::new(),
8617 reverse_has_relations: Vec::new(),
8618 generic_has_relations: Vec::new(),
8619 };
8620 for attr in &input.attrs {
8621 if !attr.path().is_ident("rustango") {
8622 continue;
8623 }
8624 attr.parse_nested_meta(|meta| {
8625 if meta.path.is_ident("table") {
8626 let s: LitStr = meta.value()?.parse()?;
8627 let name = s.value();
8628 // v0.27.3 (#65) — macro-time guard against table names
8629 // that compile but break SQL downstream. Hyphens are
8630 // the common footgun: PostgreSQL accepts a quoted
8631 // `"intermediate-region"` in CREATE TABLE, but the
8632 // FK / index name derivation in `migrate::ddl`
8633 // emits `intermediate-region_field_fkey` unquoted,
8634 // which then fails the SQL parser. Same shape rule
8635 // as Postgres regular identifiers so the safe path
8636 // is the only path.
8637 validate_table_name(&name, s.span())?;
8638 out.table = Some(name);
8639 return Ok(());
8640 }
8641 if meta.path.is_ident("display") {
8642 let s: LitStr = meta.value()?.parse()?;
8643 out.display = Some((s.value(), s.span()));
8644 return Ok(());
8645 }
8646 if meta.path.is_ident("app") {
8647 let s: LitStr = meta.value()?.parse()?;
8648 out.app = Some(s.value());
8649 return Ok(());
8650 }
8651 if meta.path.is_ident("scope") {
8652 let s: LitStr = meta.value()?.parse()?;
8653 let val = s.value();
8654 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8655 return Err(meta.error(format!(
8656 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8657 )));
8658 }
8659 out.scope = Some(val);
8660 return Ok(());
8661 }
8662 if meta.path.is_ident("admin") {
8663 let mut admin = AdminAttrs::default();
8664 meta.parse_nested_meta(|inner| {
8665 if inner.path.is_ident("list_display") {
8666 let s: LitStr = inner.value()?.parse()?;
8667 admin.list_display =
8668 Some((split_field_list(&s.value()), s.span()));
8669 return Ok(());
8670 }
8671 if inner.path.is_ident("search_fields") {
8672 let s: LitStr = inner.value()?.parse()?;
8673 admin.search_fields =
8674 Some((split_field_list(&s.value()), s.span()));
8675 return Ok(());
8676 }
8677 if inner.path.is_ident("readonly_fields") {
8678 let s: LitStr = inner.value()?.parse()?;
8679 admin.readonly_fields =
8680 Some((split_field_list(&s.value()), s.span()));
8681 return Ok(());
8682 }
8683 if inner.path.is_ident("list_per_page") {
8684 let lit: syn::LitInt = inner.value()?.parse()?;
8685 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8686 return Ok(());
8687 }
8688 if inner.path.is_ident("ordering") {
8689 let s: LitStr = inner.value()?.parse()?;
8690 admin.ordering = Some((
8691 parse_ordering_list(&s.value()),
8692 s.span(),
8693 ));
8694 return Ok(());
8695 }
8696 if inner.path.is_ident("list_filter") {
8697 let s: LitStr = inner.value()?.parse()?;
8698 admin.list_filter =
8699 Some((split_field_list(&s.value()), s.span()));
8700 return Ok(());
8701 }
8702 if inner.path.is_ident("actions") {
8703 let s: LitStr = inner.value()?.parse()?;
8704 admin.actions =
8705 Some((split_field_list(&s.value()), s.span()));
8706 return Ok(());
8707 }
8708 if inner.path.is_ident("fieldsets") {
8709 let s: LitStr = inner.value()?.parse()?;
8710 admin.fieldsets =
8711 Some((parse_fieldset_list(&s.value()), s.span()));
8712 return Ok(());
8713 }
8714 if inner.path.is_ident("list_display_links") {
8715 let s: LitStr = inner.value()?.parse()?;
8716 admin.list_display_links =
8717 Some((split_field_list(&s.value()), s.span()));
8718 return Ok(());
8719 }
8720 if inner.path.is_ident("search_help_text") {
8721 let s: LitStr = inner.value()?.parse()?;
8722 admin.search_help_text = Some(s.value());
8723 return Ok(());
8724 }
8725 if inner.path.is_ident("actions_on_top") {
8726 let lit: syn::LitBool = inner.value()?.parse()?;
8727 admin.actions_on_top = Some(lit.value);
8728 return Ok(());
8729 }
8730 if inner.path.is_ident("actions_on_bottom") {
8731 let lit: syn::LitBool = inner.value()?.parse()?;
8732 admin.actions_on_bottom = Some(lit.value);
8733 return Ok(());
8734 }
8735 if inner.path.is_ident("date_hierarchy") {
8736 let s: LitStr = inner.value()?.parse()?;
8737 admin.date_hierarchy = Some(s.value());
8738 return Ok(());
8739 }
8740 if inner.path.is_ident("prepopulated_fields") {
8741 let s: LitStr = inner.value()?.parse()?;
8742 admin.prepopulated_fields =
8743 Some((parse_prepopulated_list(&s.value()), s.span()));
8744 return Ok(());
8745 }
8746 if inner.path.is_ident("raw_id_fields") {
8747 let s: LitStr = inner.value()?.parse()?;
8748 admin.raw_id_fields =
8749 Some((split_field_list(&s.value()), s.span()));
8750 return Ok(());
8751 }
8752 if inner.path.is_ident("autocomplete_fields") {
8753 let s: LitStr = inner.value()?.parse()?;
8754 admin.autocomplete_fields =
8755 Some((split_field_list(&s.value()), s.span()));
8756 return Ok(());
8757 }
8758 if inner.path.is_ident("list_select_related") {
8759 let s: LitStr = inner.value()?.parse()?;
8760 admin.list_select_related = Some(s.value());
8761 return Ok(());
8762 }
8763 if inner.path.is_ident("formfield_overrides") {
8764 let s: LitStr = inner.value()?.parse()?;
8765 admin.formfield_overrides =
8766 Some((parse_formfield_overrides(&s.value()), s.span()));
8767 return Ok(());
8768 }
8769 Err(inner.error(
8770 "unknown admin attribute (supported: \
8771 `list_display`, `list_display_links`, \
8772 `search_fields`, `search_help_text`, \
8773 `readonly_fields`, \
8774 `list_filter`, `list_per_page`, `ordering`, `actions`, \
8775 `actions_on_top`, `actions_on_bottom`, \
8776 `date_hierarchy`, \
8777 `prepopulated_fields`, \
8778 `raw_id_fields`, \
8779 `autocomplete_fields`, \
8780 `list_select_related`, \
8781 `formfield_overrides`, \
8782 `fieldsets`)",
8783 ))
8784 })?;
8785 out.admin = Some(admin);
8786 return Ok(());
8787 }
8788 if meta.path.is_ident("manager") {
8789 // `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8790 // Stretch `from_queryset = "..."` (Django Manager.from_queryset
8791 // shape) is left as a follow-up — the issue's primary
8792 // acceptance is the `ext = ...` trait emission.
8793 meta.parse_nested_meta(|inner| {
8794 if inner.path.is_ident("ext") {
8795 let s: LitStr = inner.value()?.parse()?;
8796 let name = s.value();
8797 if name.is_empty() {
8798 return Err(inner.error("manager(ext = \"...\") cannot be empty"));
8799 }
8800 out.manager_ext =
8801 Some(syn::Ident::new(&name, s.span()));
8802 return Ok(());
8803 }
8804 Err(inner.error(
8805 "unknown manager attribute (supported: `ext = \"TraitName\"`)",
8806 ))
8807 })?;
8808 return Ok(());
8809 }
8810 if meta.path.is_ident("manager_fn") {
8811 // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8812 // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8813 // next to the default `Self::objects()`. Multiple
8814 // attributes accumulate.
8815 let s: LitStr = meta.value()?.parse()?;
8816 let name = s.value();
8817 if name.is_empty() {
8818 return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8819 }
8820 if name == "objects" {
8821 return Err(meta.error(
8822 "`manager_fn = \"objects\"` collides with the default \
8823 accessor — pick a different name",
8824 ));
8825 }
8826 let ident = syn::Ident::new(&name, s.span());
8827 if out.manager_fns.iter().any(|prev| *prev == ident) {
8828 return Err(meta.error(format!(
8829 "duplicate `manager_fn = \"{name}\"`"
8830 )));
8831 }
8832 out.manager_fns.push(ident);
8833 return Ok(());
8834 }
8835 if meta.path.is_ident("default_order") {
8836 // `#[rustango(default_order = "-created_at, status")]`
8837 // — issue #291 / T2.5. Comma-separated list; `-prefix`
8838 // means descending, `+prefix` or bare name means ascending.
8839 // Per-query opt-in via `QuerySet::with_default_order()`.
8840 let s: LitStr = meta.value()?.parse()?;
8841 let raw = s.value();
8842 let span = s.span();
8843 let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
8844 Vec::new();
8845 for entry in raw.split(',') {
8846 let trimmed = entry.trim();
8847 if trimmed.is_empty() {
8848 return Err(syn::Error::new(
8849 span,
8850 "`default_order = \"...\"` has an empty entry — \
8851 check for a stray comma",
8852 ));
8853 }
8854 let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
8855 (true, rest.trim().to_owned())
8856 } else if let Some(rest) = trimmed.strip_prefix('+') {
8857 (false, rest.trim().to_owned())
8858 } else {
8859 (false, trimmed.to_owned())
8860 };
8861 if name.is_empty() {
8862 return Err(syn::Error::new(
8863 span,
8864 "`default_order` entry has no column name after the prefix",
8865 ));
8866 }
8867 if parsed.iter().any(|(n, _, _)| *n == name) {
8868 return Err(syn::Error::new(
8869 span,
8870 format!("duplicate column `{name}` in `default_order`"),
8871 ));
8872 }
8873 parsed.push((name, desc, span));
8874 }
8875 if parsed.is_empty() {
8876 return Err(syn::Error::new(
8877 span,
8878 "`default_order = \"...\"` cannot be empty",
8879 ));
8880 }
8881 out.default_order = parsed;
8882 return Ok(());
8883 }
8884 if meta.path.is_ident("global_scope") {
8885 // `#[rustango(global_scope(name = "active", apply =
8886 // path::to::fn))]` — issue #820. The apply function
8887 // path resolves at macro-expand time in the consumer's
8888 // scope; the macro re-emits it verbatim into the
8889 // `ModelSchema::global_scopes` slice literal.
8890 let span = meta.path.span();
8891 let mut scope_name: Option<String> = None;
8892 let mut apply_path: Option<syn::Path> = None;
8893 meta.parse_nested_meta(|inner| {
8894 if inner.path.is_ident("name") {
8895 let s: LitStr = inner.value()?.parse()?;
8896 let raw = s.value();
8897 if raw.trim().is_empty() {
8898 return Err(syn::Error::new(
8899 s.span(),
8900 "`global_scope(name = \"...\")` must not be empty",
8901 ));
8902 }
8903 scope_name = Some(raw);
8904 return Ok(());
8905 }
8906 if inner.path.is_ident("apply") {
8907 let p: syn::Path = inner.value()?.parse()?;
8908 apply_path = Some(p);
8909 return Ok(());
8910 }
8911 Err(inner.error(
8912 "unknown `global_scope` attribute (supported: \
8913 `name`, `apply`)",
8914 ))
8915 })?;
8916 let Some(name) = scope_name else {
8917 return Err(syn::Error::new(
8918 span,
8919 "`global_scope` requires `name = \"...\"`",
8920 ));
8921 };
8922 let Some(apply) = apply_path else {
8923 return Err(syn::Error::new(
8924 span,
8925 "`global_scope` requires `apply = fn_path`",
8926 ));
8927 };
8928 if out.global_scopes.iter().any(|s| s.name == name) {
8929 return Err(syn::Error::new(
8930 span,
8931 format!(
8932 "duplicate global scope name `{name}` — \
8933 pick a unique identifier so \
8934 `QuerySet::without_global_scope(\"{name}\")` \
8935 is unambiguous"
8936 ),
8937 ));
8938 }
8939 out.global_scopes.push(GlobalScopeAttr { name, apply });
8940 return Ok(());
8941 }
8942 if meta.path.is_ident("through") {
8943 // `#[rustango(through(name = "posts", far = "Post",
8944 // far_fk_column = "author_id", intermediate = "User",
8945 // intermediate_fk_column = "country_id"
8946 // [, intermediate_pk_column = "id"]))]` — issue #817.
8947 let span = meta.path.span();
8948 let mut accessor_name: Option<String> = None;
8949 let mut far_ident: Option<syn::Ident> = None;
8950 let mut far_fk_column: Option<String> = None;
8951 let mut intermediate_ident: Option<syn::Ident> = None;
8952 let mut intermediate_fk_column: Option<String> = None;
8953 let mut intermediate_pk_column: Option<String> = None;
8954 fn parse_nonempty_string(
8955 inner: &syn::meta::ParseNestedMeta<'_>,
8956 field: &str,
8957 ) -> syn::Result<String> {
8958 let s: LitStr = inner.value()?.parse()?;
8959 let raw = s.value();
8960 let trimmed = raw.trim();
8961 if trimmed.is_empty() {
8962 return Err(syn::Error::new(
8963 s.span(),
8964 format!("`through({field} = \"...\")` must not be empty"),
8965 ));
8966 }
8967 Ok(trimmed.to_owned())
8968 }
8969 meta.parse_nested_meta(|inner| {
8970 if inner.path.is_ident("name") {
8971 accessor_name = Some(parse_nonempty_string(&inner, "name")?);
8972 return Ok(());
8973 }
8974 if inner.path.is_ident("far") {
8975 // Far model name accepted as a string literal
8976 // so the attribute fits inside the existing
8977 // `parse_nested_meta` shape; parsed back into a
8978 // `syn::Ident` so the emitted accessor can
8979 // reference the type directly.
8980 let s: LitStr = inner.value()?.parse()?;
8981 let raw = s.value();
8982 let trimmed = raw.trim();
8983 if trimmed.is_empty() {
8984 return Err(syn::Error::new(
8985 s.span(),
8986 "`through(far = \"...\")` must not be empty",
8987 ));
8988 }
8989 far_ident = Some(syn::Ident::new(trimmed, s.span()));
8990 return Ok(());
8991 }
8992 if inner.path.is_ident("far_fk_column") {
8993 far_fk_column =
8994 Some(parse_nonempty_string(&inner, "far_fk_column")?);
8995 return Ok(());
8996 }
8997 if inner.path.is_ident("intermediate") {
8998 let s: LitStr = inner.value()?.parse()?;
8999 let raw = s.value();
9000 let trimmed = raw.trim();
9001 if trimmed.is_empty() {
9002 return Err(syn::Error::new(
9003 s.span(),
9004 "`through(intermediate = \"...\")` must not be empty",
9005 ));
9006 }
9007 intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9008 return Ok(());
9009 }
9010 if inner.path.is_ident("intermediate_fk_column") {
9011 intermediate_fk_column =
9012 Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9013 return Ok(());
9014 }
9015 if inner.path.is_ident("intermediate_pk_column") {
9016 intermediate_pk_column =
9017 Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9018 return Ok(());
9019 }
9020 Err(inner.error(
9021 "unknown `through` attribute (supported: \
9022 `name`, `far`, `far_fk_column`, \
9023 `intermediate`, `intermediate_fk_column`, \
9024 `intermediate_pk_column`)",
9025 ))
9026 })?;
9027 let Some(name) = accessor_name else {
9028 return Err(syn::Error::new(
9029 span,
9030 "`through` requires `name = \"...\"`",
9031 ));
9032 };
9033 let Some(far) = far_ident else {
9034 return Err(syn::Error::new(
9035 span,
9036 "`through` requires `far = \"FarModelType\"`",
9037 ));
9038 };
9039 let Some(far_fk_column) = far_fk_column else {
9040 return Err(syn::Error::new(
9041 span,
9042 "`through` requires `far_fk_column = \"<column>\"`",
9043 ));
9044 };
9045 let Some(intermediate) = intermediate_ident else {
9046 return Err(syn::Error::new(
9047 span,
9048 "`through` requires `intermediate = \"IntermediateModelType\"`",
9049 ));
9050 };
9051 let Some(intermediate_fk_column) = intermediate_fk_column else {
9052 return Err(syn::Error::new(
9053 span,
9054 "`through` requires `intermediate_fk_column = \"<column>\"`",
9055 ));
9056 };
9057 let intermediate_pk_column =
9058 intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9059 if out.through_relations.iter().any(|t| t.name == name) {
9060 return Err(syn::Error::new(
9061 span,
9062 format!(
9063 "duplicate `through(name = \"{name}\")` — \
9064 pick a unique accessor name"
9065 ),
9066 ));
9067 }
9068 out.through_relations.push(ThroughAttr {
9069 name,
9070 far,
9071 far_fk_column,
9072 intermediate,
9073 intermediate_fk_column,
9074 intermediate_pk_column,
9075 });
9076 return Ok(());
9077 }
9078 if meta.path.is_ident("reverse_has") {
9079 // `#[rustango(reverse_has(name = "comments",
9080 // child = "Comment", child_fk_column = "post_id"
9081 // [, self_pk_column = "id"]))]` — issue #830.
9082 let span = meta.path.span();
9083 let mut accessor_name: Option<String> = None;
9084 let mut child_ident: Option<syn::Ident> = None;
9085 let mut child_fk_column: Option<String> = None;
9086 let mut self_pk_column: Option<String> = None;
9087 meta.parse_nested_meta(|inner| {
9088 if inner.path.is_ident("name") {
9089 let s: LitStr = inner.value()?.parse()?;
9090 let raw = s.value();
9091 if raw.trim().is_empty() {
9092 return Err(syn::Error::new(
9093 s.span(),
9094 "`reverse_has(name = \"...\")` must not be empty",
9095 ));
9096 }
9097 accessor_name = Some(raw);
9098 return Ok(());
9099 }
9100 if inner.path.is_ident("child") {
9101 let s: LitStr = inner.value()?.parse()?;
9102 let raw = s.value();
9103 let trimmed = raw.trim();
9104 if trimmed.is_empty() {
9105 return Err(syn::Error::new(
9106 s.span(),
9107 "`reverse_has(child = \"...\")` must not be empty",
9108 ));
9109 }
9110 child_ident = Some(syn::Ident::new(trimmed, s.span()));
9111 return Ok(());
9112 }
9113 if inner.path.is_ident("child_fk_column") {
9114 let s: LitStr = inner.value()?.parse()?;
9115 let raw = s.value();
9116 let trimmed = raw.trim();
9117 if trimmed.is_empty() {
9118 return Err(syn::Error::new(
9119 s.span(),
9120 "`reverse_has(child_fk_column = \"...\")` must not be empty",
9121 ));
9122 }
9123 child_fk_column = Some(trimmed.to_owned());
9124 return Ok(());
9125 }
9126 if inner.path.is_ident("self_pk_column") {
9127 let s: LitStr = inner.value()?.parse()?;
9128 let raw = s.value();
9129 let trimmed = raw.trim();
9130 if trimmed.is_empty() {
9131 return Err(syn::Error::new(
9132 s.span(),
9133 "`reverse_has(self_pk_column = \"...\")` must not be empty",
9134 ));
9135 }
9136 self_pk_column = Some(trimmed.to_owned());
9137 return Ok(());
9138 }
9139 Err(inner.error(
9140 "unknown `reverse_has` attribute (supported: \
9141 `name`, `child`, `child_fk_column`, \
9142 `self_pk_column`)",
9143 ))
9144 })?;
9145 let Some(name) = accessor_name else {
9146 return Err(syn::Error::new(
9147 span,
9148 "`reverse_has` requires `name = \"...\"`",
9149 ));
9150 };
9151 let Some(child) = child_ident else {
9152 return Err(syn::Error::new(
9153 span,
9154 "`reverse_has` requires `child = \"ChildModelType\"`",
9155 ));
9156 };
9157 let Some(child_fk_column) = child_fk_column else {
9158 return Err(syn::Error::new(
9159 span,
9160 "`reverse_has` requires `child_fk_column = \"<column>\"`",
9161 ));
9162 };
9163 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9164 if out.reverse_has_relations.iter().any(|r| r.name == name) {
9165 return Err(syn::Error::new(
9166 span,
9167 format!(
9168 "duplicate `reverse_has(name = \"{name}\")` — \
9169 pick a unique accessor name"
9170 ),
9171 ));
9172 }
9173 out.reverse_has_relations.push(ReverseHasAttr {
9174 name,
9175 child,
9176 child_fk_column,
9177 self_pk_column,
9178 });
9179 return Ok(());
9180 }
9181 if meta.path.is_ident("generic_has") {
9182 // `#[rustango(generic_has(name = "tags",
9183 // child = "Tag", ct_column = "content_type_id",
9184 // pk_column = "object_pk" [, self_pk_column = "id"]))]`
9185 // — issue #830, the reverse generic-FK (GFK) arm of the
9186 // relation-existence family.
9187 let span = meta.path.span();
9188 let mut accessor_name: Option<String> = None;
9189 let mut child_ident: Option<syn::Ident> = None;
9190 let mut ct_column: Option<String> = None;
9191 let mut pk_column: Option<String> = None;
9192 let mut self_pk_column: Option<String> = None;
9193 meta.parse_nested_meta(|inner| {
9194 if inner.path.is_ident("name") {
9195 let s: LitStr = inner.value()?.parse()?;
9196 let raw = s.value();
9197 if raw.trim().is_empty() {
9198 return Err(syn::Error::new(
9199 s.span(),
9200 "`generic_has(name = \"...\")` must not be empty",
9201 ));
9202 }
9203 accessor_name = Some(raw);
9204 return Ok(());
9205 }
9206 if inner.path.is_ident("child") {
9207 let s: LitStr = inner.value()?.parse()?;
9208 let trimmed = s.value().trim().to_owned();
9209 if trimmed.is_empty() {
9210 return Err(syn::Error::new(
9211 s.span(),
9212 "`generic_has(child = \"...\")` must not be empty",
9213 ));
9214 }
9215 child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9216 return Ok(());
9217 }
9218 if inner.path.is_ident("ct_column") {
9219 let s: LitStr = inner.value()?.parse()?;
9220 let trimmed = s.value().trim().to_owned();
9221 if trimmed.is_empty() {
9222 return Err(syn::Error::new(
9223 s.span(),
9224 "`generic_has(ct_column = \"...\")` must not be empty",
9225 ));
9226 }
9227 ct_column = Some(trimmed);
9228 return Ok(());
9229 }
9230 if inner.path.is_ident("pk_column") {
9231 let s: LitStr = inner.value()?.parse()?;
9232 let trimmed = s.value().trim().to_owned();
9233 if trimmed.is_empty() {
9234 return Err(syn::Error::new(
9235 s.span(),
9236 "`generic_has(pk_column = \"...\")` must not be empty",
9237 ));
9238 }
9239 pk_column = Some(trimmed);
9240 return Ok(());
9241 }
9242 if inner.path.is_ident("self_pk_column") {
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(self_pk_column = \"...\")` must not be empty",
9249 ));
9250 }
9251 self_pk_column = Some(trimmed);
9252 return Ok(());
9253 }
9254 Err(inner.error(
9255 "unknown `generic_has` attribute (supported: \
9256 `name`, `child`, `ct_column`, `pk_column`, \
9257 `self_pk_column`)",
9258 ))
9259 })?;
9260 let Some(name) = accessor_name else {
9261 return Err(syn::Error::new(
9262 span,
9263 "`generic_has` requires `name = \"...\"`",
9264 ));
9265 };
9266 let Some(child) = child_ident else {
9267 return Err(syn::Error::new(
9268 span,
9269 "`generic_has` requires `child = \"ChildModelType\"`",
9270 ));
9271 };
9272 let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9273 let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9274 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9275 if out.generic_has_relations.iter().any(|r| r.name == name) {
9276 return Err(syn::Error::new(
9277 span,
9278 format!(
9279 "duplicate `generic_has(name = \"{name}\")` — \
9280 pick a unique accessor name"
9281 ),
9282 ));
9283 }
9284 out.generic_has_relations.push(GenericHasAttr {
9285 name,
9286 child,
9287 ct_column,
9288 pk_column,
9289 self_pk_column,
9290 });
9291 return Ok(());
9292 }
9293 if meta.path.is_ident("audit") {
9294 let mut audit = AuditAttrs::default();
9295 meta.parse_nested_meta(|inner| {
9296 if inner.path.is_ident("track") {
9297 let s: LitStr = inner.value()?.parse()?;
9298 audit.track =
9299 Some((split_field_list(&s.value()), s.span()));
9300 return Ok(());
9301 }
9302 Err(inner.error(
9303 "unknown audit attribute (supported: `track`)",
9304 ))
9305 })?;
9306 out.audit = Some(audit);
9307 return Ok(());
9308 }
9309 if meta.path.is_ident("permissions") {
9310 // Two forms accepted:
9311 // #[rustango(permissions)] — flag form, true
9312 // #[rustango(permissions = false)] — explicit opt-out
9313 // #[rustango(permissions = true)] — explicit opt-in
9314 if let Ok(v) = meta.value() {
9315 let lit: syn::LitBool = v.parse()?;
9316 out.permissions = lit.value;
9317 } else {
9318 out.permissions = true;
9319 }
9320 return Ok(());
9321 }
9322 if meta.path.is_ident("view") {
9323 // Issue #293 / T2.10. Two forms accepted, matching
9324 // the `permissions` flag pattern:
9325 // #[rustango(view)] — flag form, true
9326 // #[rustango(view = false)] — explicit opt-out
9327 // #[rustango(view = true)] — explicit opt-in
9328 if let Ok(v) = meta.value() {
9329 let lit: syn::LitBool = v.parse()?;
9330 out.is_view = lit.value;
9331 } else {
9332 out.is_view = true;
9333 }
9334 return Ok(());
9335 }
9336 if meta.path.is_ident("managed") {
9337 // Django-shape Meta.managed. Issue #321.
9338 // #[rustango(managed = false)] — operator-managed table
9339 // #[rustango(managed = true)] — rustango-managed (the default)
9340 // Bare-flag form is intentionally not accepted: writing
9341 // `#[rustango(managed)]` reads as "yes please manage it"
9342 // which is already the default. The opt-out is the only
9343 // useful state, so it must be explicit.
9344 let v = meta.value()?;
9345 let lit: syn::LitBool = v.parse()?;
9346 out.managed = lit.value;
9347 return Ok(());
9348 }
9349 if meta.path.is_ident("verbose_name") {
9350 let s: LitStr = meta.value()?.parse()?;
9351 out.verbose_name = Some(s.value());
9352 return Ok(());
9353 }
9354 if meta.path.is_ident("verbose_name_plural") {
9355 let s: LitStr = meta.value()?.parse()?;
9356 out.verbose_name_plural = Some(s.value());
9357 return Ok(());
9358 }
9359 if meta.path.is_ident("db_table_comment") {
9360 // Django-shape `Meta.db_table_comment` (4.2+) — free-form
9361 // table-level comment attached to the DB catalog.
9362 let s: LitStr = meta.value()?.parse()?;
9363 out.db_table_comment = Some(s.value());
9364 return Ok(());
9365 }
9366 if meta.path.is_ident("proxy") {
9367 // Django-shape `Meta.proxy = True` — declarative flag
9368 // marking the struct as a proxy of another model that
9369 // shares its DB table. Stored on `ModelSchema::proxy`
9370 // so future codegen can skip `CreateTable` emission
9371 // for proxies (parent owns the table) and route
9372 // per-instance method resolution to the proxy class.
9373 //
9374 // Accepts `proxy` (bare → true) and `proxy = true/false`.
9375 let value = if meta.input.peek(syn::Token![=]) {
9376 meta.value()?.parse::<syn::LitBool>()?.value
9377 } else {
9378 true
9379 };
9380 out.proxy = value;
9381 return Ok(());
9382 }
9383 if meta.path.is_ident("order_with_respect_to") {
9384 // Django-shape `Meta.order_with_respect_to = "parent_fk"` —
9385 // the model's instances are intrinsically ordered
9386 // relative to their parent FK. Django auto-generates
9387 // a `_order` integer column + admin reordering UI.
9388 //
9389 // rustango stores the FK field name on
9390 // `ModelSchema::order_with_respect_to`. Declarative-only
9391 // today: the migration writer + admin surfaces still
9392 // treat every model identically. Future codegen will
9393 // key off the metadata to auto-emit the `_order`
9394 // column and reorder helpers.
9395 //
9396 // Validated as a Rust-shape identifier so the macro
9397 // can reject typos at derive time.
9398 let s: LitStr = meta.value()?.parse()?;
9399 let raw = s.value();
9400 if raw.is_empty() {
9401 return Err(syn::Error::new(
9402 s.span(),
9403 "`order_with_respect_to` must be a non-empty FK field name",
9404 ));
9405 }
9406 let valid = raw
9407 .chars()
9408 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9409 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9410 if !valid {
9411 return Err(syn::Error::new(
9412 s.span(),
9413 format!(
9414 "`order_with_respect_to` must be a valid Rust \
9415 identifier (letters / digits / underscores, \
9416 not starting with a digit); got `{raw}`"
9417 ),
9418 ));
9419 }
9420 out.order_with_respect_to = Some(raw);
9421 return Ok(());
9422 }
9423 if meta.path.is_ident("required_db_features") {
9424 // Django-shape `Meta.required_db_features` — capability
9425 // tokens the model needs (e.g. `"json_extract"`,
9426 // `"window_functions"`, `"row_security"`). Comma-separated.
9427 // `manage check --deploy` walks every model and warns
9428 // when the active backend doesn't advertise the
9429 // capability.
9430 //
9431 // rustango ships a small registry of capability tokens
9432 // each dialect supports — see `Dialect::supports`.
9433 // Unknown tokens still parse (they end up on the
9434 // schema and show up in the warning) so projects can
9435 // declare aspirational capabilities and the check
9436 // verb will keep nagging until the dialect implements
9437 // them.
9438 let s: LitStr = meta.value()?.parse()?;
9439 let raw = s.value();
9440 let features: Vec<String> = raw
9441 .split(',')
9442 .map(str::trim)
9443 .filter(|s| !s.is_empty())
9444 .map(str::to_owned)
9445 .collect();
9446 if features.is_empty() {
9447 return Err(syn::Error::new(
9448 s.span(),
9449 "`required_db_features` must list at least one \
9450 comma-separated capability token",
9451 ));
9452 }
9453 out.required_db_features = features;
9454 return Ok(());
9455 }
9456 if meta.path.is_ident("required_db_vendor") {
9457 // Django-shape `Meta.required_db_vendor` — the model
9458 // is only meant to run against the named DB backend.
9459 // `manage check --deploy` flags a mismatch so
9460 // ops catches "I forgot to switch DATABASE_URL" at
9461 // deploy time rather than runtime.
9462 //
9463 // Django spells it as a free-form string; rustango
9464 // restricts to the three backends it ships dialects
9465 // for so the check verb can compare reliably.
9466 let s: LitStr = meta.value()?.parse()?;
9467 let raw = s.value().to_ascii_lowercase();
9468 match raw.as_str() {
9469 "postgresql" | "postgres" | "pg" => {
9470 out.required_db_vendor = Some("postgres".to_owned());
9471 }
9472 "mysql" | "mariadb" => {
9473 out.required_db_vendor = Some("mysql".to_owned());
9474 }
9475 "sqlite" | "sqlite3" => {
9476 out.required_db_vendor = Some("sqlite".to_owned());
9477 }
9478 _ => {
9479 return Err(syn::Error::new(
9480 s.span(),
9481 format!(
9482 "unknown required_db_vendor `{raw}` — \
9483 expected `postgres` (aliases: `postgresql`, `pg`), \
9484 `mysql` (alias: `mariadb`), or `sqlite` \
9485 (alias: `sqlite3`)"
9486 ),
9487 ));
9488 }
9489 }
9490 return Ok(());
9491 }
9492 if meta.path.is_ident("base_manager_name") {
9493 // Django-shape `Meta.base_manager_name` — name of the
9494 // Manager subclass that `<instance>.<relation>_set`
9495 // uses when resolving reverse-relation managers.
9496 // Distinct from `default_manager_name` (what
9497 // `Model.objects` returns at the class level).
9498 // Stored on `ModelSchema::base_manager_name`.
9499 //
9500 // Validated as a Rust identifier so it stays safe to
9501 // re-emit as code in future reverse-manager codegen.
9502 let s: LitStr = meta.value()?.parse()?;
9503 let raw = s.value();
9504 if raw.is_empty() {
9505 return Err(syn::Error::new(
9506 s.span(),
9507 "`base_manager_name` must be a non-empty string",
9508 ));
9509 }
9510 let valid = raw
9511 .chars()
9512 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9513 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9514 if !valid {
9515 return Err(syn::Error::new(
9516 s.span(),
9517 format!(
9518 "`base_manager_name` must be a valid Rust \
9519 identifier (letters / digits / underscores, \
9520 not starting with a digit); got `{raw}`"
9521 ),
9522 ));
9523 }
9524 out.base_manager_name = Some(raw);
9525 return Ok(());
9526 }
9527 if meta.path.is_ident("default_related_name") {
9528 // Django-shape `Meta.default_related_name` — the name
9529 // reverse-relation accessors use when callers don't
9530 // override `related_name=...` on the FK / M2M field.
9531 // Stored on `ModelSchema::default_related_name` so
9532 // future reverse-manager codegen / DRF schema emit /
9533 // admin templates can pick the right accessor name
9534 // (today rustango doesn't auto-emit reverse managers;
9535 // the metadata is the foundation for that work).
9536 //
9537 // Django requires snake_case + no `+` suffix; we
9538 // enforce non-empty + ASCII identifier-shape so the
9539 // string is safe to use as a Rust ident later.
9540 let s: LitStr = meta.value()?.parse()?;
9541 let raw = s.value();
9542 if raw.is_empty() {
9543 return Err(syn::Error::new(
9544 s.span(),
9545 "`default_related_name` must be a non-empty string",
9546 ));
9547 }
9548 let valid = raw
9549 .chars()
9550 .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9551 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9552 if !valid {
9553 return Err(syn::Error::new(
9554 s.span(),
9555 format!(
9556 "`default_related_name` must be snake_case ASCII \
9557 (lowercase letters / digits / underscores, not \
9558 starting with a digit); got `{raw}`"
9559 ),
9560 ));
9561 }
9562 out.default_related_name = Some(raw);
9563 return Ok(());
9564 }
9565 if meta.path.is_ident("extra_permissions") {
9566 // Django-shape `Meta.permissions = [(codename, name), ...]`.
9567 // Comma-separated `codename:label` pairs.
9568 let s: LitStr = meta.value()?.parse()?;
9569 let raw = s.value();
9570 let mut pairs = Vec::new();
9571 for entry in raw.split(',') {
9572 let entry = entry.trim();
9573 if entry.is_empty() {
9574 continue;
9575 }
9576 let (codename, label) = match entry.split_once(':') {
9577 Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9578 None => (entry.to_owned(), entry.to_owned()),
9579 };
9580 if codename.is_empty() {
9581 return Err(meta.error(
9582 "`extra_permissions` entries must be `codename:label` pairs",
9583 ));
9584 }
9585 pairs.push((codename, label));
9586 }
9587 if pairs.is_empty() {
9588 return Err(meta
9589 .error("`extra_permissions = \"…\"` must list at least one pair"));
9590 }
9591 out.extra_permissions = pairs;
9592 return Ok(());
9593 }
9594 if meta.path.is_ident("default_permissions") {
9595 // Django-shape `Meta.default_permissions = ('view',
9596 // 'change')`. Comma-separated subset of the CRUD
9597 // action set. Empty means all four (the framework
9598 // default — matches Django when the option is
9599 // omitted).
9600 let s: LitStr = meta.value()?.parse()?;
9601 let raw = s.value();
9602 let mut actions: Vec<String> = Vec::new();
9603 for entry in raw.split(',') {
9604 let action = entry.trim().to_ascii_lowercase();
9605 if action.is_empty() {
9606 continue;
9607 }
9608 match action.as_str() {
9609 "add" | "change" | "delete" | "view" => {}
9610 other => {
9611 return Err(syn::Error::new(
9612 s.span(),
9613 format!(
9614 "unknown default_permissions action `{other}` — \
9615 expected one of `add`, `change`, `delete`, `view`"
9616 ),
9617 ));
9618 }
9619 }
9620 if !actions.contains(&action) {
9621 actions.push(action);
9622 }
9623 }
9624 if actions.is_empty() {
9625 return Err(syn::Error::new(
9626 s.span(),
9627 "`default_permissions = \"…\"` must list at least one action; \
9628 use `permissions = false` on the container if you want NO \
9629 permissions seeded for this model.",
9630 ));
9631 }
9632 out.default_permissions = actions;
9633 return Ok(());
9634 }
9635 if meta.path.is_ident("get_latest_by") {
9636 // Django-shape `Meta.get_latest_by`. The `-` prefix
9637 // selects descending order (Django muscle memory).
9638 let s: LitStr = meta.value()?.parse()?;
9639 let raw = s.value();
9640 let trimmed = raw.trim();
9641 if trimmed.is_empty() {
9642 return Err(meta.error("`get_latest_by` must name a column"));
9643 }
9644 let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9645 (stripped.to_owned(), true)
9646 } else if let Some(stripped) = trimmed.strip_prefix('+') {
9647 (stripped.to_owned(), false)
9648 } else {
9649 (trimmed.to_owned(), false)
9650 };
9651 if col.is_empty() {
9652 return Err(meta.error("`get_latest_by` must name a column"));
9653 }
9654 out.get_latest_by = Some((col, desc));
9655 return Ok(());
9656 }
9657 if meta.path.is_ident("unique_together") {
9658 // Django-shape composite UNIQUE index. Two syntaxes:
9659 //
9660 // #[rustango(unique_together = "org_id, user_id")] — auto-derived name
9661 // #[rustango(unique_together(columns = "org_id, user_id", name = "x"))] — explicit name
9662 //
9663 // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9664 // (col1, col2)`, where <name> defaults to
9665 // `<table>_<col1>_<col2>_uq` when not supplied.
9666 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9667 out.indexes.push(IndexAttr {
9668 name,
9669 columns,
9670 unique: true,
9671 method: "btree".to_owned(),
9672 where_clause: None,
9673 include: Vec::new(),
9674 });
9675 return Ok(());
9676 }
9677 if meta.path.is_ident("index_together") {
9678 // Django-shape composite (non-unique) index. Two syntaxes
9679 // mirroring `unique_together`.
9680 //
9681 // #[rustango(index_together = "created_at, status")]
9682 // #[rustango(index_together(columns = "created_at, status", name = "x"))]
9683 let (columns, name) = parse_together_attr(&meta, "index_together")?;
9684 out.indexes.push(IndexAttr {
9685 name,
9686 columns,
9687 unique: false,
9688 method: "btree".to_owned(),
9689 where_clause: None,
9690 include: Vec::new(),
9691 });
9692 return Ok(());
9693 }
9694 if meta.path.is_ident("unique_when") {
9695 // Django 4.0+ `UniqueConstraint(condition=Q(...))` —
9696 // partial unique index. Issue #265 / T1.3.
9697 //
9698 // #[rustango(unique_when(
9699 // columns = "email",
9700 // condition = "deleted_at IS NULL",
9701 // name = "unique_active_email"
9702 // ))]
9703 //
9704 // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9705 // on PG / SQLite (both ship partial indexes natively).
9706 // MySQL falls back to a plain UNIQUE index — the
9707 // condition is lost; document the limitation in the
9708 // generated migration.
9709 let mut columns: Option<Vec<String>> = None;
9710 let mut condition: Option<String> = None;
9711 let mut name: Option<String> = None;
9712 let mut include: Vec<String> = Vec::new();
9713 meta.parse_nested_meta(|inner| {
9714 if inner.path.is_ident("columns") {
9715 let s: LitStr = inner.value()?.parse()?;
9716 columns = Some(split_field_list(&s.value()));
9717 return Ok(());
9718 }
9719 if inner.path.is_ident("condition") {
9720 let s: LitStr = inner.value()?.parse()?;
9721 condition = Some(s.value());
9722 return Ok(());
9723 }
9724 if inner.path.is_ident("name") {
9725 let s: LitStr = inner.value()?.parse()?;
9726 name = Some(s.value());
9727 return Ok(());
9728 }
9729 if inner.path.is_ident("include") {
9730 // Django `UniqueConstraint(include=[...])` — PG
9731 // 11+ covering-index columns. Non-key columns
9732 // travel with the index leaf for index-only
9733 // scans. Dropped on MySQL/SQLite by the writer.
9734 let s: LitStr = inner.value()?.parse()?;
9735 include = split_field_list(&s.value());
9736 return Ok(());
9737 }
9738 Err(inner.error(
9739 "unknown unique_when attribute (supported: \
9740 `columns = \"...\"`, `condition = \"...\"`, \
9741 `name = \"...\"`, `include = \"...\"`)",
9742 ))
9743 })?;
9744 let columns = columns.ok_or_else(|| {
9745 meta.error("`unique_when(...)` requires `columns = \"...\"`")
9746 })?;
9747 let condition = condition.ok_or_else(|| {
9748 meta.error("`unique_when(...)` requires `condition = \"...\"`")
9749 })?;
9750 if columns.is_empty() {
9751 return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9752 }
9753 out.indexes.push(IndexAttr {
9754 name,
9755 columns,
9756 unique: true,
9757 method: "btree".to_owned(),
9758 where_clause: Some(condition),
9759 include,
9760 });
9761 return Ok(());
9762 }
9763 if meta.path.is_ident("index_when") {
9764 // Django `Index(fields=..., condition=Q(...))` parity —
9765 // non-unique partial index. Sibling of `unique_when`
9766 // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9767 //
9768 // #[rustango(index_when(
9769 // columns = "status, created_at",
9770 // condition = "deleted_at IS NULL",
9771 // name = "active_status_created_idx"
9772 // ))]
9773 //
9774 // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9775 // on PG / SQLite (both ship partial indexes natively).
9776 // MySQL has no native partial-index support — the writer
9777 // emits a plain CREATE INDEX and the condition is lost;
9778 // operators wanting that selectivity on MySQL should
9779 // declare a covering index plus an application-level
9780 // filter.
9781 let mut columns: Option<Vec<String>> = None;
9782 let mut condition: Option<String> = None;
9783 let mut name: Option<String> = None;
9784 let mut method: String = "btree".to_owned();
9785 let mut include: Vec<String> = Vec::new();
9786 meta.parse_nested_meta(|inner| {
9787 if inner.path.is_ident("columns") {
9788 let s: LitStr = inner.value()?.parse()?;
9789 columns = Some(split_field_list(&s.value()));
9790 return Ok(());
9791 }
9792 if inner.path.is_ident("condition") {
9793 let s: LitStr = inner.value()?.parse()?;
9794 condition = Some(s.value());
9795 return Ok(());
9796 }
9797 if inner.path.is_ident("name") {
9798 let s: LitStr = inner.value()?.parse()?;
9799 name = Some(s.value());
9800 return Ok(());
9801 }
9802 if inner.path.is_ident("method") {
9803 let s: LitStr = inner.value()?.parse()?;
9804 method = s.value();
9805 return Ok(());
9806 }
9807 if inner.path.is_ident("include") {
9808 // Django `Index(include=[...])` — PG 11+
9809 // covering-index columns; non-key columns
9810 // travel with the index leaf. Dropped on
9811 // MySQL/SQLite.
9812 let s: LitStr = inner.value()?.parse()?;
9813 include = split_field_list(&s.value());
9814 return Ok(());
9815 }
9816 Err(inner.error(
9817 "unknown index_when attribute (supported: \
9818 `columns = \"...\"`, `condition = \"...\"`, \
9819 `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9820 `include = \"...\"`)",
9821 ))
9822 })?;
9823 let columns = columns
9824 .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9825 let condition = condition.ok_or_else(|| {
9826 meta.error("`index_when(...)` requires `condition = \"...\"`")
9827 })?;
9828 if columns.is_empty() {
9829 return Err(meta.error("`index_when(columns = \"\")` is empty"));
9830 }
9831 out.indexes.push(IndexAttr {
9832 name,
9833 columns,
9834 unique: false,
9835 method,
9836 where_clause: Some(condition),
9837 include,
9838 });
9839 return Ok(());
9840 }
9841 if meta.path.is_ident("index") {
9842 // Container-level composite index. Two syntaxes:
9843 // #[rustango(index = "col1, col2")] — bare, non-unique btree
9844 // #[rustango(index("col1, col2"))] — call form (same result)
9845 // #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
9846 // The call form takes the column list as a leading string
9847 // literal, then optional `unique` / `name = "..."` /
9848 // `method = "..."` flags (a leading literal can't compose
9849 // under `parse_nested_meta`, so the paren body is parsed by
9850 // hand). `unique_together` / `index_together` remain the
9851 // Django-shape aliases for the same feature.
9852 let cols_lit: LitStr;
9853 let mut unique = false;
9854 let mut name: Option<String> = None;
9855 let mut method = "btree".to_owned();
9856 if meta.input.peek(syn::token::Paren) {
9857 let content;
9858 syn::parenthesized!(content in meta.input);
9859 cols_lit = content.parse()?;
9860 while content.peek(syn::Token![,]) {
9861 content.parse::<syn::Token![,]>()?;
9862 if content.is_empty() {
9863 break;
9864 }
9865 let flag: syn::Ident = content.parse()?;
9866 if flag == "unique" {
9867 unique = true;
9868 } else if flag == "name" {
9869 content.parse::<syn::Token![=]>()?;
9870 let s: LitStr = content.parse()?;
9871 name = Some(s.value());
9872 } else if flag == "method" {
9873 content.parse::<syn::Token![=]>()?;
9874 let s: LitStr = content.parse()?;
9875 let v = s.value();
9876 match v.as_str() {
9877 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
9878 | "bloom" => method = v,
9879 other => {
9880 return Err(syn::Error::new(
9881 s.span(),
9882 format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
9883 ));
9884 }
9885 }
9886 } else {
9887 return Err(syn::Error::new(
9888 flag.span(),
9889 "unknown index flag (supported: `unique`, `name`, `method`)",
9890 ));
9891 }
9892 }
9893 } else {
9894 cols_lit = meta.value()?.parse()?;
9895 }
9896 let columns = split_field_list(&cols_lit.value());
9897 out.indexes.push(IndexAttr {
9898 name,
9899 columns,
9900 unique,
9901 method,
9902 where_clause: None,
9903 include: Vec::new(),
9904 });
9905 return Ok(());
9906 }
9907 if meta.path.is_ident("check") {
9908 // #[rustango(check(name = "…", expr = "…"))]
9909 let mut name: Option<String> = None;
9910 let mut expr: Option<String> = None;
9911 meta.parse_nested_meta(|inner| {
9912 if inner.path.is_ident("name") {
9913 let s: LitStr = inner.value()?.parse()?;
9914 name = Some(s.value());
9915 return Ok(());
9916 }
9917 if inner.path.is_ident("expr") {
9918 let s: LitStr = inner.value()?.parse()?;
9919 expr = Some(s.value());
9920 return Ok(());
9921 }
9922 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
9923 })?;
9924 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
9925 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
9926 out.checks.push(CheckAttr { name, expr });
9927 return Ok(());
9928 }
9929 if meta.path.is_ident("exclude") {
9930 // #[rustango(exclude(name = "…", using = "gist",
9931 // elements = "col WITH op, col WITH op",
9932 // where = "…"))]
9933 let mut name: Option<String> = None;
9934 let mut using: Option<String> = None;
9935 let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
9936 let mut where_clause: Option<String> = None;
9937 meta.parse_nested_meta(|inner| {
9938 if inner.path.is_ident("name") {
9939 let s: LitStr = inner.value()?.parse()?;
9940 name = Some(s.value());
9941 return Ok(());
9942 }
9943 if inner.path.is_ident("using") {
9944 let s: LitStr = inner.value()?.parse()?;
9945 using = Some(s.value());
9946 return Ok(());
9947 }
9948 if inner.path.is_ident("elements") {
9949 let s: LitStr = inner.value()?.parse()?;
9950 elements_raw = Some((s.value(), s.span()));
9951 return Ok(());
9952 }
9953 if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
9954 let s: LitStr = inner.value()?.parse()?;
9955 where_clause = Some(s.value());
9956 return Ok(());
9957 }
9958 Err(inner.error(
9959 "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
9960 ))
9961 })?;
9962 let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
9963 let using = using.unwrap_or_else(|| "gist".to_owned());
9964 let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
9965 meta.error(
9966 "exclude requires `elements = \"col WITH op, col WITH op\"`",
9967 )
9968 })?;
9969 // Parse `col WITH op` pairs separated by commas.
9970 let mut elements: Vec<(String, String)> = Vec::new();
9971 for pair in elements_str.split(',') {
9972 let pair = pair.trim();
9973 if pair.is_empty() {
9974 continue;
9975 }
9976 let mut split = pair.splitn(2, |c: char| c.is_whitespace());
9977 let col = split.next().unwrap_or("").trim();
9978 let rest = split.next().unwrap_or("").trim();
9979 // `WITH op` — case-insensitive on `WITH`, then op.
9980 let rest_lc = rest.to_ascii_lowercase();
9981 let op = rest_lc
9982 .strip_prefix("with")
9983 .map(|r| r.trim_start())
9984 .filter(|r| !r.is_empty())
9985 .map(|_| {
9986 // Pull the original-case op from `rest` after the
9987 // `WITH ` token (5 chars).
9988 rest[4..].trim_start().to_owned()
9989 });
9990 let Some(op) = op else {
9991 return Err(syn::Error::new(
9992 elements_span,
9993 format!(
9994 "exclude elements: `{pair}` must be `<col> WITH <op>` \
9995 (e.g. `room_id WITH =` or `during WITH &&`)"
9996 ),
9997 ));
9998 };
9999 if col.is_empty() || op.is_empty() {
10000 return Err(syn::Error::new(
10001 elements_span,
10002 format!(
10003 "exclude elements: `{pair}` must be `<col> WITH <op>` \
10004 (both sides non-empty)"
10005 ),
10006 ));
10007 }
10008 elements.push((col.to_owned(), op));
10009 }
10010 if elements.is_empty() {
10011 return Err(syn::Error::new(
10012 elements_span,
10013 "exclude requires at least one `col WITH op` element",
10014 ));
10015 }
10016 out.excludes.push(ExcludeAttr {
10017 name,
10018 using,
10019 elements,
10020 where_clause,
10021 });
10022 return Ok(());
10023 }
10024 if meta.path.is_ident("generic_fk") {
10025 let mut gfk = GenericFkAttr {
10026 name: String::new(),
10027 ct_column: String::new(),
10028 pk_column: String::new(),
10029 };
10030 meta.parse_nested_meta(|inner| {
10031 if inner.path.is_ident("name") {
10032 let s: LitStr = inner.value()?.parse()?;
10033 gfk.name = s.value();
10034 return Ok(());
10035 }
10036 if inner.path.is_ident("ct_column") {
10037 let s: LitStr = inner.value()?.parse()?;
10038 gfk.ct_column = s.value();
10039 return Ok(());
10040 }
10041 if inner.path.is_ident("pk_column") {
10042 let s: LitStr = inner.value()?.parse()?;
10043 gfk.pk_column = s.value();
10044 return Ok(());
10045 }
10046 Err(inner.error(
10047 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10048 ))
10049 })?;
10050 if gfk.name.is_empty() {
10051 return Err(meta.error("generic_fk requires `name = \"...\"`"));
10052 }
10053 if gfk.ct_column.is_empty() {
10054 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10055 }
10056 if gfk.pk_column.is_empty() {
10057 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10058 }
10059 out.generic_fks.push(gfk);
10060 return Ok(());
10061 }
10062 if meta.path.is_ident("fk_composite") {
10063 let mut fk = CompositeFkAttr {
10064 name: String::new(),
10065 to: String::new(),
10066 from: Vec::new(),
10067 on: Vec::new(),
10068 };
10069 meta.parse_nested_meta(|inner| {
10070 if inner.path.is_ident("name") {
10071 let s: LitStr = inner.value()?.parse()?;
10072 fk.name = s.value();
10073 return Ok(());
10074 }
10075 if inner.path.is_ident("to") {
10076 let s: LitStr = inner.value()?.parse()?;
10077 fk.to = s.value();
10078 return Ok(());
10079 }
10080 // `on = ("col1", "col2", ...)` — parse a parenthesised
10081 // comma-list of string literals.
10082 if inner.path.is_ident("on") || inner.path.is_ident("from") {
10083 let value = inner.value()?;
10084 let content;
10085 syn::parenthesized!(content in value);
10086 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10087 content.parse_terminated(
10088 |p| p.parse::<syn::LitStr>(),
10089 syn::Token![,],
10090 )?;
10091 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10092 if inner.path.is_ident("on") {
10093 fk.on = cols;
10094 } else {
10095 fk.from = cols;
10096 }
10097 return Ok(());
10098 }
10099 Err(inner.error(
10100 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10101 ))
10102 })?;
10103 if fk.name.is_empty() {
10104 return Err(meta.error("fk_composite requires `name = \"...\"`"));
10105 }
10106 if fk.to.is_empty() {
10107 return Err(meta.error("fk_composite requires `to = \"...\"`"));
10108 }
10109 if fk.from.is_empty() || fk.on.is_empty() {
10110 return Err(meta.error(
10111 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10112 ));
10113 }
10114 if fk.from.len() != fk.on.len() {
10115 return Err(meta.error(format!(
10116 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10117 fk.from.len(),
10118 fk.on.len(),
10119 )));
10120 }
10121 out.composite_fks.push(fk);
10122 return Ok(());
10123 }
10124 if meta.path.is_ident("m2m") {
10125 let mut m2m = M2MAttr {
10126 name: String::new(),
10127 to: String::new(),
10128 through: String::new(),
10129 src: String::new(),
10130 dst: String::new(),
10131 auto_create: true,
10132 };
10133 meta.parse_nested_meta(|inner| {
10134 if inner.path.is_ident("name") {
10135 let s: LitStr = inner.value()?.parse()?;
10136 m2m.name = s.value();
10137 return Ok(());
10138 }
10139 if inner.path.is_ident("to") {
10140 let s: LitStr = inner.value()?.parse()?;
10141 m2m.to = s.value();
10142 return Ok(());
10143 }
10144 if inner.path.is_ident("through") {
10145 let s: LitStr = inner.value()?.parse()?;
10146 m2m.through = s.value();
10147 return Ok(());
10148 }
10149 if inner.path.is_ident("src") {
10150 let s: LitStr = inner.value()?.parse()?;
10151 m2m.src = s.value();
10152 return Ok(());
10153 }
10154 if inner.path.is_ident("dst") {
10155 let s: LitStr = inner.value()?.parse()?;
10156 m2m.dst = s.value();
10157 return Ok(());
10158 }
10159 if inner.path.is_ident("auto_create") {
10160 let lit: syn::LitBool = inner.value()?.parse()?;
10161 m2m.auto_create = lit.value;
10162 return Ok(());
10163 }
10164 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10165 })?;
10166 if m2m.name.is_empty() {
10167 return Err(meta.error("m2m requires `name = \"...\"`"));
10168 }
10169 if m2m.to.is_empty() {
10170 return Err(meta.error("m2m requires `to = \"...\"`"));
10171 }
10172 if m2m.through.is_empty() {
10173 return Err(meta.error("m2m requires `through = \"...\"`"));
10174 }
10175 if m2m.src.is_empty() {
10176 return Err(meta.error("m2m requires `src = \"...\"`"));
10177 }
10178 if m2m.dst.is_empty() {
10179 return Err(meta.error("m2m requires `dst = \"...\"`"));
10180 }
10181 out.m2m.push(m2m);
10182 return Ok(());
10183 }
10184 if meta.path.is_ident("generic_m2m") {
10185 let mut gm = GenericM2MAttr {
10186 name: String::new(),
10187 through: String::new(),
10188 pk_column: String::new(),
10189 ct_column: String::new(),
10190 related_column: String::new(),
10191 };
10192 meta.parse_nested_meta(|inner| {
10193 let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10194 let s: LitStr = inner.value()?.parse()?;
10195 Ok(s.value())
10196 };
10197 if inner.path.is_ident("name") {
10198 gm.name = field(&inner)?;
10199 return Ok(());
10200 }
10201 if inner.path.is_ident("through") {
10202 gm.through = field(&inner)?;
10203 return Ok(());
10204 }
10205 if inner.path.is_ident("pk_column") {
10206 gm.pk_column = field(&inner)?;
10207 return Ok(());
10208 }
10209 if inner.path.is_ident("ct_column") {
10210 gm.ct_column = field(&inner)?;
10211 return Ok(());
10212 }
10213 if inner.path.is_ident("related_column") {
10214 gm.related_column = field(&inner)?;
10215 return Ok(());
10216 }
10217 Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10218 })?;
10219 for (val, label) in [
10220 (&gm.name, "name"),
10221 (&gm.through, "through"),
10222 (&gm.pk_column, "pk_column"),
10223 (&gm.ct_column, "ct_column"),
10224 (&gm.related_column, "related_column"),
10225 ] {
10226 if val.is_empty() {
10227 return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10228 }
10229 }
10230 out.generic_m2m.push(gm);
10231 return Ok(());
10232 }
10233 Err(meta.error("unknown rustango container attribute"))
10234 })?;
10235 }
10236 Ok(out)
10237}
10238
10239/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10240/// owned field names, trimming whitespace and skipping empty entries.
10241/// Field-name validation against the model is done by the caller.
10242fn split_field_list(raw: &str) -> Vec<String> {
10243 raw.split(',')
10244 .map(str::trim)
10245 .filter(|s| !s.is_empty())
10246 .map(str::to_owned)
10247 .collect()
10248}
10249
10250/// Shared parser for `unique_together` and `index_together` container
10251/// attrs. Accepts both shapes:
10252///
10253/// * `attr = "col1, col2"` — auto-derived index name.
10254/// * `attr(columns = "col1, col2", name = "...")` — explicit name.
10255///
10256/// Returns `(columns, name)`.
10257fn parse_together_attr(
10258 meta: &syn::meta::ParseNestedMeta<'_>,
10259 attr: &str,
10260) -> syn::Result<(Vec<String>, Option<String>)> {
10261 // Disambiguate by whether the next token is `=` (key-value) or
10262 // `(` (parenthesized).
10263 if meta.input.peek(syn::Token![=]) {
10264 let cols_lit: LitStr = meta.value()?.parse()?;
10265 let columns = split_field_list(&cols_lit.value());
10266 check_together_columns(meta, attr, &columns)?;
10267 return Ok((columns, None));
10268 }
10269 let mut columns: Option<Vec<String>> = None;
10270 let mut name: Option<String> = None;
10271 meta.parse_nested_meta(|inner| {
10272 if inner.path.is_ident("columns") {
10273 let s: LitStr = inner.value()?.parse()?;
10274 columns = Some(split_field_list(&s.value()));
10275 return Ok(());
10276 }
10277 if inner.path.is_ident("name") {
10278 let s: LitStr = inner.value()?.parse()?;
10279 name = Some(s.value());
10280 return Ok(());
10281 }
10282 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10283 })?;
10284 let columns = columns.ok_or_else(|| {
10285 meta.error(format!(
10286 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10287 ))
10288 })?;
10289 check_together_columns(meta, attr, &columns)?;
10290 Ok((columns, name))
10291}
10292
10293fn check_together_columns(
10294 meta: &syn::meta::ParseNestedMeta<'_>,
10295 attr: &str,
10296 columns: &[String],
10297) -> syn::Result<()> {
10298 if columns.len() < 2 {
10299 let single = if attr == "unique_together" {
10300 "#[rustango(unique)] on the field"
10301 } else {
10302 "#[rustango(index)] on the field"
10303 };
10304 return Err(meta.error(format!(
10305 "{attr} expects two or more columns; for a single-column equivalent use {single}",
10306 )));
10307 }
10308 Ok(())
10309}
10310
10311/// Parse the fieldsets DSL: pipe-separated sections, optional
10312/// `"Title:"` prefix on each, comma-separated field names after.
10313/// Examples:
10314/// * `"name, office"` → one untitled section with two fields
10315/// * `"Identity: name, office | Metadata: created_at"` → two titled
10316/// sections
10317///
10318/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10319fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10320 raw.split('|')
10321 .map(str::trim)
10322 .filter(|s| !s.is_empty())
10323 .map(|section| {
10324 // Split off an optional `Title:` prefix (first colon).
10325 let (title, rest) = match section.split_once(':') {
10326 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10327 _ => (String::new(), section),
10328 };
10329 let fields = split_field_list(rest);
10330 (title, fields)
10331 })
10332 .collect()
10333}
10334
10335/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10336/// comma-separated entry maps a target field to one or more source
10337/// fields joined with `+`. Whitespace around tokens is trimmed.
10338/// Entries missing `:` or with empty target/source lists are dropped.
10339fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10340 raw.split(',')
10341 .map(str::trim)
10342 .filter(|s| !s.is_empty())
10343 .filter_map(|entry| {
10344 let (target, sources_raw) = entry.split_once(':')?;
10345 let target = target.trim().to_owned();
10346 if target.is_empty() {
10347 return None;
10348 }
10349 let sources: Vec<String> = sources_raw
10350 .split('+')
10351 .map(|s| s.trim().to_owned())
10352 .filter(|s| !s.is_empty())
10353 .collect();
10354 if sources.is_empty() {
10355 return None;
10356 }
10357 Some((target, sources))
10358 })
10359 .collect()
10360}
10361
10362/// Parse Django-shape `formfield_overrides` — `"field:widget,field2:widget2"`
10363/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10364/// and empty halves drop silently — the macro layer only enforces shape,
10365/// not field-name vs. widget-name validity (those checks happen at
10366/// `AdminConfig` consumption time). Issue #359.
10367fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10368 raw.split(',')
10369 .map(str::trim)
10370 .filter(|s| !s.is_empty())
10371 .filter_map(|entry| {
10372 let (field, widget) = entry.split_once(':')?;
10373 let field = field.trim().to_owned();
10374 let widget = widget.trim().to_owned();
10375 if field.is_empty() || widget.is_empty() {
10376 return None;
10377 }
10378 Some((field, widget))
10379 })
10380 .collect()
10381}
10382
10383/// Parse Django-shape ordering — `"name"` is ASC, `"-name"` is DESC.
10384/// Returns `(field_name, desc)` pairs in the same order as the input.
10385fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10386 raw.split(',')
10387 .map(str::trim)
10388 .filter(|s| !s.is_empty())
10389 .map(|spec| {
10390 spec.strip_prefix('-')
10391 .map_or((spec.to_owned(), false), |rest| {
10392 (rest.trim().to_owned(), true)
10393 })
10394 })
10395 .collect()
10396}
10397
10398struct FieldAttrs {
10399 column: Option<String>,
10400 primary_key: bool,
10401 fk: Option<String>,
10402 o2o: Option<String>,
10403 on: Option<String>,
10404 /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10405 /// "set_default" | "no_action")]` — Django-shape
10406 /// `ForeignKey(on_delete=…)`. Only meaningful when `fk` / `o2o` is
10407 /// also set; the macro errors at compile time if applied to a
10408 /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10409 /// DDL writer renders `ON DELETE <action>` after the constraint
10410 /// clause when this is `Some`; `None` falls back to the database
10411 /// default (NO ACTION on every backend rustango supports).
10412 on_delete: Option<String>,
10413 /// `#[rustango(related_name = "...")]` — Django-shape per-FK
10414 /// reverse-accessor override. When set, the derive emits
10415 /// `Parent::<related_name>[_pool]` instead of the container-level
10416 /// `default_related_name` or the `<child_snake>_set[_pool]`
10417 /// fallback. Only meaningful when `fk` / `o2o` is also set;
10418 /// silently ignored on non-FK fields. Follow-up to #816.
10419 related_name: Option<String>,
10420 max_length: Option<u32>,
10421 /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10422 /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10423 /// unconstrained `vector` column.
10424 vector_dims: Option<u32>,
10425 /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10426 /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10427 /// unconstrained `geometry(Point)` column (SRID 0).
10428 geometry_srid: Option<u32>,
10429 min: Option<i64>,
10430 max: Option<i64>,
10431 default: Option<String>,
10432 /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10433 /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10434 /// "gen_random_uuid()"`. The Rust field type must be
10435 /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10436 /// INSERTs so the DB DEFAULT fires.
10437 auto_uuid: bool,
10438 /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10439 /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10440 /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10441 /// the field is `Auto::Unset`, then bound as a normal parameter
10442 /// rather than relying on a per-dialect DB function. Issue #823
10443 /// (Eloquent `HasUuids`).
10444 ///
10445 /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10446 /// Composes with every backend (PG / MySQL / SQLite) — no
10447 /// `gen_random_uuid()` requirement on the database.
10448 default_uuid_v7: bool,
10449 /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10450 /// Server-set on insert, immutable from app code afterwards.
10451 /// Implies `auto + default = "now()"`. Field type must be
10452 /// `DateTime<Utc>`.
10453 auto_now_add: bool,
10454 /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10455 /// every insert AND every update. Implies `auto + default =
10456 /// "now()"`; the macro additionally rewrites `update_on` /
10457 /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10458 /// field value.
10459 auto_now: bool,
10460 /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10461 /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10462 /// `soft_delete_on(executor)` and `restore_on(executor)`
10463 /// methods on the model.
10464 soft_delete: bool,
10465 /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10466 /// the column in the generated DDL.
10467 unique: bool,
10468 /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10469 /// generates a `CREATE INDEX` for this column. `unique` here means
10470 /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10471 index: bool,
10472 index_unique: bool,
10473 index_name: Option<String>,
10474 /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10475 /// `"btree"`. Issue #34.
10476 index_method: String,
10477 /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10478 /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10479 /// the macro skips this column from every INSERT and UPDATE
10480 /// path, so the database always recomputes the value from
10481 /// `EXPR`. Backlog item #35.
10482 generated_as: Option<String>,
10483 /// `#[rustango(help_text = "…")]` — Django-shape help text
10484 /// rendered below the admin form's input. Threaded into
10485 /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10486 /// layers can read it.
10487 help_text: Option<String>,
10488 /// `#[rustango(choices = "value:Label, value:Label")]` — Django-shape
10489 /// enumerated allowed values. Threaded into `FieldSchema::choices`
10490 /// as a `&'static [(&'static str, &'static str)]` slice. When
10491 /// present, the admin form renders a `<select>` instead of `<input>`
10492 /// and the validator rejects values not in the list. Only meaningful
10493 /// for `FieldType::String`; the macro errors at compile time if
10494 /// applied to a non-string field.
10495 choices: Option<Vec<(String, String)>>,
10496 /// `#[rustango(db_comment = "…")]` — Django-shape DB-side column
10497 /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10498 /// the comment in CREATE TABLE; Postgres emits a separate
10499 /// `COMMENT ON COLUMN` statement after the table is created;
10500 /// SQLite silently drops the value (no native column comments).
10501 db_comment: Option<String>,
10502 /// `#[rustango(verbose_name = "…")]` — Django-shape human-readable
10503 /// label for the field. Threaded into `FieldSchema::verbose_name`
10504 /// so admin column headers, form labels, and other display
10505 /// surfaces can prefer the friendly caption over the Rust
10506 /// identifier. `None` means renderers fall back to the field name.
10507 verbose_name: Option<String>,
10508 /// `#[rustango(editable = false)]` — Django-shape opt-out from
10509 /// auto-generated form rendering. Defaults to `true` so existing
10510 /// fields keep their current admin / form behavior; setting
10511 /// `false` removes the field from the admin change-form entirely
10512 /// (the value is still visible on detail / list views, just not
10513 /// editable).
10514 editable: bool,
10515 /// `#[rustango(blank)]` / `#[rustango(blank = true)]` — Django-shape
10516 /// "form may submit empty even when DB is NOT NULL". Threaded into
10517 /// `FieldSchema::blank`. Defaults to `false`.
10518 blank: bool,
10519 /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10520 /// Django-shape `CITextField`. Threaded into
10521 /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10522 /// fields; the macro errors at derive time if applied elsewhere.
10523 case_insensitive: bool,
10524 /// `#[rustango(validators = "email,url")]` — Django-shape
10525 /// model-level validator chain. Comma-separated names that
10526 /// dispatch to the `validators::*` family in `validate_value`.
10527 /// Empty by default; fires on every typed INSERT/UPDATE.
10528 validators: Vec<String>,
10529}
10530
10531fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10532 let mut out = FieldAttrs {
10533 column: None,
10534 primary_key: false,
10535 fk: None,
10536 o2o: None,
10537 on: None,
10538 on_delete: None,
10539 related_name: None,
10540 max_length: None,
10541 vector_dims: None,
10542 geometry_srid: None,
10543 min: None,
10544 max: None,
10545 default: None,
10546 auto_uuid: false,
10547 default_uuid_v7: false,
10548 auto_now_add: false,
10549 auto_now: false,
10550 soft_delete: false,
10551 unique: false,
10552 index: false,
10553 index_unique: false,
10554 index_name: None,
10555 index_method: "btree".to_owned(),
10556 generated_as: None,
10557 help_text: None,
10558 choices: None,
10559 db_comment: None,
10560 verbose_name: None,
10561 editable: true,
10562 blank: false,
10563 case_insensitive: false,
10564 validators: Vec::new(),
10565 };
10566 for attr in &field.attrs {
10567 if !attr.path().is_ident("rustango") {
10568 continue;
10569 }
10570 attr.parse_nested_meta(|meta| {
10571 if meta.path.is_ident("column") {
10572 let s: LitStr = meta.value()?.parse()?;
10573 let name = s.value();
10574 validate_sql_identifier(&name, "column", s.span())?;
10575 out.column = Some(name);
10576 return Ok(());
10577 }
10578 if meta.path.is_ident("primary_key") {
10579 out.primary_key = true;
10580 return Ok(());
10581 }
10582 if meta.path.is_ident("fk") {
10583 let s: LitStr = meta.value()?.parse()?;
10584 out.fk = Some(s.value());
10585 return Ok(());
10586 }
10587 if meta.path.is_ident("o2o") {
10588 let s: LitStr = meta.value()?.parse()?;
10589 out.o2o = Some(s.value());
10590 return Ok(());
10591 }
10592 if meta.path.is_ident("on") {
10593 let s: LitStr = meta.value()?.parse()?;
10594 out.on = Some(s.value());
10595 return Ok(());
10596 }
10597 if meta.path.is_ident("on_delete") {
10598 let s: LitStr = meta.value()?.parse()?;
10599 let raw = s.value();
10600 let normalized = raw.trim().to_ascii_lowercase();
10601 // Validate at parse time so the user gets a clear span
10602 // rather than a downstream compile error in the emit.
10603 match normalized.as_str() {
10604 "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10605 _ => {
10606 return Err(syn::Error::new(
10607 s.span(),
10608 format!(
10609 "unknown on_delete action `{raw}`; expected one of \
10610 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10611 ),
10612 ));
10613 }
10614 }
10615 out.on_delete = Some(normalized);
10616 return Ok(());
10617 }
10618 if meta.path.is_ident("related_name") {
10619 let s: LitStr = meta.value()?.parse()?;
10620 let raw = s.value();
10621 if raw.trim().is_empty() {
10622 return Err(syn::Error::new(
10623 s.span(),
10624 "`related_name` must be a non-empty identifier",
10625 ));
10626 }
10627 // Validate as a Rust-ident shape — the value becomes
10628 // a method name on the parent type. Same rule as
10629 // `default_related_name` so the two surfaces match.
10630 if !raw
10631 .chars()
10632 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10633 || raw.starts_with(char::is_numeric)
10634 {
10635 return Err(syn::Error::new(
10636 s.span(),
10637 "`related_name` must be snake_case ASCII (lowercase letters, \
10638 digits, underscores; no leading digit)",
10639 ));
10640 }
10641 out.related_name = Some(raw);
10642 return Ok(());
10643 }
10644 if meta.path.is_ident("max_length") {
10645 let lit: syn::LitInt = meta.value()?.parse()?;
10646 out.max_length = Some(lit.base10_parse::<u32>()?);
10647 return Ok(());
10648 }
10649 // `#[rustango(vector(dims = N))]` — pgvector column
10650 // dimension (#824). Nested-meta form so it reads like the
10651 // other typed-column attrs.
10652 if meta.path.is_ident("vector") {
10653 meta.parse_nested_meta(|inner| {
10654 if inner.path.is_ident("dims") {
10655 let lit: syn::LitInt = inner.value()?.parse()?;
10656 out.vector_dims = Some(lit.base10_parse::<u32>()?);
10657 return Ok(());
10658 }
10659 Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10660 })?;
10661 return Ok(());
10662 }
10663 // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10664 // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10665 if meta.path.is_ident("geometry") {
10666 meta.parse_nested_meta(|inner| {
10667 if inner.path.is_ident("srid") {
10668 let lit: syn::LitInt = inner.value()?.parse()?;
10669 out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10670 return Ok(());
10671 }
10672 Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10673 })?;
10674 return Ok(());
10675 }
10676 if meta.path.is_ident("min") {
10677 out.min = Some(parse_signed_i64(&meta)?);
10678 return Ok(());
10679 }
10680 if meta.path.is_ident("max") {
10681 out.max = Some(parse_signed_i64(&meta)?);
10682 return Ok(());
10683 }
10684 if meta.path.is_ident("default") {
10685 let s: LitStr = meta.value()?.parse()?;
10686 out.default = Some(s.value());
10687 return Ok(());
10688 }
10689 if meta.path.is_ident("generated_as") {
10690 let s: LitStr = meta.value()?.parse()?;
10691 out.generated_as = Some(s.value());
10692 return Ok(());
10693 }
10694 if meta.path.is_ident("help_text") {
10695 let s: LitStr = meta.value()?.parse()?;
10696 out.help_text = Some(s.value());
10697 return Ok(());
10698 }
10699 if meta.path.is_ident("choices") {
10700 let s: LitStr = meta.value()?.parse()?;
10701 let raw = s.value();
10702 let mut pairs: Vec<(String, String)> = Vec::new();
10703 for chunk in raw.split(',') {
10704 let chunk = chunk.trim();
10705 if chunk.is_empty() {
10706 continue;
10707 }
10708 let (value, label) = match chunk.split_once(':') {
10709 Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10710 None => (chunk.to_owned(), chunk.to_owned()),
10711 };
10712 if value.is_empty() {
10713 return Err(syn::Error::new(
10714 s.span(),
10715 "`choices` entry has empty value before `:`",
10716 ));
10717 }
10718 pairs.push((value, label));
10719 }
10720 if pairs.is_empty() {
10721 return Err(syn::Error::new(
10722 s.span(),
10723 "`choices = \"…\"` must contain at least one value",
10724 ));
10725 }
10726 out.choices = Some(pairs);
10727 return Ok(());
10728 }
10729 if meta.path.is_ident("db_comment") {
10730 let s: LitStr = meta.value()?.parse()?;
10731 out.db_comment = Some(s.value());
10732 return Ok(());
10733 }
10734 if meta.path.is_ident("verbose_name") {
10735 let s: LitStr = meta.value()?.parse()?;
10736 out.verbose_name = Some(s.value());
10737 return Ok(());
10738 }
10739 if meta.path.is_ident("editable") {
10740 // Two forms accepted:
10741 // #[rustango(editable = false)] / true — explicit
10742 // #[rustango(editable)] — flag form (= true, the
10743 // default, so harmless; included for symmetry)
10744 if let Ok(v) = meta.value() {
10745 let lit: syn::LitBool = v.parse()?;
10746 out.editable = lit.value;
10747 } else {
10748 out.editable = true;
10749 }
10750 return Ok(());
10751 }
10752 if meta.path.is_ident("blank") {
10753 // Two forms accepted:
10754 // #[rustango(blank)] — flag form, true
10755 // #[rustango(blank = false)] / true — explicit
10756 if let Ok(v) = meta.value() {
10757 let lit: syn::LitBool = v.parse()?;
10758 out.blank = lit.value;
10759 } else {
10760 out.blank = true;
10761 }
10762 return Ok(());
10763 }
10764 if meta.path.is_ident("citext") {
10765 // Django-parity CITextField (#344). Two forms:
10766 // #[rustango(citext)] — flag form, true
10767 // #[rustango(citext = true)] — explicit
10768 // #[rustango(citext = false)] — explicit opt-out
10769 // String-only validation lives in the field-type
10770 // emitter (the FieldType discriminant is computed in
10771 // `detect_type`); the macro records the flag and the
10772 // DDL writer emits dialect-specific COLLATE / CITEXT.
10773 if let Ok(v) = meta.value() {
10774 let lit: syn::LitBool = v.parse()?;
10775 out.case_insensitive = lit.value;
10776 } else {
10777 out.case_insensitive = true;
10778 }
10779 return Ok(());
10780 }
10781 if meta.path.is_ident("validators") {
10782 let s: LitStr = meta.value()?.parse()?;
10783 let raw = s.value();
10784 out.validators = raw
10785 .split(',')
10786 .map(str::trim)
10787 .filter(|s| !s.is_empty())
10788 .map(str::to_owned)
10789 .collect();
10790 if out.validators.is_empty() {
10791 return Err(syn::Error::new(
10792 s.span(),
10793 "`validators = \"…\"` must list at least one name",
10794 ));
10795 }
10796 return Ok(());
10797 }
10798 if meta.path.is_ident("auto_uuid") {
10799 out.auto_uuid = true;
10800 // Implied: PK + auto + DEFAULT gen_random_uuid().
10801 // Each is also explicitly settable; the explicit
10802 // value wins if conflicting.
10803 out.primary_key = true;
10804 if out.default.is_none() {
10805 out.default = Some("gen_random_uuid()".into());
10806 }
10807 return Ok(());
10808 }
10809 if meta.path.is_ident("default_uuid_v7") {
10810 // Backend-neutral counterpart of `auto_uuid` — issue #823.
10811 // No SQL DEFAULT (the macro fills the value Rust-side
10812 // before binding); just mark the field as PK + Auto
10813 // so the insert path is the `Auto::Unset → generate`
10814 // branch.
10815 out.default_uuid_v7 = true;
10816 out.primary_key = true;
10817 return Ok(());
10818 }
10819 if meta.path.is_ident("auto_now_add") {
10820 out.auto_now_add = true;
10821 if out.default.is_none() {
10822 out.default = Some("now()".into());
10823 }
10824 return Ok(());
10825 }
10826 if meta.path.is_ident("auto_now") {
10827 out.auto_now = true;
10828 if out.default.is_none() {
10829 out.default = Some("now()".into());
10830 }
10831 return Ok(());
10832 }
10833 if meta.path.is_ident("soft_delete") {
10834 out.soft_delete = true;
10835 return Ok(());
10836 }
10837 if meta.path.is_ident("unique") {
10838 out.unique = true;
10839 return Ok(());
10840 }
10841 if meta.path.is_ident("index") {
10842 out.index = true;
10843 // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
10844 if meta.input.peek(syn::token::Paren) {
10845 meta.parse_nested_meta(|inner| {
10846 if inner.path.is_ident("unique") {
10847 out.index_unique = true;
10848 return Ok(());
10849 }
10850 if inner.path.is_ident("name") {
10851 let s: LitStr = inner.value()?.parse()?;
10852 out.index_name = Some(s.value());
10853 return Ok(());
10854 }
10855 if inner.path.is_ident("method") {
10856 let s: LitStr = inner.value()?.parse()?;
10857 let v = s.value();
10858 match v.as_str() {
10859 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
10860 out.index_method = v;
10861 }
10862 other => {
10863 return Err(inner.error(format!(
10864 "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
10865 )));
10866 }
10867 }
10868 return Ok(());
10869 }
10870 Err(inner.error(
10871 "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
10872 ))
10873 })?;
10874 }
10875 return Ok(());
10876 }
10877 Err(meta.error("unknown rustango field attribute"))
10878 })?;
10879 }
10880 Ok(out)
10881}
10882
10883/// Parse a signed integer literal, accepting optional leading `-`.
10884fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
10885 let expr: syn::Expr = meta.value()?.parse()?;
10886 match expr {
10887 syn::Expr::Lit(syn::ExprLit {
10888 lit: syn::Lit::Int(lit),
10889 ..
10890 }) => lit.base10_parse::<i64>(),
10891 syn::Expr::Unary(syn::ExprUnary {
10892 op: syn::UnOp::Neg(_),
10893 expr,
10894 ..
10895 }) => {
10896 if let syn::Expr::Lit(syn::ExprLit {
10897 lit: syn::Lit::Int(lit),
10898 ..
10899 }) = *expr
10900 {
10901 let v: i64 = lit.base10_parse()?;
10902 Ok(-v)
10903 } else {
10904 Err(syn::Error::new_spanned(expr, "expected integer literal"))
10905 }
10906 }
10907 other => Err(syn::Error::new_spanned(
10908 other,
10909 "expected integer literal (signed)",
10910 )),
10911 }
10912}
10913
10914struct FieldInfo<'a> {
10915 ident: &'a syn::Ident,
10916 column: String,
10917 primary_key: bool,
10918 /// `true` when the Rust type was `Auto<T>` — the INSERT path will
10919 /// skip this column when `Auto::Unset` and emit it under
10920 /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
10921 auto: bool,
10922 /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
10923 /// the `Column::Value` associated type for typed-column tokens.
10924 value_ty: &'a Type,
10925 /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
10926 field_type_tokens: TokenStream2,
10927 schema: TokenStream2,
10928 from_row_init: TokenStream2,
10929 /// Variant of [`Self::from_row_init`] that reads the column via
10930 /// `format!("{prefix}__{col}")` so a model can be decoded out of
10931 /// the aliased columns of a JOINed row. Drives slice 9.0d's
10932 /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
10933 /// helper that `select_related` calls when stitching loaded FKs.
10934 from_aliased_row_init: TokenStream2,
10935 /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
10936 /// relation helper emit (`Author::<child>_set`) needs to know `T`
10937 /// to point the generated method at the right child model.
10938 fk_inner: Option<Type>,
10939 /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
10940 /// `kind` (since ForeignKey detection sets `kind` to K's
10941 /// underlying type) but stored separately for clarity at the
10942 /// `FkRelation` construction site, which only sees the FK's
10943 /// surface fields.
10944 fk_pk_kind: DetectedKind,
10945 /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
10946 /// the bare `ForeignKey<T, K>`. Routes the load_related and
10947 /// fk_pk_access emitters to wrap assignments / accessors in
10948 /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
10949 /// FK column compiles end-to-end. The DDL writer reads this off
10950 /// the field schema (`nullable` flag); the macro just needs to
10951 /// keep the Rust-side codegen consistent.
10952 nullable: bool,
10953 /// `true` when this column was marked `#[rustango(auto_now)]` —
10954 /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
10955 /// column instead of the user-supplied value, so `updated_at`
10956 /// always reflects the latest write without the caller having
10957 /// to remember to set it.
10958 auto_now: bool,
10959 /// `true` when this column was marked `#[rustango(auto_now_add)]`
10960 /// — the column is server-set on INSERT (DB DEFAULT) and
10961 /// **immutable** afterwards. `update_on` / `save_on` skip the
10962 /// column entirely so a stale `created_at` value in memory never
10963 /// rewrites the persisted timestamp.
10964 auto_now_add: bool,
10965 /// `true` when this column was marked `#[rustango(soft_delete)]`.
10966 /// Triggers emission of `soft_delete_on(executor)` and
10967 /// `restore_on(executor)` on the model's inherent impl. There is
10968 /// at most one such column per model — emission asserts this.
10969 soft_delete: bool,
10970 /// `Some` when this column was marked
10971 /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
10972 /// every INSERT and UPDATE path; the database recomputes the
10973 /// value from `EXPR`. Backlog item #35.
10974 generated_as: Option<String>,
10975 /// `true` when this column was marked
10976 /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
10977 /// emit an `insert_push` that auto-fills an `Auto::Unset` value
10978 /// with `Uuid::now_v7()` before binding, so the PK is generated
10979 /// Rust-side and the column is always present in the INSERT
10980 /// statement (no DB DEFAULT requirement). Issue #823.
10981 default_uuid_v7: bool,
10982 /// `Some` when this FK field carried `#[rustango(related_name =
10983 /// "...")]`. Threaded into [`FkRelation::related_name`] and
10984 /// consumed by [`reverse_helper_tokens`] to override the default
10985 /// `<child_snake>_set` accessor name. Follow-up to #816.
10986 related_name: Option<String>,
10987}
10988
10989/// Reject table names that won't survive SQL identifier
10990/// derivation downstream. Postgres' regular-identifier rule
10991/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
10992/// through the framework's unquoted FK / index / constraint name
10993/// emission without surprises. We also disallow leading-digit and
10994/// the empty string for clarity.
10995///
10996/// Reserved-word collisions (`select`, `from`, …) aren't flagged
10997/// here — those produce a runtime error from the SQL parser,
10998/// which is loud enough; statically enumerating reserved words
10999/// across the three supported dialects is more friction than help.
11000///
11001/// Backlog item #65.
11002fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11003 validate_sql_identifier(name, "table", span)
11004}
11005
11006/// Reject SQL identifiers that compile but break downstream SQL
11007/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11008/// `kind` is "table" / "column" — used for the error message so users
11009/// see which attribute caused the failure.
11010fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11011 if name.is_empty() {
11012 return Err(syn::Error::new(
11013 span,
11014 format!("`{kind} = \"\"` is not a valid SQL identifier"),
11015 ));
11016 }
11017 let mut chars = name.chars();
11018 let first = chars.next().unwrap();
11019 if !(first.is_ascii_alphabetic() || first == '_') {
11020 return Err(syn::Error::new(
11021 span,
11022 format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11023 ));
11024 }
11025 for c in chars {
11026 if !(c.is_ascii_alphanumeric() || c == '_') {
11027 return Err(syn::Error::new(
11028 span,
11029 format!(
11030 "{kind} name `{name}` contains invalid character {c:?} — \
11031 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11032 Hyphens in particular break FK / index name derivation \
11033 downstream; use underscores instead (e.g. `{}`)",
11034 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11035 ),
11036 ));
11037 }
11038 }
11039 Ok(())
11040}
11041
11042fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11043 let root = rustango_root();
11044 let attrs = parse_field_attrs(field)?;
11045 let ident = field
11046 .ident
11047 .as_ref()
11048 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11049 let name = ident.to_string();
11050 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11051 let primary_key = attrs.primary_key;
11052 let DetectedType {
11053 kind,
11054 nullable,
11055 auto: detected_auto,
11056 fk_inner,
11057 } = detect_type(&field.ty)?;
11058 check_bound_compatibility(field, &attrs, kind)?;
11059 let auto = detected_auto;
11060 // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11061 // INSERT path: the user must wrap the field in `Auto<T>`, which
11062 // marks the column as DB-default-supplied. The mixin attrs then
11063 // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11064 // for `auto_now`, force the value on UPDATE too.
11065 if attrs.auto_uuid {
11066 if kind != DetectedKind::Uuid {
11067 return Err(syn::Error::new_spanned(
11068 field,
11069 "`#[rustango(auto_uuid)]` requires the field type to be \
11070 `Auto<uuid::Uuid>`",
11071 ));
11072 }
11073 if !detected_auto {
11074 return Err(syn::Error::new_spanned(
11075 field,
11076 "`#[rustango(auto_uuid)]` requires the field type to be \
11077 wrapped in `Auto<...>` so the macro skips the column on \
11078 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11079 ));
11080 }
11081 }
11082 if attrs.default_uuid_v7 {
11083 if kind != DetectedKind::Uuid {
11084 return Err(syn::Error::new_spanned(
11085 field,
11086 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11087 `Auto<uuid::Uuid>`",
11088 ));
11089 }
11090 if !detected_auto {
11091 return Err(syn::Error::new_spanned(
11092 field,
11093 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11094 wrapped in `Auto<...>` so the macro can detect the \
11095 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11096 ));
11097 }
11098 if attrs.auto_uuid {
11099 return Err(syn::Error::new_spanned(
11100 field,
11101 "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11102 `#[rustango(auto_uuid)]` — the former generates the UUID \
11103 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11104 Pick one.",
11105 ));
11106 }
11107 }
11108 if attrs.auto_now_add || attrs.auto_now {
11109 if kind != DetectedKind::DateTime {
11110 return Err(syn::Error::new_spanned(
11111 field,
11112 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11113 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11114 ));
11115 }
11116 if !detected_auto {
11117 return Err(syn::Error::new_spanned(
11118 field,
11119 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11120 the field type to be wrapped in `Auto<...>` so the macro skips \
11121 the column on INSERT and the DB DEFAULT (`now()`) fires",
11122 ));
11123 }
11124 }
11125 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11126 return Err(syn::Error::new_spanned(
11127 field,
11128 "`#[rustango(soft_delete)]` requires the field type to be \
11129 `Option<chrono::DateTime<chrono::Utc>>`",
11130 ));
11131 }
11132 let is_mixin_auto =
11133 attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11134 if detected_auto && !primary_key && !is_mixin_auto {
11135 return Err(syn::Error::new_spanned(
11136 field,
11137 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11138 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11139 `auto_now`",
11140 ));
11141 }
11142 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11143 return Err(syn::Error::new_spanned(
11144 field,
11145 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11146 SERIAL / BIGSERIAL already supplies a default sequence.",
11147 ));
11148 }
11149 if fk_inner.is_some() && primary_key {
11150 return Err(syn::Error::new_spanned(
11151 field,
11152 "`ForeignKey<T>` is not allowed on a primary-key field — \
11153 a row's PK is its own identity, not a reference to a parent.",
11154 ));
11155 }
11156 if attrs.generated_as.is_some() {
11157 if primary_key {
11158 return Err(syn::Error::new_spanned(
11159 field,
11160 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11161 primary-key field — a PK must be writable so the row \
11162 has an identity at INSERT time.",
11163 ));
11164 }
11165 if attrs.default.is_some() {
11166 return Err(syn::Error::new_spanned(
11167 field,
11168 "`#[rustango(generated_as = \"…\")]` cannot combine with \
11169 `default = \"…\"` — Postgres rejects DEFAULT on \
11170 generated columns. The expression IS the default.",
11171 ));
11172 }
11173 if detected_auto {
11174 return Err(syn::Error::new_spanned(
11175 field,
11176 "`#[rustango(generated_as = \"…\")]` is not allowed on \
11177 an `Auto<T>` field — generated columns are computed \
11178 by the DB, not server-assigned via a sequence. Use a \
11179 plain Rust type (e.g. `f64`).",
11180 ));
11181 }
11182 if fk_inner.is_some() {
11183 return Err(syn::Error::new_spanned(
11184 field,
11185 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11186 ForeignKey field.",
11187 ));
11188 }
11189 }
11190 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11191 let column_lit = column.as_str();
11192 // pgvector (#824): the `vector(dims = N)` attribute supplies the
11193 // dimension that the bare `Vector` Rust type can't carry, so emit
11194 // `FieldType::Vector(N)` here rather than the `variant_tokens`
11195 // fallback of `Vector(0)`.
11196 let field_type_tokens = if kind == DetectedKind::Vector {
11197 let root = rustango_root();
11198 let dims = attrs.vector_dims.unwrap_or(0);
11199 quote!(#root::core::FieldType::Vector(#dims))
11200 } else if kind == DetectedKind::Geometry {
11201 // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11202 // SRID that the bare `Point` Rust type can't carry, so emit
11203 // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11204 let root = rustango_root();
11205 let srid = attrs.geometry_srid.unwrap_or(0);
11206 quote!(#root::core::FieldType::Geometry(#srid))
11207 } else {
11208 kind.variant_tokens()
11209 };
11210 let max_length = optional_u32(attrs.max_length);
11211 let min = optional_i64(attrs.min);
11212 let max = optional_i64(attrs.max);
11213 let default = optional_str(attrs.default.as_deref());
11214
11215 let unique = attrs.unique;
11216 let generated_as = optional_str(attrs.generated_as.as_deref());
11217 let help_text = optional_str(attrs.help_text.as_deref());
11218 let choices = optional_choices(attrs.choices.as_deref());
11219 let db_comment = optional_str(attrs.db_comment.as_deref());
11220 let verbose_name = optional_str(attrs.verbose_name.as_deref());
11221 let editable = attrs.editable;
11222 let blank = attrs.blank;
11223 let case_insensitive = attrs.case_insensitive;
11224 let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11225 if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() {
11226 return Err(syn::Error::new_spanned(
11227 field,
11228 "`#[rustango(on_delete = \"…\")]` requires either `fk = \"<table>\"` \
11229 or `o2o = \"<table>\"` on the same field — it has no meaning on a \
11230 non-FK column.",
11231 ));
11232 }
11233 let fk_on_delete = match attrs.on_delete.as_deref() {
11234 None => quote!(::core::option::Option::None),
11235 Some(action) => {
11236 let variant = match action {
11237 "cascade" => quote!(Cascade),
11238 "restrict" => quote!(Restrict),
11239 "set_null" => quote!(SetNull),
11240 "set_default" => quote!(SetDefault),
11241 "no_action" => quote!(NoAction),
11242 // parse_field_attrs already validated this — guard against future drift.
11243 other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11244 };
11245 quote!(::core::option::Option::Some(
11246 #root::core::OnDeleteAction::#variant
11247 ))
11248 }
11249 };
11250 let schema = quote! {
11251 #root::core::FieldSchema {
11252 name: #name,
11253 column: #column_lit,
11254 ty: #field_type_tokens,
11255 nullable: #nullable,
11256 primary_key: #primary_key,
11257 relation: #relation,
11258 max_length: #max_length,
11259 min: #min,
11260 max: #max,
11261 default: #default,
11262 auto: #auto,
11263 unique: #unique,
11264 generated_as: #generated_as,
11265 help_text: #help_text,
11266 choices: #choices,
11267 db_comment: #db_comment,
11268 verbose_name: #verbose_name,
11269 editable: #editable,
11270 blank: #blank,
11271 case_insensitive: #case_insensitive,
11272 fk_on_delete: #fk_on_delete,
11273 validators: &[ #(#validators_lits),* ],
11274 }
11275 };
11276
11277 let from_row_init = quote! {
11278 #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11279 };
11280 let from_aliased_row_init = quote! {
11281 #ident: #root::sql::sqlx::Row::try_get(
11282 row,
11283 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11284 )?
11285 };
11286
11287 Ok(FieldInfo {
11288 ident,
11289 column,
11290 primary_key,
11291 auto,
11292 value_ty: &field.ty,
11293 field_type_tokens,
11294 schema,
11295 from_row_init,
11296 from_aliased_row_init,
11297 fk_inner: fk_inner.cloned(),
11298 fk_pk_kind: kind,
11299 nullable,
11300 auto_now: attrs.auto_now,
11301 auto_now_add: attrs.auto_now_add,
11302 soft_delete: attrs.soft_delete,
11303 generated_as: attrs.generated_as.clone(),
11304 default_uuid_v7: attrs.default_uuid_v7,
11305 related_name: attrs.related_name.clone(),
11306 })
11307}
11308
11309fn check_bound_compatibility(
11310 field: &syn::Field,
11311 attrs: &FieldAttrs,
11312 kind: DetectedKind,
11313) -> syn::Result<()> {
11314 if attrs.max_length.is_some() && kind != DetectedKind::String {
11315 return Err(syn::Error::new_spanned(
11316 field,
11317 "`max_length` is only valid on `String` fields (or `Option<String>`)",
11318 ));
11319 }
11320 if attrs.choices.is_some() && kind != DetectedKind::String {
11321 return Err(syn::Error::new_spanned(
11322 field,
11323 "`choices` is only valid on `String` fields (or `Option<String>`) — \
11324 integer-valued enumerations should be modeled with a Rust enum and \
11325 custom (de)serializer for now",
11326 ));
11327 }
11328 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11329 return Err(syn::Error::new_spanned(
11330 field,
11331 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11332 ));
11333 }
11334 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11335 if min > max {
11336 return Err(syn::Error::new_spanned(
11337 field,
11338 format!("`min` ({min}) is greater than `max` ({max})"),
11339 ));
11340 }
11341 }
11342 Ok(())
11343}
11344
11345fn optional_u32(value: Option<u32>) -> TokenStream2 {
11346 if let Some(v) = value {
11347 quote!(::core::option::Option::Some(#v))
11348 } else {
11349 quote!(::core::option::Option::None)
11350 }
11351}
11352
11353fn optional_i64(value: Option<i64>) -> TokenStream2 {
11354 if let Some(v) = value {
11355 quote!(::core::option::Option::Some(#v))
11356 } else {
11357 quote!(::core::option::Option::None)
11358 }
11359}
11360
11361fn optional_str(value: Option<&str>) -> TokenStream2 {
11362 if let Some(v) = value {
11363 quote!(::core::option::Option::Some(#v))
11364 } else {
11365 quote!(::core::option::Option::None)
11366 }
11367}
11368
11369fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11370 let Some(pairs) = pairs else {
11371 return quote!(::core::option::Option::None);
11372 };
11373 let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11374 quote!(::core::option::Option::Some(&[#(#entries),*]))
11375}
11376
11377fn relation_tokens(
11378 field: &syn::Field,
11379 attrs: &FieldAttrs,
11380 fk_inner: Option<&syn::Type>,
11381 table: &str,
11382) -> syn::Result<TokenStream2> {
11383 let root = rustango_root();
11384 if let Some(inner) = fk_inner {
11385 if attrs.fk.is_some() || attrs.o2o.is_some() {
11386 return Err(syn::Error::new_spanned(
11387 field,
11388 "`ForeignKey<T>` already declares the FK target via the type parameter — \
11389 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11390 ));
11391 }
11392 let on = attrs.on.as_deref().unwrap_or("id");
11393 return Ok(quote! {
11394 ::core::option::Option::Some(#root::core::Relation::Fk {
11395 to: <#inner as #root::core::Model>::SCHEMA.table,
11396 on: #on,
11397 })
11398 });
11399 }
11400 match (&attrs.fk, &attrs.o2o) {
11401 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11402 field,
11403 "`fk` and `o2o` are mutually exclusive",
11404 )),
11405 (Some(to), None) => {
11406 let on = attrs.on.as_deref().unwrap_or("id");
11407 // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11408 // the model's own table. Threaded as a literal string at
11409 // macro-expansion time to sidestep the const-eval cycle
11410 // that `Self::SCHEMA.table` would create when referenced
11411 // inside Self::SCHEMA's own initializer.
11412 let resolved = if to == "self" { table } else { to };
11413 Ok(quote! {
11414 ::core::option::Option::Some(#root::core::Relation::Fk { to: #resolved, on: #on })
11415 })
11416 }
11417 (None, Some(to)) => {
11418 let on = attrs.on.as_deref().unwrap_or("id");
11419 let resolved = if to == "self" { table } else { to };
11420 Ok(quote! {
11421 ::core::option::Option::Some(#root::core::Relation::O2O { to: #resolved, on: #on })
11422 })
11423 }
11424 (None, None) => {
11425 if attrs.on.is_some() {
11426 return Err(syn::Error::new_spanned(
11427 field,
11428 "`on` requires `fk` or `o2o`",
11429 ));
11430 }
11431 Ok(quote!(::core::option::Option::None))
11432 }
11433 }
11434}
11435
11436/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11437/// about kinds without depending on `rustango-core` (which would require a
11438/// proc-macro/normal split it doesn't have today).
11439#[derive(Clone, Copy, PartialEq, Eq)]
11440enum DetectedKind {
11441 I16,
11442 I32,
11443 I64,
11444 F32,
11445 F64,
11446 Bool,
11447 String,
11448 DateTime,
11449 Date,
11450 Time,
11451 Uuid,
11452 Json,
11453 Decimal,
11454 Binary,
11455 /// `Array<String>` → PG `text[]` (#341).
11456 ArrayText,
11457 /// `Array<i32>` → PG `integer[]` (#341).
11458 ArrayInt,
11459 /// `Array<i64>` → PG `bigint[]` (#341).
11460 ArrayBigInt,
11461 /// `Range<i32>` → PG `int4range` (#343).
11462 RangeInt,
11463 /// `Range<i64>` → PG `int8range` (#343).
11464 RangeBigInt,
11465 /// `Range<Decimal>` → PG `numrange` (#343).
11466 RangeNumeric,
11467 /// `Range<NaiveDate>` → PG `daterange` (#343).
11468 RangeDate,
11469 /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11470 RangeDateTime,
11471 /// `HStore` → PG `hstore` (#342).
11472 HStore,
11473 /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11474 /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11475 /// in at the `FieldType` emission site (not carried on this enum).
11476 Vector,
11477 /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11478 /// from the `#[rustango(geometry(srid = N))]` field attribute,
11479 /// threaded in at the `FieldType` emission site.
11480 Geometry,
11481}
11482
11483impl DetectedKind {
11484 fn variant_tokens(self) -> TokenStream2 {
11485 let root = rustango_root();
11486 match self {
11487 Self::I16 => quote!(#root::core::FieldType::I16),
11488 Self::I32 => quote!(#root::core::FieldType::I32),
11489 Self::I64 => quote!(#root::core::FieldType::I64),
11490 Self::F32 => quote!(#root::core::FieldType::F32),
11491 Self::F64 => quote!(#root::core::FieldType::F64),
11492 Self::Bool => quote!(#root::core::FieldType::Bool),
11493 Self::String => quote!(#root::core::FieldType::String),
11494 Self::DateTime => quote!(#root::core::FieldType::DateTime),
11495 Self::Date => quote!(#root::core::FieldType::Date),
11496 Self::Time => quote!(#root::core::FieldType::Time),
11497 Self::Uuid => quote!(#root::core::FieldType::Uuid),
11498 Self::Json => quote!(#root::core::FieldType::Json),
11499 Self::Decimal => quote!(#root::core::FieldType::Decimal),
11500 Self::Binary => quote!(#root::core::FieldType::Binary),
11501 Self::ArrayText => {
11502 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11503 }
11504 Self::ArrayInt => {
11505 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11506 }
11507 Self::ArrayBigInt => {
11508 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11509 }
11510 Self::RangeInt => {
11511 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11512 }
11513 Self::RangeBigInt => {
11514 quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11515 }
11516 Self::RangeNumeric => {
11517 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11518 }
11519 Self::RangeDate => {
11520 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11521 }
11522 Self::RangeDateTime => {
11523 quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11524 }
11525 Self::HStore => quote!(#root::core::FieldType::HStore),
11526 // Dimension comes from the `vector(dims = N)` attribute,
11527 // applied at the emission site; `0` here is just a fallback.
11528 Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11529 // SRID comes from the `geometry(srid = N)` attribute, applied
11530 // at the emission site; `0` here is just a fallback.
11531 Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11532 }
11533 }
11534
11535 fn is_integer(self) -> bool {
11536 matches!(self, Self::I16 | Self::I32 | Self::I64)
11537 }
11538
11539 /// `(SqlValue::<Variant>, default expr)` for emitting the
11540 /// `match SqlValue { … }` arm in `LoadRelated::__rustango_load_related`
11541 /// for a `ForeignKey<T, K>` FK whose K maps to `self`. The default
11542 /// fires only when the parent's `__rustango_pk_value` returns a
11543 /// different variant than expected, which is a compile-time bug —
11544 /// but we still need a value-typed fallback to keep the match
11545 /// total.
11546 fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
11547 let root = rustango_root();
11548 match self {
11549 Self::I16 => (quote!(I16), quote!(0i16)),
11550 Self::I32 => (quote!(I32), quote!(0i32)),
11551 Self::I64 => (quote!(I64), quote!(0i64)),
11552 Self::F32 => (quote!(F32), quote!(0f32)),
11553 Self::F64 => (quote!(F64), quote!(0f64)),
11554 Self::Bool => (quote!(Bool), quote!(false)),
11555 Self::String => (quote!(String), quote!(::std::string::String::new())),
11556 Self::DateTime => (
11557 quote!(DateTime),
11558 quote!(<#root::__chrono::DateTime<#root::__chrono::Utc> as ::std::default::Default>::default()),
11559 ),
11560 Self::Date => (
11561 quote!(Date),
11562 quote!(<#root::__chrono::NaiveDate as ::std::default::Default>::default()),
11563 ),
11564 Self::Time => (
11565 quote!(Time),
11566 quote!(<#root::__chrono::NaiveTime as ::std::default::Default>::default()),
11567 ),
11568 Self::Uuid => (quote!(Uuid), quote!(#root::__uuid::Uuid::nil())),
11569 Self::Json => (quote!(Json), quote!(#root::__serde_json::Value::Null)),
11570 Self::Decimal => (
11571 quote!(Decimal),
11572 quote!(<#root::__rust_decimal::Decimal as ::std::default::Default>::default()),
11573 ),
11574 Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
11575 // Arrays (#341) can never be a foreign-key primary key, so
11576 // this arm is never reached at runtime — but the match must
11577 // stay total. A bare empty `SqlValue::Array` is the dummy.
11578 Self::ArrayText | Self::ArrayInt | Self::ArrayBigInt => {
11579 (quote!(Array), quote!(::std::vec::Vec::new()))
11580 }
11581 // Ranges (#343) likewise can't be a FK PK — never reached.
11582 Self::RangeInt
11583 | Self::RangeBigInt
11584 | Self::RangeNumeric
11585 | Self::RangeDate
11586 | Self::RangeDateTime => (quote!(RangeLiteral), quote!(::std::string::String::new())),
11587 // HStore (#342) can't be a FK PK — never reached.
11588 Self::HStore => (quote!(HStore), quote!(::std::vec::Vec::new())),
11589 // Vector (#824) can't be a FK PK — never reached.
11590 Self::Vector => (quote!(Vector), quote!(::std::vec::Vec::new())),
11591 // Geometry (#443) can't be a FK PK — never reached (arm is
11592 // exhaustiveness-only; never interpolated into emitted code).
11593 Self::Geometry => (quote!(Geometry), quote!(::std::vec::Vec::new())),
11594 }
11595 }
11596}
11597
11598/// Result of walking a field's Rust type. `kind` is the underlying
11599/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11600/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11601/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11602/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11603#[derive(Clone, Copy)]
11604struct DetectedType<'a> {
11605 kind: DetectedKind,
11606 nullable: bool,
11607 auto: bool,
11608 fk_inner: Option<&'a syn::Type>,
11609}
11610
11611/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11612/// non-`Auto` types — the caller should already have routed Auto-only
11613/// codegen through this helper, so a `None` indicates a macro-internal
11614/// invariant break.
11615fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11616 let Type::Path(TypePath { path, qself: None }) = ty else {
11617 return None;
11618 };
11619 let last = path.segments.last()?;
11620 if last.ident != "Auto" {
11621 return None;
11622 }
11623 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11624 return None;
11625 };
11626 args.args.iter().find_map(|a| match a {
11627 syn::GenericArgument::Type(t) => Some(t),
11628 _ => None,
11629 })
11630}
11631
11632fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11633 let Type::Path(TypePath { path, qself: None }) = ty else {
11634 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11635 };
11636 let last = path
11637 .segments
11638 .last()
11639 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11640
11641 if last.ident == "Option" {
11642 let inner = generic_inner(ty, &last.arguments, "Option")?;
11643 let inner_det = detect_type(inner)?;
11644 if inner_det.nullable {
11645 return Err(syn::Error::new_spanned(
11646 ty,
11647 "nested Option is not supported",
11648 ));
11649 }
11650 if inner_det.auto {
11651 return Err(syn::Error::new_spanned(
11652 ty,
11653 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11654 ));
11655 }
11656 return Ok(DetectedType {
11657 nullable: true,
11658 ..inner_det
11659 });
11660 }
11661
11662 if last.ident == "Auto" {
11663 let inner = generic_inner(ty, &last.arguments, "Auto")?;
11664 let inner_det = detect_type(inner)?;
11665 if inner_det.auto {
11666 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11667 }
11668 if inner_det.nullable {
11669 return Err(syn::Error::new_spanned(
11670 ty,
11671 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11672 ));
11673 }
11674 if inner_det.fk_inner.is_some() {
11675 return Err(syn::Error::new_spanned(
11676 ty,
11677 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11678 ));
11679 }
11680 if !matches!(
11681 inner_det.kind,
11682 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11683 ) {
11684 return Err(syn::Error::new_spanned(
11685 ty,
11686 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11687 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11688 (DEFAULT now())",
11689 ));
11690 }
11691 return Ok(DetectedType {
11692 auto: true,
11693 ..inner_det
11694 });
11695 }
11696
11697 if last.ident == "ForeignKey" {
11698 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11699 // Resolve the FK column's underlying SQL type from `K`. When the
11700 // user wrote `ForeignKey<T>` without a key parameter, the type
11701 // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11702 // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11703 // recurse into K so the column DDL emits the right SQL type
11704 // (VARCHAR for String, UUID for Uuid, …) and the load_related
11705 // emitter knows which `SqlValue` variant to match.
11706 let kind = match key_ty {
11707 Some(k) => detect_type(k)?.kind,
11708 None => DetectedKind::I64,
11709 };
11710 return Ok(DetectedType {
11711 kind,
11712 nullable: false,
11713 auto: false,
11714 fk_inner: Some(inner),
11715 });
11716 }
11717
11718 let kind = match last.ident.to_string().as_str() {
11719 "i16" => DetectedKind::I16,
11720 "i32" => DetectedKind::I32,
11721 "i64" => DetectedKind::I64,
11722 "f32" => DetectedKind::F32,
11723 "f64" => DetectedKind::F64,
11724 "bool" => DetectedKind::Bool,
11725 "String" => DetectedKind::String,
11726 "DateTime" => DetectedKind::DateTime,
11727 "NaiveDate" => DetectedKind::Date,
11728 "NaiveTime" => DetectedKind::Time,
11729 "Uuid" => DetectedKind::Uuid,
11730 "Value" => DetectedKind::Json,
11731 "Decimal" => DetectedKind::Decimal,
11732 // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11733 // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11734 // — that would emit Binary DDL and decode-fail at runtime.
11735 "Vec" => {
11736 let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11737 if let Type::Path(TypePath { path, qself: None }) = inner {
11738 if let Some(seg) = path.segments.last() {
11739 if seg.ident == "u8" && seg.arguments.is_empty() {
11740 return Ok(DetectedType {
11741 kind: DetectedKind::Binary,
11742 nullable: false,
11743 auto: false,
11744 fk_inner: None,
11745 });
11746 }
11747 }
11748 }
11749 return Err(syn::Error::new_spanned(
11750 ty,
11751 "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11752 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11753 ));
11754 }
11755 // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11756 // `integer[]` / `bigint[]` (Django `ArrayField`, #341).
11757 "Array" => {
11758 let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11759 let elem = match inner {
11760 Type::Path(TypePath { path, qself: None }) => {
11761 path.segments.last().map(|s| s.ident.to_string())
11762 }
11763 _ => None,
11764 };
11765 let kind = match elem.as_deref() {
11766 Some("String") => DetectedKind::ArrayText,
11767 Some("i32") => DetectedKind::ArrayInt,
11768 Some("i64") => DetectedKind::ArrayBigInt,
11769 _ => {
11770 return Err(syn::Error::new_spanned(
11771 ty,
11772 "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
11773 `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
11774 ));
11775 }
11776 };
11777 return Ok(DetectedType {
11778 kind,
11779 nullable: false,
11780 auto: false,
11781 fk_inner: None,
11782 });
11783 }
11784 // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
11785 // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
11786 // `int8range` / `numrange` / `daterange` / `tstzrange` (Django
11787 // `RangeField` family, #343).
11788 "Range" => {
11789 let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
11790 let elem = match inner {
11791 Type::Path(TypePath { path, qself: None }) => {
11792 path.segments.last().map(|s| s.ident.to_string())
11793 }
11794 _ => None,
11795 };
11796 let kind = match elem.as_deref() {
11797 Some("i32") => DetectedKind::RangeInt,
11798 Some("i64") => DetectedKind::RangeBigInt,
11799 Some("Decimal") => DetectedKind::RangeNumeric,
11800 Some("NaiveDate") => DetectedKind::RangeDate,
11801 Some("DateTime") => DetectedKind::RangeDateTime,
11802 _ => {
11803 return Err(syn::Error::new_spanned(
11804 ty,
11805 "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
11806 `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
11807 `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
11808 (→ tstzrange) are supported (#343)",
11809 ));
11810 }
11811 };
11812 return Ok(DetectedType {
11813 kind,
11814 nullable: false,
11815 auto: false,
11816 fk_inner: None,
11817 });
11818 }
11819 // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
11820 // (the `CastValue` impl bridges logical↔stored); the field's own
11821 // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
11822 // schema just needs `FieldType::String`.
11823 "Cast" => {
11824 return Ok(DetectedType {
11825 kind: DetectedKind::String,
11826 nullable: false,
11827 auto: false,
11828 fk_inner: None,
11829 });
11830 }
11831 // `HStore` → PG `hstore` (Django `HStoreField`, #342). No generic
11832 // parameter — always a string→string map.
11833 "HStore" => {
11834 return Ok(DetectedType {
11835 kind: DetectedKind::HStore,
11836 nullable: false,
11837 auto: false,
11838 fk_inner: None,
11839 });
11840 }
11841 // `Vector` → pgvector `vector(N)` (#824). The dimension is
11842 // supplied by `#[rustango(vector(dims = N))]`, not the type.
11843 "Vector" => {
11844 return Ok(DetectedType {
11845 kind: DetectedKind::Vector,
11846 nullable: false,
11847 auto: false,
11848 fk_inner: None,
11849 });
11850 }
11851 // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
11852 // supplied by `#[rustango(geometry(srid = N))]`, not the type.
11853 "Point" => {
11854 return Ok(DetectedType {
11855 kind: DetectedKind::Geometry,
11856 nullable: false,
11857 auto: false,
11858 fk_inner: None,
11859 });
11860 }
11861 other => {
11862 return Err(syn::Error::new_spanned(
11863 ty,
11864 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)"),
11865 ));
11866 }
11867 };
11868 Ok(DetectedType {
11869 kind,
11870 nullable: false,
11871 auto: false,
11872 fk_inner: None,
11873 })
11874}
11875
11876fn generic_inner<'a>(
11877 ty: &'a Type,
11878 arguments: &'a PathArguments,
11879 wrapper: &str,
11880) -> syn::Result<&'a Type> {
11881 let PathArguments::AngleBracketed(args) = arguments else {
11882 return Err(syn::Error::new_spanned(
11883 ty,
11884 format!("{wrapper} requires a generic argument"),
11885 ));
11886 };
11887 args.args
11888 .iter()
11889 .find_map(|a| match a {
11890 GenericArgument::Type(t) => Some(t),
11891 _ => None,
11892 })
11893 .ok_or_else(|| {
11894 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11895 })
11896}
11897
11898/// Like [`generic_inner`] but pulls *two* type args — the first is
11899/// required, the second is optional. Used by the `ForeignKey<T, K>`
11900/// detection where K defaults to `i64` when omitted.
11901fn generic_pair<'a>(
11902 ty: &'a Type,
11903 arguments: &'a PathArguments,
11904 wrapper: &str,
11905) -> syn::Result<(&'a Type, Option<&'a Type>)> {
11906 let PathArguments::AngleBracketed(args) = arguments else {
11907 return Err(syn::Error::new_spanned(
11908 ty,
11909 format!("{wrapper} requires a generic argument"),
11910 ));
11911 };
11912 let mut types = args.args.iter().filter_map(|a| match a {
11913 GenericArgument::Type(t) => Some(t),
11914 _ => None,
11915 });
11916 let first = types.next().ok_or_else(|| {
11917 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11918 })?;
11919 let second = types.next();
11920 Ok((first, second))
11921}
11922
11923fn to_snake_case(s: &str) -> String {
11924 let mut out = String::with_capacity(s.len() + 4);
11925 for (i, ch) in s.chars().enumerate() {
11926 if ch.is_ascii_uppercase() {
11927 if i > 0 {
11928 out.push('_');
11929 }
11930 out.push(ch.to_ascii_lowercase());
11931 } else {
11932 out.push(ch);
11933 }
11934 }
11935 out
11936}
11937
11938// ============================================================
11939// #[derive(Form)] — slice 8.4B
11940// ============================================================
11941
11942/// Per-field `#[form(...)]` attributes recognised by the derive.
11943#[derive(Default)]
11944struct FormFieldAttrs {
11945 min: Option<i64>,
11946 max: Option<i64>,
11947 min_length: Option<u32>,
11948 max_length: Option<u32>,
11949 /// `#[form(clean = "fn_name")]` — Django-shape `clean_<field>` hook.
11950 /// The named static method on the form struct is called after the
11951 /// field's typed parse + length/range checks; it gets the parsed
11952 /// value by reference and returns `Result<<FieldType>, String>`.
11953 /// On Ok, the returned value replaces the parsed one; on Err, the
11954 /// message is attached to the field error list. Issue #372.
11955 clean: Option<syn::Ident>,
11956}
11957
11958/// Container-level `#[form(...)]` attributes. Currently only the
11959/// Django-shape cross-field `validate` hook (issue #373).
11960#[derive(Default)]
11961struct FormContainerAttrs {
11962 /// `#[form(validate = "fn_name")]` — Django-shape `clean()` hook.
11963 /// After every per-field parse succeeds, the named method on the
11964 /// form struct is called with `&self` and may return
11965 /// `Result<(), FormErrors>`. Errors merge into the field error
11966 /// list. Issue #373.
11967 validate: Option<syn::Ident>,
11968}
11969
11970/// Detected shape of a form field's Rust type.
11971#[derive(Clone, Copy)]
11972enum FormFieldKind {
11973 String,
11974 I16,
11975 I32,
11976 I64,
11977 F32,
11978 F64,
11979 Bool,
11980}
11981
11982impl FormFieldKind {
11983 fn parse_method(self) -> &'static str {
11984 match self {
11985 Self::I16 => "i16",
11986 Self::I32 => "i32",
11987 Self::I64 => "i64",
11988 Self::F32 => "f32",
11989 Self::F64 => "f64",
11990 // String + Bool don't go through `str::parse`; the codegen
11991 // handles them inline.
11992 Self::String | Self::Bool => "",
11993 }
11994 }
11995}
11996
11997fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
11998 let root = rustango_root();
11999 let struct_name = &input.ident;
12000
12001 let Data::Struct(data) = &input.data else {
12002 return Err(syn::Error::new_spanned(
12003 struct_name,
12004 "Form can only be derived on structs",
12005 ));
12006 };
12007 let Fields::Named(named) = &data.fields else {
12008 return Err(syn::Error::new_spanned(
12009 struct_name,
12010 "Form requires a struct with named fields",
12011 ));
12012 };
12013
12014 // #373 — container-level `#[form(validate = "fn")]` hook.
12015 let container = parse_form_container_attrs(input)?;
12016 let post_field_clean: Vec<TokenStream2> = Vec::new();
12017 let _ = post_field_clean;
12018
12019 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12020 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12021
12022 for field in &named.named {
12023 let ident = field
12024 .ident
12025 .as_ref()
12026 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12027 let attrs = parse_form_field_attrs(field)?;
12028 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12029
12030 let name_lit = ident.to_string();
12031 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12032 // #372 — append the per-field `clean_<field>` call right after
12033 // the parse block when the attribute is set. The clean fn
12034 // takes &T and returns Result<T, String>; on Err we attach
12035 // the message to the field error list without aborting
12036 // (matches Django's "collect all field errors" shape).
12037 let clean_block = if let Some(clean_fn) = &attrs.clean {
12038 quote! {
12039 if __errors.fields().get(#name_lit).is_none() {
12040 match Self::#clean_fn(&#ident) {
12041 ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
12042 ::core::result::Result::Err(__msg) => {
12043 __errors.add(#name_lit, __msg);
12044 }
12045 }
12046 }
12047 }
12048 } else {
12049 quote! {}
12050 };
12051 field_blocks.push(quote! {
12052 #parse_block
12053 #clean_block
12054 });
12055 field_idents.push(ident);
12056 }
12057
12058 // #373 — after every per-field parse + clean succeeds, call the
12059 // cross-field validator if declared. Errors merge into the
12060 // outgoing FormErrors via the existing `FormErrors::merge` helper
12061 // (same primitive the DRF serializer cross-field hook uses).
12062 let cross_field_call = if let Some(validate_fn) = &container.validate {
12063 quote! {
12064 if __errors.is_empty() {
12065 let __candidate = Self { #( #field_idents ),* };
12066 if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12067 __errors.merge(__other);
12068 }
12069 if !__errors.is_empty() {
12070 return ::core::result::Result::Err(__errors);
12071 }
12072 return ::core::result::Result::Ok(__candidate);
12073 }
12074 }
12075 } else {
12076 quote! {}
12077 };
12078
12079 Ok(quote! {
12080 impl #root::forms::Form for #struct_name {
12081 fn parse(
12082 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12083 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12084 let mut __errors = #root::forms::FormErrors::default();
12085 #( #field_blocks )*
12086 #cross_field_call
12087 if !__errors.is_empty() {
12088 return ::core::result::Result::Err(__errors);
12089 }
12090 ::core::result::Result::Ok(Self {
12091 #( #field_idents ),*
12092 })
12093 }
12094 }
12095 })
12096}
12097
12098fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12099 let mut out = FormContainerAttrs::default();
12100 for attr in &input.attrs {
12101 if !attr.path().is_ident("form") {
12102 continue;
12103 }
12104 attr.parse_nested_meta(|meta| {
12105 if meta.path.is_ident("validate") {
12106 let s: LitStr = meta.value()?.parse()?;
12107 out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12108 return Ok(());
12109 }
12110 Err(meta.error("unknown form container attribute (supported: `validate`)"))
12111 })?;
12112 }
12113 Ok(out)
12114}
12115
12116fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12117 let mut out = FormFieldAttrs::default();
12118 for attr in &field.attrs {
12119 if !attr.path().is_ident("form") {
12120 continue;
12121 }
12122 attr.parse_nested_meta(|meta| {
12123 if meta.path.is_ident("min") {
12124 let lit: syn::LitInt = meta.value()?.parse()?;
12125 out.min = Some(lit.base10_parse::<i64>()?);
12126 return Ok(());
12127 }
12128 if meta.path.is_ident("max") {
12129 let lit: syn::LitInt = meta.value()?.parse()?;
12130 out.max = Some(lit.base10_parse::<i64>()?);
12131 return Ok(());
12132 }
12133 if meta.path.is_ident("min_length") {
12134 let lit: syn::LitInt = meta.value()?.parse()?;
12135 out.min_length = Some(lit.base10_parse::<u32>()?);
12136 return Ok(());
12137 }
12138 if meta.path.is_ident("max_length") {
12139 let lit: syn::LitInt = meta.value()?.parse()?;
12140 out.max_length = Some(lit.base10_parse::<u32>()?);
12141 return Ok(());
12142 }
12143 if meta.path.is_ident("clean") {
12144 let s: LitStr = meta.value()?.parse()?;
12145 out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12146 return Ok(());
12147 }
12148 Err(meta.error(
12149 "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12150 ))
12151 })?;
12152 }
12153 Ok(out)
12154}
12155
12156fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12157 let Type::Path(TypePath { path, qself: None }) = ty else {
12158 return Err(syn::Error::new(
12159 span,
12160 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12161 ));
12162 };
12163 let last = path
12164 .segments
12165 .last()
12166 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12167
12168 if last.ident == "Option" {
12169 let inner = generic_inner(ty, &last.arguments, "Option")?;
12170 let (kind, nested) = detect_form_field(inner, span)?;
12171 if nested {
12172 return Err(syn::Error::new(
12173 span,
12174 "nested Option in Form fields is not supported",
12175 ));
12176 }
12177 return Ok((kind, true));
12178 }
12179
12180 let kind = match last.ident.to_string().as_str() {
12181 "String" => FormFieldKind::String,
12182 "i16" => FormFieldKind::I16,
12183 "i32" => FormFieldKind::I32,
12184 "i64" => FormFieldKind::I64,
12185 "f32" => FormFieldKind::F32,
12186 "f64" => FormFieldKind::F64,
12187 "bool" => FormFieldKind::Bool,
12188 other => {
12189 return Err(syn::Error::new(
12190 span,
12191 format!(
12192 "Form field type `{other}` is not supported in v0.8 — use String / \
12193 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12194 ),
12195 ));
12196 }
12197 };
12198 Ok((kind, false))
12199}
12200
12201#[allow(clippy::too_many_lines)]
12202fn render_form_field_parse(
12203 ident: &syn::Ident,
12204 name_lit: &str,
12205 kind: FormFieldKind,
12206 nullable: bool,
12207 attrs: &FormFieldAttrs,
12208) -> TokenStream2 {
12209 // Pull the raw &str from the payload. Uses variable name `data` to
12210 // match the new `Form::parse(data: &HashMap<…>)` signature.
12211 let lookup = quote! {
12212 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12213 };
12214
12215 let parsed_value = match kind {
12216 FormFieldKind::Bool => quote! {
12217 let __v: bool = match __raw {
12218 ::core::option::Option::None => false,
12219 ::core::option::Option::Some(__s) => !matches!(
12220 __s.to_ascii_lowercase().as_str(),
12221 "" | "false" | "0" | "off" | "no"
12222 ),
12223 };
12224 },
12225 FormFieldKind::String => {
12226 if nullable {
12227 quote! {
12228 let __v: ::core::option::Option<::std::string::String> = match __raw {
12229 ::core::option::Option::None => ::core::option::Option::None,
12230 ::core::option::Option::Some(__s) if __s.is_empty() => {
12231 ::core::option::Option::None
12232 }
12233 ::core::option::Option::Some(__s) => {
12234 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12235 }
12236 };
12237 }
12238 } else {
12239 quote! {
12240 let __v: ::std::string::String = match __raw {
12241 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12242 ::core::clone::Clone::clone(__s)
12243 }
12244 _ => {
12245 __errors.add(#name_lit, "This field is required.");
12246 ::std::string::String::new()
12247 }
12248 };
12249 }
12250 }
12251 }
12252 FormFieldKind::I16
12253 | FormFieldKind::I32
12254 | FormFieldKind::I64
12255 | FormFieldKind::F32
12256 | FormFieldKind::F64 => {
12257 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12258 let ty_lit = kind.parse_method();
12259 let default_val = match kind {
12260 FormFieldKind::I16 => quote! { 0i16 },
12261 FormFieldKind::I32 => quote! { 0i32 },
12262 FormFieldKind::I64 => quote! { 0i64 },
12263 FormFieldKind::F32 => quote! { 0f32 },
12264 FormFieldKind::F64 => quote! { 0f64 },
12265 _ => quote! { Default::default() },
12266 };
12267 if nullable {
12268 quote! {
12269 let __v: ::core::option::Option<#parse_ty> = match __raw {
12270 ::core::option::Option::None => ::core::option::Option::None,
12271 ::core::option::Option::Some(__s) if __s.is_empty() => {
12272 ::core::option::Option::None
12273 }
12274 ::core::option::Option::Some(__s) => {
12275 match __s.parse::<#parse_ty>() {
12276 ::core::result::Result::Ok(__n) => {
12277 ::core::option::Option::Some(__n)
12278 }
12279 ::core::result::Result::Err(__e) => {
12280 __errors.add(
12281 #name_lit,
12282 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12283 );
12284 ::core::option::Option::None
12285 }
12286 }
12287 }
12288 };
12289 }
12290 } else {
12291 quote! {
12292 let __v: #parse_ty = match __raw {
12293 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12294 match __s.parse::<#parse_ty>() {
12295 ::core::result::Result::Ok(__n) => __n,
12296 ::core::result::Result::Err(__e) => {
12297 __errors.add(
12298 #name_lit,
12299 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12300 );
12301 #default_val
12302 }
12303 }
12304 }
12305 _ => {
12306 __errors.add(#name_lit, "This field is required.");
12307 #default_val
12308 }
12309 };
12310 }
12311 }
12312 }
12313 };
12314
12315 let validators = render_form_validators(name_lit, kind, nullable, attrs);
12316
12317 quote! {
12318 // `mut` so the per-field `clean` hook (#372) can rewrite the
12319 // parsed value in-place when it returns Ok with a normalized
12320 // form (e.g. trim / lowercase).
12321 let mut #ident = {
12322 #lookup
12323 #parsed_value
12324 #validators
12325 __v
12326 };
12327 }
12328}
12329
12330fn render_form_validators(
12331 name_lit: &str,
12332 kind: FormFieldKind,
12333 nullable: bool,
12334 attrs: &FormFieldAttrs,
12335) -> TokenStream2 {
12336 let mut checks: Vec<TokenStream2> = Vec::new();
12337
12338 let val_ref = if nullable {
12339 quote! { __v.as_ref() }
12340 } else {
12341 quote! { ::core::option::Option::Some(&__v) }
12342 };
12343
12344 let is_string = matches!(kind, FormFieldKind::String);
12345 let is_numeric = matches!(
12346 kind,
12347 FormFieldKind::I16
12348 | FormFieldKind::I32
12349 | FormFieldKind::I64
12350 | FormFieldKind::F32
12351 | FormFieldKind::F64
12352 );
12353
12354 if is_string {
12355 if let Some(min_len) = attrs.min_length {
12356 let min_len_usize = min_len as usize;
12357 checks.push(quote! {
12358 if let ::core::option::Option::Some(__s) = #val_ref {
12359 if __s.len() < #min_len_usize {
12360 __errors.add(
12361 #name_lit,
12362 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12363 );
12364 }
12365 }
12366 });
12367 }
12368 if let Some(max_len) = attrs.max_length {
12369 let max_len_usize = max_len as usize;
12370 checks.push(quote! {
12371 if let ::core::option::Option::Some(__s) = #val_ref {
12372 if __s.len() > #max_len_usize {
12373 __errors.add(
12374 #name_lit,
12375 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12376 );
12377 }
12378 }
12379 });
12380 }
12381 }
12382
12383 if is_numeric {
12384 if let Some(min) = attrs.min {
12385 checks.push(quote! {
12386 if let ::core::option::Option::Some(__n) = #val_ref {
12387 if (*__n as f64) < (#min as f64) {
12388 __errors.add(
12389 #name_lit,
12390 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12391 );
12392 }
12393 }
12394 });
12395 }
12396 if let Some(max) = attrs.max {
12397 checks.push(quote! {
12398 if let ::core::option::Option::Some(__n) = #val_ref {
12399 if (*__n as f64) > (#max as f64) {
12400 __errors.add(
12401 #name_lit,
12402 ::std::format!("Ensure this value is less than or equal to {}.", #max),
12403 );
12404 }
12405 }
12406 });
12407 }
12408 }
12409
12410 quote! { #( #checks )* }
12411}
12412
12413// ============================================================
12414// #[derive(ViewSet)]
12415// ============================================================
12416
12417struct ViewSetAttrs {
12418 model: syn::Path,
12419 fields: Option<Vec<String>>,
12420 filter_fields: Vec<String>,
12421 search_fields: Vec<String>,
12422 /// (field_name, desc)
12423 ordering: Vec<(String, bool)>,
12424 page_size: Option<usize>,
12425 read_only: bool,
12426 perms: ViewSetPermsAttrs,
12427 /// `#[viewset(serializer = SomeSerializer)]` — render list /
12428 /// retrieve / create responses through this `#[derive(Serializer)]`
12429 /// type instead of the default field-level projection (requires the
12430 /// `serializer` feature). Tri-dialect.
12431 serializer: Option<syn::Path>,
12432}
12433
12434#[derive(Default)]
12435struct ViewSetPermsAttrs {
12436 list: Vec<String>,
12437 retrieve: Vec<String>,
12438 create: Vec<String>,
12439 update: Vec<String>,
12440 destroy: Vec<String>,
12441}
12442
12443fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12444 let root = rustango_root();
12445 let struct_name = &input.ident;
12446
12447 // Must be a unit struct or an empty named struct.
12448 match &input.data {
12449 Data::Struct(s) => match &s.fields {
12450 Fields::Unit | Fields::Named(_) => {}
12451 Fields::Unnamed(_) => {
12452 return Err(syn::Error::new_spanned(
12453 struct_name,
12454 "ViewSet can only be derived on a unit struct or an empty named struct",
12455 ));
12456 }
12457 },
12458 _ => {
12459 return Err(syn::Error::new_spanned(
12460 struct_name,
12461 "ViewSet can only be derived on a struct",
12462 ));
12463 }
12464 }
12465
12466 let attrs = parse_viewset_attrs(input)?;
12467 let model_path = &attrs.model;
12468
12469 // `.fields(&[...])` call — None means skip (use all scalar fields).
12470 let fields_call = if let Some(ref fields) = attrs.fields {
12471 let lits = fields.iter().map(|f| f.as_str());
12472 quote!(.fields(&[ #(#lits),* ]))
12473 } else {
12474 quote!()
12475 };
12476
12477 let filter_fields_call = if attrs.filter_fields.is_empty() {
12478 quote!()
12479 } else {
12480 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12481 quote!(.filter_fields(&[ #(#lits),* ]))
12482 };
12483
12484 let search_fields_call = if attrs.search_fields.is_empty() {
12485 quote!()
12486 } else {
12487 let lits = attrs.search_fields.iter().map(|f| f.as_str());
12488 quote!(.search_fields(&[ #(#lits),* ]))
12489 };
12490
12491 let ordering_call = if attrs.ordering.is_empty() {
12492 quote!()
12493 } else {
12494 let pairs = attrs.ordering.iter().map(|(f, desc)| {
12495 let f = f.as_str();
12496 quote!((#f, #desc))
12497 });
12498 quote!(.ordering(&[ #(#pairs),* ]))
12499 };
12500
12501 let page_size_call = if let Some(n) = attrs.page_size {
12502 quote!(.page_size(#n))
12503 } else {
12504 quote!()
12505 };
12506
12507 let read_only_call = if attrs.read_only {
12508 quote!(.read_only())
12509 } else {
12510 quote!()
12511 };
12512
12513 // `.serializer::<S>()` — reshape responses through a derived
12514 // serializer. Requires the downstream crate to enable the
12515 // `serializer` feature (the method is gated on it).
12516 let serializer_call = if let Some(ref ser) = attrs.serializer {
12517 quote!(.serializer::<#ser>())
12518 } else {
12519 quote!()
12520 };
12521
12522 let perms = &attrs.perms;
12523 let perms_call = if perms.list.is_empty()
12524 && perms.retrieve.is_empty()
12525 && perms.create.is_empty()
12526 && perms.update.is_empty()
12527 && perms.destroy.is_empty()
12528 {
12529 quote!()
12530 } else {
12531 let list_lits = perms.list.iter().map(|s| s.as_str());
12532 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12533 let create_lits = perms.create.iter().map(|s| s.as_str());
12534 let update_lits = perms.update.iter().map(|s| s.as_str());
12535 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12536 quote! {
12537 .permissions(#root::viewset::ViewSetPerms {
12538 list: ::std::vec![ #(#list_lits.to_owned()),* ],
12539 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12540 create: ::std::vec![ #(#create_lits.to_owned()),* ],
12541 update: ::std::vec![ #(#update_lits.to_owned()),* ],
12542 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
12543 })
12544 }
12545 };
12546
12547 Ok(quote! {
12548 impl #struct_name {
12549 /// Build an `axum::Router` with the six standard REST endpoints
12550 /// for this ViewSet, mounted at `prefix`.
12551 pub fn router(prefix: &str, pool: #root::sql::sqlx::PgPool) -> #root::__axum::Router {
12552 #root::viewset::ViewSet::for_model(
12553 <#model_path as #root::core::Model>::SCHEMA
12554 )
12555 #fields_call
12556 #filter_fields_call
12557 #search_fields_call
12558 #ordering_call
12559 #page_size_call
12560 #perms_call
12561 #read_only_call
12562 #serializer_call
12563 .router(prefix, pool)
12564 }
12565 }
12566 })
12567}
12568
12569fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
12570 let mut model: Option<syn::Path> = None;
12571 let mut fields: Option<Vec<String>> = None;
12572 let mut filter_fields: Vec<String> = Vec::new();
12573 let mut search_fields: Vec<String> = Vec::new();
12574 let mut ordering: Vec<(String, bool)> = Vec::new();
12575 let mut page_size: Option<usize> = None;
12576 let mut read_only = false;
12577 let mut perms = ViewSetPermsAttrs::default();
12578 let mut serializer: Option<syn::Path> = None;
12579
12580 for attr in &input.attrs {
12581 if !attr.path().is_ident("viewset") {
12582 continue;
12583 }
12584 attr.parse_nested_meta(|meta| {
12585 if meta.path.is_ident("model") {
12586 let path: syn::Path = meta.value()?.parse()?;
12587 model = Some(path);
12588 return Ok(());
12589 }
12590 if meta.path.is_ident("serializer") {
12591 let path: syn::Path = meta.value()?.parse()?;
12592 serializer = Some(path);
12593 return Ok(());
12594 }
12595 if meta.path.is_ident("fields") {
12596 let s: LitStr = meta.value()?.parse()?;
12597 fields = Some(split_field_list(&s.value()));
12598 return Ok(());
12599 }
12600 if meta.path.is_ident("filter_fields") {
12601 let s: LitStr = meta.value()?.parse()?;
12602 filter_fields = split_field_list(&s.value());
12603 return Ok(());
12604 }
12605 if meta.path.is_ident("search_fields") {
12606 let s: LitStr = meta.value()?.parse()?;
12607 search_fields = split_field_list(&s.value());
12608 return Ok(());
12609 }
12610 if meta.path.is_ident("ordering") {
12611 let s: LitStr = meta.value()?.parse()?;
12612 ordering = parse_ordering_list(&s.value());
12613 return Ok(());
12614 }
12615 if meta.path.is_ident("page_size") {
12616 let lit: syn::LitInt = meta.value()?.parse()?;
12617 page_size = Some(lit.base10_parse::<usize>()?);
12618 return Ok(());
12619 }
12620 if meta.path.is_ident("read_only") {
12621 read_only = true;
12622 return Ok(());
12623 }
12624 if meta.path.is_ident("permissions") {
12625 meta.parse_nested_meta(|inner| {
12626 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
12627 let s: LitStr = inner.value()?.parse()?;
12628 Ok(split_field_list(&s.value()))
12629 };
12630 if inner.path.is_ident("list") {
12631 perms.list = parse_codenames(&inner)?;
12632 } else if inner.path.is_ident("retrieve") {
12633 perms.retrieve = parse_codenames(&inner)?;
12634 } else if inner.path.is_ident("create") {
12635 perms.create = parse_codenames(&inner)?;
12636 } else if inner.path.is_ident("update") {
12637 perms.update = parse_codenames(&inner)?;
12638 } else if inner.path.is_ident("destroy") {
12639 perms.destroy = parse_codenames(&inner)?;
12640 } else {
12641 return Err(inner.error(
12642 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
12643 ));
12644 }
12645 Ok(())
12646 })?;
12647 return Ok(());
12648 }
12649 Err(meta.error(
12650 "unknown viewset attribute (supported: model, fields, filter_fields, \
12651 search_fields, ordering, page_size, read_only, serializer, permissions(...))",
12652 ))
12653 })?;
12654 }
12655
12656 let model = model.ok_or_else(|| {
12657 syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
12658 })?;
12659
12660 Ok(ViewSetAttrs {
12661 model,
12662 fields,
12663 filter_fields,
12664 search_fields,
12665 ordering,
12666 page_size,
12667 read_only,
12668 perms,
12669 serializer,
12670 })
12671}
12672
12673// ============================================================ #[derive(Serializer)]
12674
12675struct SerializerContainerAttrs {
12676 model: syn::Path,
12677 /// `#[serializer(validate = "fn_name")]` on the struct — DRF-shape
12678 /// cross-field validation hook (#436). The named inherent method
12679 /// must take `&self` and return
12680 /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
12681 /// `validate()` runs every per-field validator first; then calls
12682 /// the cross-field method if declared; aggregates all errors into
12683 /// one `FormErrors`.
12684 cross_validate: Option<syn::Ident>,
12685}
12686
12687#[derive(Default)]
12688struct SerializerFieldAttrs {
12689 read_only: bool,
12690 write_only: bool,
12691 source: Option<String>,
12692 skip: bool,
12693 /// `#[serializer(method = "fn_name")]` — DRF SerializerMethodField
12694 /// analog. The macro emits `from_model` initializer that calls
12695 /// `Self::fn_name(&model)` and stores the return value.
12696 method: Option<String>,
12697 /// `#[serializer(validate = "fn_name")]` — per-field validator
12698 /// callable run by `Self::validate(&self)`. Must return
12699 /// `Result<(), String>`. Errors land in `FormErrors` keyed by
12700 /// the field name.
12701 validate: Option<String>,
12702 /// `#[serializer(nested)]` on a field whose type is another
12703 /// `Serializer` — the macro emits `from_model` initializer that
12704 /// reads the parent via `model.<source>.value()` then calls the
12705 /// child serializer's `from_model(parent)`. When the FK is
12706 /// unloaded the field falls back to `Default::default()` (does
12707 /// NOT panic) so a missing prefetch in prod degrades gracefully.
12708 /// Source field on the model defaults to the field name; override
12709 /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
12710 /// panic-on-unloaded behavior for tests.
12711 nested: bool,
12712 /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
12713 /// strict behavior: panic when the FK isn't loaded. Useful in
12714 /// test code where forgetting select_related must trip a hard
12715 /// failure rather than render a blank nested object.
12716 nested_strict: bool,
12717 /// `#[serializer(many = TagSerializer)]` — declare the field as
12718 /// a list of nested serializers. Field type must be `Vec<S>`
12719 /// where `S` is the inner serializer. The macro initializes the
12720 /// field to `Vec::new()` in `from_model` and emits a typed
12721 /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
12722 /// maps each model row through `S::from_model`. Auto-load isn't
12723 /// possible (the M2M / one-to-many accessor is async); callers
12724 /// fetch the children + call the setter post-from_model.
12725 many: Option<syn::Type>,
12726 /// `#[serializer(slug = "name")]` — DRF `SlugRelatedField` analog.
12727 /// Source field on the model must be a `ForeignKey<T>`; the
12728 /// macro emits `from_model` glue that walks
12729 /// `model.<source>.value()?.<slug>` and clones it. Field type on
12730 /// the serializer is typically `String` (whatever type the slug
12731 /// column has). When the FK is unloaded the field falls back to
12732 /// `Default::default()`, same graceful-degrade contract as
12733 /// `nested`. Source defaults to the field name; override with
12734 /// `source = "..."`. v0.44.
12735 slug: Option<String>,
12736 /// `#[serializer(max_length = N)]` — DRF `MaxLengthValidator`. Caps
12737 /// the character count of a string field on write. Overrides the
12738 /// model's `max_length`; when absent the model value is inherited.
12739 max_length: Option<u64>,
12740 /// `#[serializer(min_length = N)]` — DRF `MinLengthValidator`.
12741 /// Serializer-only (the model has no `min_length` column).
12742 min_length: Option<u64>,
12743 /// `#[serializer(min = N)]` — DRF `MinValueValidator`. Inclusive
12744 /// integer lower bound; overrides the model's `min` when given.
12745 min: Option<i64>,
12746 /// `#[serializer(max = N)]` — DRF `MaxValueValidator`. Inclusive
12747 /// integer upper bound; overrides the model's `max` when given.
12748 max: Option<i64>,
12749}
12750
12751fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
12752 let mut model: Option<syn::Path> = None;
12753 let mut cross_validate: Option<syn::Ident> = None;
12754 for attr in &input.attrs {
12755 if !attr.path().is_ident("serializer") {
12756 continue;
12757 }
12758 attr.parse_nested_meta(|meta| {
12759 if meta.path.is_ident("model") {
12760 let _eq: syn::Token![=] = meta.input.parse()?;
12761 model = Some(meta.input.parse()?);
12762 return Ok(());
12763 }
12764 if meta.path.is_ident("validate") {
12765 // #436 — container-level `validate = "fn_name"` for the
12766 // DRF cross-field-validation shape. Field-level
12767 // `#[serializer(validate = "...")]` on a field is
12768 // parsed separately in `parse_serializer_field_attrs`.
12769 let s: LitStr = meta.value()?.parse()?;
12770 cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
12771 return Ok(());
12772 }
12773 Err(meta.error(
12774 "unknown serializer container attribute \
12775 (supported: `model`, `validate`)",
12776 ))
12777 })?;
12778 }
12779 let model = model.ok_or_else(|| {
12780 syn::Error::new_spanned(
12781 &input.ident,
12782 "`#[serializer(model = SomeModel)]` is required",
12783 )
12784 })?;
12785 Ok(SerializerContainerAttrs {
12786 model,
12787 cross_validate,
12788 })
12789}
12790
12791fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
12792 let mut out = SerializerFieldAttrs::default();
12793 for attr in &field.attrs {
12794 if !attr.path().is_ident("serializer") {
12795 continue;
12796 }
12797 attr.parse_nested_meta(|meta| {
12798 if meta.path.is_ident("read_only") {
12799 out.read_only = true;
12800 return Ok(());
12801 }
12802 if meta.path.is_ident("write_only") {
12803 out.write_only = true;
12804 return Ok(());
12805 }
12806 if meta.path.is_ident("skip") {
12807 out.skip = true;
12808 return Ok(());
12809 }
12810 if meta.path.is_ident("source") {
12811 let s: LitStr = meta.value()?.parse()?;
12812 out.source = Some(s.value());
12813 return Ok(());
12814 }
12815 if meta.path.is_ident("method") {
12816 let s: LitStr = meta.value()?.parse()?;
12817 out.method = Some(s.value());
12818 return Ok(());
12819 }
12820 if meta.path.is_ident("validate") {
12821 let s: LitStr = meta.value()?.parse()?;
12822 out.validate = Some(s.value());
12823 return Ok(());
12824 }
12825 if meta.path.is_ident("many") {
12826 let _eq: syn::Token![=] = meta.input.parse()?;
12827 out.many = Some(meta.input.parse()?);
12828 return Ok(());
12829 }
12830 if meta.path.is_ident("nested") {
12831 out.nested = true;
12832 // Optional strict flag inside parentheses:
12833 // #[serializer(nested(strict))]
12834 if meta.input.peek(syn::token::Paren) {
12835 meta.parse_nested_meta(|inner| {
12836 if inner.path.is_ident("strict") {
12837 out.nested_strict = true;
12838 return Ok(());
12839 }
12840 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
12841 })?;
12842 }
12843 return Ok(());
12844 }
12845 if meta.path.is_ident("slug") {
12846 let s: LitStr = meta.value()?.parse()?;
12847 out.slug = Some(s.value());
12848 return Ok(());
12849 }
12850 if meta.path.is_ident("max_length") {
12851 let lit: syn::LitInt = meta.value()?.parse()?;
12852 out.max_length = Some(lit.base10_parse::<u64>()?);
12853 return Ok(());
12854 }
12855 if meta.path.is_ident("min_length") {
12856 let lit: syn::LitInt = meta.value()?.parse()?;
12857 out.min_length = Some(lit.base10_parse::<u64>()?);
12858 return Ok(());
12859 }
12860 if meta.path.is_ident("min") {
12861 let lit: syn::LitInt = meta.value()?.parse()?;
12862 out.min = Some(lit.base10_parse::<i64>()?);
12863 return Ok(());
12864 }
12865 if meta.path.is_ident("max") {
12866 let lit: syn::LitInt = meta.value()?.parse()?;
12867 out.max = Some(lit.base10_parse::<i64>()?);
12868 return Ok(());
12869 }
12870 Err(meta.error(
12871 "unknown serializer field attribute (supported: \
12872 `read_only`, `write_only`, `source`, `skip`, `method`, \
12873 `validate`, `nested`, `many`, `slug`, `max_length`, \
12874 `min_length`, `min`, `max`)",
12875 ))
12876 })?;
12877 }
12878 // Validate: read_only + write_only is nonsensical
12879 if out.read_only && out.write_only {
12880 return Err(syn::Error::new_spanned(
12881 field,
12882 "a field cannot be both `read_only` and `write_only`",
12883 ));
12884 }
12885 if out.method.is_some() && out.source.is_some() {
12886 return Err(syn::Error::new_spanned(
12887 field,
12888 "`method` and `source` are mutually exclusive — `method` computes \
12889 the value from a method, `source` reads it from a different model field",
12890 ));
12891 }
12892 if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
12893 return Err(syn::Error::new_spanned(
12894 field,
12895 "`slug` is mutually exclusive with `method`, `nested`, and `many` \
12896 — pick one strategy for populating the field",
12897 ));
12898 }
12899 Ok(out)
12900}
12901
12902fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
12903 let root = rustango_root();
12904 let struct_name = &input.ident;
12905 let struct_name_lit = struct_name.to_string();
12906
12907 let Data::Struct(data) = &input.data else {
12908 return Err(syn::Error::new_spanned(
12909 struct_name,
12910 "Serializer can only be derived on structs",
12911 ));
12912 };
12913 let Fields::Named(named) = &data.fields else {
12914 return Err(syn::Error::new_spanned(
12915 struct_name,
12916 "Serializer requires a struct with named fields",
12917 ));
12918 };
12919
12920 let container = parse_serializer_container_attrs(input)?;
12921 let model_path = &container.model;
12922
12923 // Classify each field. `ty` is only consumed by the
12924 // `#[cfg(feature = "openapi")]` block below, but we always
12925 // capture it to keep the field-info build a single pass.
12926 #[allow(dead_code)]
12927 struct FieldInfo {
12928 ident: syn::Ident,
12929 ty: syn::Type,
12930 attrs: SerializerFieldAttrs,
12931 }
12932 let mut fields_info: Vec<FieldInfo> = Vec::new();
12933 for field in &named.named {
12934 let ident = field.ident.clone().expect("named field has ident");
12935 let attrs = parse_serializer_field_attrs(field)?;
12936 fields_info.push(FieldInfo {
12937 ident,
12938 ty: field.ty.clone(),
12939 attrs,
12940 });
12941 }
12942
12943 // Generate from_model body: struct literal with each field assigned.
12944 let from_model_fields = fields_info.iter().map(|fi| {
12945 let ident = &fi.ident;
12946 let ty = &fi.ty;
12947 if let Some(_inner) = &fi.attrs.many {
12948 // Many — collection field. Initialize empty; caller
12949 // populates via the macro-emitted set_<field> helper
12950 // after fetching the M2M children.
12951 quote! { #ident: ::std::vec::Vec::new() }
12952 } else if let Some(method) = &fi.attrs.method {
12953 // SerializerMethodField: call Self::<method>(&model) to
12954 // compute the value. Method signature must be
12955 // `fn <method>(model: &T) -> <field type>`.
12956 let method_ident = syn::Ident::new(method, ident.span());
12957 quote! { #ident: Self::#method_ident(model) }
12958 } else if let Some(slug_field) = &fi.attrs.slug {
12959 // v0.44 — SlugRelatedField. Source defaults to the field
12960 // name on this struct; override via `source = "..."`. The
12961 // source field on the model is expected to be a
12962 // `ForeignKey<T>`; the slug field on the parent is named
12963 // by the attribute value. When the FK is unloaded the
12964 // field falls back to `Default::default()` — same
12965 // graceful-degrade contract as `nested`.
12966 let src_name = fi
12967 .attrs
12968 .source
12969 .as_deref()
12970 .unwrap_or(&fi.ident.to_string())
12971 .to_owned();
12972 let src_ident = syn::Ident::new(&src_name, ident.span());
12973 let slug_ident = syn::Ident::new(slug_field, ident.span());
12974 quote! {
12975 #ident: match model.#src_ident.value() {
12976 ::core::option::Option::Some(__loaded) =>
12977 ::core::clone::Clone::clone(&__loaded.#slug_ident),
12978 ::core::option::Option::None =>
12979 ::core::default::Default::default(),
12980 }
12981 }
12982 } else if fi.attrs.nested {
12983 // Nested serializer. Source defaults to the field name on
12984 // this struct; override via `source = "..."`. The source
12985 // field on the model is expected to be a `ForeignKey<T>`
12986 // whose `.value()` returns `Option<&T>` after lazy-load.
12987 //
12988 // Behavior matrix (tweakable per-field):
12989 // * FK loaded → nested object materializes via
12990 // ChildSerializer::from_model(parent).
12991 // * FK unloaded → fall back to ChildSerializer::default()
12992 // (so prod doesn't crash on a missing
12993 // prefetch — just renders a blank nested
12994 // object). Add `#[serializer(nested,
12995 // strict)]` to keep the v0.18.1
12996 // panic-on-unloaded behavior for tests
12997 // that want hard guardrails.
12998 let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
12999 let src_ident = syn::Ident::new(&src_name, ident.span());
13000 if fi.attrs.nested_strict {
13001 let panic_msg = format!(
13002 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
13003 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
13004 );
13005 quote! {
13006 #ident: <#ty as #root::serializer::ModelSerializer>::from_model(
13007 model.#src_ident.value().expect(#panic_msg),
13008 )
13009 }
13010 } else {
13011 quote! {
13012 #ident: match model.#src_ident.value() {
13013 ::core::option::Option::Some(__loaded) =>
13014 <#ty as #root::serializer::ModelSerializer>::from_model(__loaded),
13015 ::core::option::Option::None =>
13016 ::core::default::Default::default(),
13017 }
13018 }
13019 }
13020 } else if fi.attrs.write_only || fi.attrs.skip {
13021 // Not read from model — use default
13022 quote! { #ident: ::core::default::Default::default() }
13023 } else if let Some(src) = &fi.attrs.source {
13024 let src_ident = syn::Ident::new(src, ident.span());
13025 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13026 } else {
13027 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13028 }
13029 });
13030
13031 // is_writable predicate (also used by writable_lits /
13032 // writable_source_lits below).
13033 let is_writable = |fi: &&FieldInfo| {
13034 !fi.attrs.read_only
13035 && !fi.attrs.skip
13036 && fi.attrs.method.is_none()
13037 && !fi.attrs.nested
13038 && fi.attrs.many.is_none()
13039 && fi.attrs.slug.is_none()
13040 };
13041
13042 // Declarative field constraints (DRF `validators=[...]`): one block
13043 // per writable field, run inside `validate()`. Each resolves its
13044 // bounds as the serializer attr when given (`#[serializer(max_length
13045 // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13046 // `max` / `choices`), then dispatches on the field's JSON value via
13047 // `forms::validators::check_value` (string → length/choices, integer
13048 // → min/max).
13049 let opt_usize = |v: Option<u64>| match v {
13050 Some(n) => {
13051 let n = n as usize;
13052 quote!(::core::option::Option::Some(#n))
13053 }
13054 None => quote!(::core::option::Option::None),
13055 };
13056 let opt_i64 = |v: Option<i64>| match v {
13057 Some(n) => quote!(::core::option::Option::Some(#n)),
13058 None => quote!(::core::option::Option::None),
13059 };
13060 let constraint_blocks: Vec<_> = fields_info
13061 .iter()
13062 .filter(is_writable)
13063 .map(|fi| {
13064 let ident = &fi.ident;
13065 let fname = ident.to_string();
13066 let mname = fi
13067 .attrs
13068 .source
13069 .clone()
13070 .unwrap_or_else(|| fi.ident.to_string());
13071 let attr_max_len = opt_usize(fi.attrs.max_length);
13072 let attr_min_len = opt_usize(fi.attrs.min_length);
13073 let attr_min = opt_i64(fi.attrs.min);
13074 let attr_max = opt_i64(fi.attrs.max);
13075 quote! {
13076 {
13077 let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13078 let __max_length: ::core::option::Option<usize> = #attr_max_len
13079 .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13080 let __min_length: ::core::option::Option<usize> = #attr_min_len;
13081 let __min: ::core::option::Option<i64> =
13082 #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13083 let __max: ::core::option::Option<i64> =
13084 #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13085 let __choices = __sf.and_then(|__f| __f.choices);
13086 let __v = #root::__serde_json::to_value(&self.#ident)
13087 .unwrap_or(#root::__serde_json::Value::Null);
13088 #root::forms::validators::check_value(
13089 #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13090 &mut __errors,
13091 );
13092 }
13093 }
13094 })
13095 .collect();
13096 let has_constraints = !constraint_blocks.is_empty();
13097
13098 // Per-field validators (DRF-shape `validators=[...]`). Emit a
13099 // `validate(&self)` method that runs each user-defined validator
13100 // and aggregates errors into `FormErrors`.
13101 let validator_calls: Vec<_> = fields_info
13102 .iter()
13103 .filter_map(|fi| {
13104 let ident = &fi.ident;
13105 let name_lit = ident.to_string();
13106 let method = fi.attrs.validate.as_ref()?;
13107 let method_ident = syn::Ident::new(method, ident.span());
13108 Some(quote! {
13109 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13110 __errors.add(#name_lit.to_owned(), __e);
13111 }
13112 })
13113 })
13114 .collect();
13115 // #436 — DRF cross-field `validate(self)` shape. If the
13116 // container declared `#[serializer(validate = "fn_name")]`,
13117 // the macro-generated `validate(&self)` runs every per-field
13118 // validator first, then calls the user's cross-field method,
13119 // merging its `FormErrors` into the per-field errors. Either
13120 // alone is enough to emit the wrapper.
13121 let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13122 quote! {
13123 // Merge cross-field errors into the per-field bucket so
13124 // a single .validate() call surfaces both layers.
13125 if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13126 __errors.merge(__cross);
13127 }
13128 }
13129 });
13130 let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13131 // Anything to run? Declarative constraints (for any writable field) +
13132 // per-field validators + cross-field hook.
13133 let has_run_validations = has_validators || has_constraints;
13134 // Shared body: declarative field constraints first (length / range /
13135 // choices, serializer-attr-or-model), then per-field validators, then
13136 // the cross-field hook.
13137 let validate_body = quote! {
13138 let mut __errors = #root::forms::FormErrors::default();
13139 #( #constraint_blocks )*
13140 #( #validator_calls )*
13141 #cross_validate_call
13142 if __errors.is_empty() {
13143 ::core::result::Result::Ok(())
13144 } else {
13145 ::core::result::Result::Err(__errors)
13146 }
13147 };
13148 // Inherent `validate(&self)` — kept for back-compat with direct
13149 // `serializer.validate()` calls that don't import `ModelSerializer`.
13150 // Emitted only when the serializer declares per-field/cross-field
13151 // validators (unchanged rule) so it never collides with a
13152 // hand-written inherent `validate`.
13153 let validate_method = if has_validators {
13154 quote! {
13155 impl #struct_name {
13156 /// Run the declarative field constraints, every
13157 /// `#[serializer(validate = "...")]` per-field validator,
13158 /// and (when declared) the container-level cross-field
13159 /// validator. Aggregates errors into `FormErrors` keyed by
13160 /// the field name. Returns `Ok(())` when all pass.
13161 pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13162 #validate_body
13163 }
13164 }
13165 }
13166 } else {
13167 quote! {}
13168 };
13169 // Trait-method override so the type-erased ViewSet write path can
13170 // dispatch `ModelSerializer::validate` generically. Emitted whenever
13171 // there's anything to run (declarative constraints inherited from the
13172 // model count); otherwise the trait's default no-op is used.
13173 let trait_validate_override = if has_run_validations {
13174 quote! {
13175 fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13176 #validate_body
13177 }
13178 }
13179 } else {
13180 quote! {}
13181 };
13182
13183 // For every `#[serializer(many = S)]` field, emit a
13184 // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13185 // helper that maps the parents through `S::from_model`.
13186 let many_setters: Vec<_> = fields_info
13187 .iter()
13188 .filter_map(|fi| {
13189 let many_ty = fi.attrs.many.as_ref()?;
13190 let ident = &fi.ident;
13191 let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
13192 Some(quote! {
13193 /// Populate this `many` field by mapping each parent model
13194 /// through the inner serializer's `from_model`. Call after
13195 /// fetching the M2M / one-to-many children since
13196 /// `from_model` itself can't await an SQL query.
13197 pub fn #setter(
13198 &mut self,
13199 models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13200 ) -> &mut Self {
13201 self.#ident = models.iter()
13202 .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13203 .collect();
13204 self
13205 }
13206 })
13207 })
13208 .collect();
13209 let many_setters_impl = if many_setters.is_empty() {
13210 quote! {}
13211 } else {
13212 quote! {
13213 impl #struct_name {
13214 #( #many_setters )*
13215 }
13216 }
13217 };
13218
13219 // Generate custom Serialize: skip write_only fields
13220 let output_fields: Vec<_> = fields_info
13221 .iter()
13222 .filter(|fi| !fi.attrs.write_only)
13223 .collect();
13224 let output_field_count = output_fields.len();
13225 let serialize_fields = output_fields.iter().map(|fi| {
13226 let ident = &fi.ident;
13227 let name_lit = ident.to_string();
13228 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13229 });
13230
13231 // writable_fields: normal + write_only.
13232 // Exclude:
13233 // - `read_only` — server-computed.
13234 // - `skip` — caller sets manually post-from_model.
13235 // - `method` — computed from a Self::fn(&model) call; accepting
13236 // it on write is meaningless.
13237 // - `nested` / `many` — populated from related-model data, not
13238 // from a field on the wire body.
13239 // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13240 // `many` in `writable_fields()`, which made the ViewSet write
13241 // path accept those fields from the JSON body and try to bind
13242 // them to the SQL UPDATE — a silent no-op at best, a type
13243 // mismatch at worst.
13244 // (`is_writable` is defined above, near the constraint codegen.)
13245 let writable_lits: Vec<_> = fields_info
13246 .iter()
13247 .filter(is_writable)
13248 .map(|fi| fi.ident.to_string())
13249 .collect();
13250
13251 // `writable_source_fields`: the MODEL field names of writable
13252 // serializer fields (source-resolved). The ViewSet write path skips
13253 // every model column NOT in this set, so `read_only` / `method` /
13254 // computed fields a client posts are ignored instead of written.
13255 let writable_source_lits: Vec<String> = fields_info
13256 .iter()
13257 .filter(is_writable)
13258 .map(|fi| {
13259 fi.attrs
13260 .source
13261 .clone()
13262 .unwrap_or_else(|| fi.ident.to_string())
13263 })
13264 .collect();
13265
13266 // `from_writable_json`: build a partial instance for input
13267 // validation. Writable fields are parsed from the JSON body (keyed
13268 // by serializer field name); every other field defaults. Per-field
13269 // type errors collect into `FormErrors` keyed by the field name.
13270 // Construction is field-by-field (not `Self::default()`) so it works
13271 // for serializers regardless of a struct-level `Default`.
13272 let from_writable_json_inits: Vec<_> = fields_info
13273 .iter()
13274 .map(|fi| {
13275 let ident = &fi.ident;
13276 let fname = ident.to_string();
13277 let ty = &fi.ty;
13278 if is_writable(&fi) {
13279 quote! {
13280 #ident: match __obj.and_then(|__o| __o.get(#fname)) {
13281 ::core::option::Option::Some(__v) => {
13282 match #root::__serde_json::from_value::<#ty>(
13283 ::core::clone::Clone::clone(__v),
13284 ) {
13285 ::core::result::Result::Ok(__x) => __x,
13286 ::core::result::Result::Err(__e) => {
13287 __errors.add(#fname.to_owned(), __e.to_string());
13288 ::core::default::Default::default()
13289 }
13290 }
13291 }
13292 ::core::option::Option::None => ::core::default::Default::default(),
13293 }
13294 }
13295 } else {
13296 quote! { #ident: ::core::default::Default::default() }
13297 }
13298 })
13299 .collect();
13300
13301 // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
13302 // Only includes fields shown in JSON output (skips write_only). For each
13303 // `Option<T>` field, omit from `required` and add `.nullable()`.
13304 let openapi_impl = {
13305 #[cfg(feature = "openapi")]
13306 {
13307 let property_calls = output_fields.iter().map(|fi| {
13308 let ident = &fi.ident;
13309 let name_lit = ident.to_string();
13310 let ty = &fi.ty;
13311 let nullable_call = if is_option(ty) {
13312 quote! { .nullable() }
13313 } else {
13314 quote! {}
13315 };
13316 quote! {
13317 .property(
13318 #name_lit,
13319 <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13320 #nullable_call,
13321 )
13322 }
13323 });
13324 let required_lits: Vec<_> = output_fields
13325 .iter()
13326 .filter(|fi| !is_option(&fi.ty))
13327 .map(|fi| fi.ident.to_string())
13328 .collect();
13329 quote! {
13330 impl #root::openapi::OpenApiSchema for #struct_name {
13331 fn openapi_schema() -> #root::openapi::Schema {
13332 #root::openapi::Schema::object()
13333 #( #property_calls )*
13334 .required([ #( #required_lits ),* ])
13335 }
13336 }
13337 }
13338 }
13339 #[cfg(not(feature = "openapi"))]
13340 {
13341 quote! {}
13342 }
13343 };
13344
13345 Ok(quote! {
13346 impl #root::serializer::ModelSerializer for #struct_name {
13347 type Model = #model_path;
13348
13349 fn from_model(model: &Self::Model) -> Self {
13350 Self {
13351 #( #from_model_fields ),*
13352 }
13353 }
13354
13355 fn writable_fields() -> &'static [&'static str] {
13356 &[ #( #writable_lits ),* ]
13357 }
13358
13359 fn writable_source_fields() -> &'static [&'static str] {
13360 &[ #( #writable_source_lits ),* ]
13361 }
13362
13363 fn from_writable_json(
13364 __body: &#root::__serde_json::Value,
13365 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
13366 let mut __errors = #root::forms::FormErrors::default();
13367 let __obj = __body.as_object();
13368 let __out = Self {
13369 #( #from_writable_json_inits ),*
13370 };
13371 if __errors.is_empty() {
13372 ::core::result::Result::Ok(__out)
13373 } else {
13374 ::core::result::Result::Err(__errors)
13375 }
13376 }
13377
13378 #trait_validate_override
13379 }
13380
13381 impl #root::__serde::Serialize for #struct_name {
13382 fn serialize<S>(&self, serializer: S)
13383 -> ::core::result::Result<S::Ok, S::Error>
13384 where
13385 S: #root::__serde::Serializer,
13386 {
13387 use #root::__serde::ser::SerializeStruct;
13388 let mut __state = serializer.serialize_struct(
13389 #struct_name_lit,
13390 #output_field_count,
13391 )?;
13392 #( #serialize_fields )*
13393 __state.end()
13394 }
13395 }
13396
13397 #openapi_impl
13398
13399 #validate_method
13400
13401 #many_setters_impl
13402 })
13403}
13404
13405/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
13406/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
13407/// when the feature is off.
13408#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
13409fn is_option(ty: &syn::Type) -> bool {
13410 if let syn::Type::Path(p) = ty {
13411 if let Some(last) = p.path.segments.last() {
13412 return last.ident == "Option";
13413 }
13414 }
13415 false
13416}