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 // The escaped-LIKE lookups delegate to the typed Column
436 // methods, which escape the value and pair it with the
437 // *Escaped ops so `%` / `_` match literally on every dialect
438 // (#1257) — the macro no longer owns any pattern wrapping.
439 "iexact" => quote! {
440 #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
441 },
442 "contains" => quote! {
443 #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
444 },
445 "icontains" => quote! {
446 #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
447 },
448 "startswith" => quote! {
449 #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
450 },
451 "istartswith" => quote! {
452 #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
453 },
454 "endswith" => quote! {
455 #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
456 },
457 "iendswith" => quote! {
458 #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
459 },
460 "in" => quote! {
461 #root::core::Column::is_in(#path::#base_ident, #value)
462 },
463 "not_in" => quote! {
464 #root::core::Column::not_in(#path::#base_ident, #value)
465 },
466 "isnull" => {
467 // Must be a bool literal at macro time so we can route to
468 // is_null vs is_not_null without a runtime branch.
469 let b = match value {
470 syn::Expr::Lit(syn::ExprLit {
471 lit: syn::Lit::Bool(b),
472 ..
473 }) => b.value(),
474 _ => {
475 return Err(syn::Error::new_spanned(
476 value,
477 "Q!(): `__isnull` requires a `true` or `false` literal",
478 ));
479 }
480 };
481 if b {
482 quote! { #root::core::Column::is_null(#path::#base_ident) }
483 } else {
484 quote! { #root::core::Column::is_not_null(#path::#base_ident) }
485 }
486 }
487 "between" => {
488 // Accept a tuple literal `(lo, hi)`.
489 let tuple = match value {
490 syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
491 _ => {
492 return Err(syn::Error::new_spanned(
493 value,
494 "Q!(): `__between` requires a tuple literal `(lo, hi)`",
495 ));
496 }
497 };
498 let lo = &tuple.elems[0];
499 let hi = &tuple.elems[1];
500 quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
501 }
502 "regex" => quote! {
503 #root::core::Column::regex(#path::#base_ident, #value)
504 },
505 "iregex" => quote! {
506 #root::core::Column::iregex(#path::#base_ident, #value)
507 },
508 _ => {
509 return Err(syn::Error::new(
510 field_span,
511 format!(
512 "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",
513 suffix
514 ),
515 ));
516 }
517 };
518 Ok(expanded)
519}
520
521fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
522 // Default to "./migrations" if invoked without args.
523 let path_str = if input.is_empty() {
524 "./migrations".to_string()
525 } else {
526 let lit: LitStr = syn::parse2(input)?;
527 lit.value()
528 };
529
530 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
531 syn::Error::new(
532 proc_macro2::Span::call_site(),
533 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
534 )
535 })?;
536 let abs = std::path::Path::new(&manifest).join(&path_str);
537
538 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
539 if abs.is_dir() {
540 let read = std::fs::read_dir(&abs).map_err(|e| {
541 syn::Error::new(
542 proc_macro2::Span::call_site(),
543 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
544 )
545 })?;
546 for entry in read.flatten() {
547 let path = entry.path();
548 if !path.is_file() {
549 continue;
550 }
551 if path.extension().and_then(|s| s.to_str()) != Some("json") {
552 continue;
553 }
554 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
555 continue;
556 };
557 entries.push((stem.to_owned(), path));
558 }
559 }
560 entries.sort_by(|a, b| a.0.cmp(&b.0));
561
562 // Compile-time chain validation: read each migration's JSON,
563 // pull `name` and `prev` (file-stem-keyed for the chain check),
564 // and verify every `prev` points to another migration in the
565 // slice. Mismatches between the file stem and the embedded
566 // `name` field — and broken `prev` chains — fail at MACRO
567 // EXPANSION time so a misshapen migration set never compiles.
568 //
569 // This is the v0.4 Slice 5 distinguisher: rustango's JSON
570 // migrations + a Rust proc-macro that reads them is the unique
571 // combo nothing else in the Django-shape Rust camp can match
572 // (Cot's are imperative Rust code, Loco's are SeaORM up/down,
573 // Rwf's are raw SQL — none have a static chain to validate).
574 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
575 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
576 for (stem, path) in &entries {
577 let raw = std::fs::read_to_string(path).map_err(|e| {
578 syn::Error::new(
579 proc_macro2::Span::call_site(),
580 format!(
581 "embed_migrations!: cannot read {} for chain validation: {e}",
582 path.display()
583 ),
584 )
585 })?;
586 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
587 syn::Error::new(
588 proc_macro2::Span::call_site(),
589 format!(
590 "embed_migrations!: {} is not valid JSON: {e}",
591 path.display()
592 ),
593 )
594 })?;
595 let name = json
596 .get("name")
597 .and_then(|v| v.as_str())
598 .ok_or_else(|| {
599 syn::Error::new(
600 proc_macro2::Span::call_site(),
601 format!(
602 "embed_migrations!: {} is missing the `name` field",
603 path.display()
604 ),
605 )
606 })?
607 .to_owned();
608 if name != *stem {
609 return Err(syn::Error::new(
610 proc_macro2::Span::call_site(),
611 format!(
612 "embed_migrations!: file stem `{stem}` does not match the migration's \
613 `name` field `{name}` — rename the file or fix the JSON",
614 ),
615 ));
616 }
617 let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
618 chain_names.push(name.clone());
619 prev_refs.push((name, prev));
620 }
621
622 let name_set: std::collections::HashSet<&str> =
623 chain_names.iter().map(String::as_str).collect();
624 for (name, prev) in &prev_refs {
625 if let Some(p) = prev {
626 if !name_set.contains(p.as_str()) {
627 return Err(syn::Error::new(
628 proc_macro2::Span::call_site(),
629 format!(
630 "embed_migrations!: broken migration chain — `{name}` declares \
631 prev=`{p}` but no migration with that name exists in {}",
632 abs.display()
633 ),
634 ));
635 }
636 }
637 }
638
639 let pairs: Vec<TokenStream2> = entries
640 .iter()
641 .map(|(name, path)| {
642 let path_lit = path.display().to_string();
643 quote! { (#name, ::core::include_str!(#path_lit)) }
644 })
645 .collect();
646
647 Ok(quote! {
648 {
649 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
650 __RUSTANGO_EMBEDDED
651 }
652 })
653}
654
655fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
656 let root = rustango_root();
657 let struct_name = &input.ident;
658
659 let Data::Struct(data) = &input.data else {
660 return Err(syn::Error::new_spanned(
661 struct_name,
662 "Model can only be derived on structs",
663 ));
664 };
665 let Fields::Named(named) = &data.fields else {
666 return Err(syn::Error::new_spanned(
667 struct_name,
668 "Model requires a struct with named fields",
669 ));
670 };
671
672 let container = parse_container_attrs(input)?;
673 let table = container
674 .table
675 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
676 let model_name = struct_name.to_string();
677
678 let collected = collect_fields(named, &table)?;
679
680 // Validate that #[rustango(display = "…")] names a real field.
681 if let Some((ref display, span)) = container.display {
682 if !collected.field_names.iter().any(|n| n == display) {
683 return Err(syn::Error::new(
684 span,
685 format!("`display = \"{display}\"` does not match any field on this struct"),
686 ));
687 }
688 }
689 let display = container.display.map(|(name, _)| name);
690 let app_label = container.app.clone();
691
692 // Validate admin field-name lists against declared field names.
693 // Note: `list_display` is intentionally NOT validated here. As of
694 // v0.32 it may also reference inventory-registered computed
695 // fields (via `register_admin_computed!`) whose existence the
696 // macro can't see at compile time — they're submitted from any
697 // crate that depends on rustango. The runtime list-view resolves
698 // unknown names against the inventory + silently drops the
699 // truly-bogus ones, which is the cheaper trade-off versus
700 // forcing a per-Model attr to opt out.
701 if let Some(admin) = &container.admin {
702 for (label, list) in [
703 ("search_fields", &admin.search_fields),
704 ("readonly_fields", &admin.readonly_fields),
705 ("list_filter", &admin.list_filter),
706 ] {
707 if let Some((names, span)) = list {
708 for name in names {
709 if !collected.field_names.iter().any(|n| n == name) {
710 return Err(syn::Error::new(
711 *span,
712 format!(
713 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
714 ),
715 ));
716 }
717 }
718 }
719 }
720 if let Some((pairs, span)) = &admin.ordering {
721 for (name, _) in pairs {
722 if !collected.field_names.iter().any(|n| n == name) {
723 return Err(syn::Error::new(
724 *span,
725 format!(
726 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
727 ),
728 ));
729 }
730 }
731 }
732 if let Some((groups, span)) = &admin.fieldsets {
733 for (_, fields) in groups {
734 for name in fields {
735 if !collected.field_names.iter().any(|n| n == name) {
736 return Err(syn::Error::new(
737 *span,
738 format!(
739 "`fieldsets`: \"{name}\" is not a declared field on this struct"
740 ),
741 ));
742 }
743 }
744 }
745 }
746 }
747 if let Some(audit) = &container.audit {
748 if let Some((names, span)) = &audit.track {
749 for name in names {
750 if !collected.field_names.iter().any(|n| n == name) {
751 return Err(syn::Error::new(
752 *span,
753 format!(
754 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
755 ),
756 ));
757 }
758 }
759 }
760 }
761
762 // Issue #291 / T2.5 — validate each `default_order` column name
763 // against the model's collected fields. Typos fail at macro-expand
764 // time, not at the database.
765 for (col, _desc, span) in &container.default_order {
766 if !collected.field_names.iter().any(|n| n == col) {
767 return Err(syn::Error::new(
768 *span,
769 format!(
770 "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
771 ),
772 ));
773 }
774 }
775
776 // Build the audit_track list for ModelSchema: None when no audit attr,
777 // Some(empty) when audit present without track, Some(names) when explicit.
778 let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
779 audit
780 .track
781 .as_ref()
782 .map(|(names, _)| names.clone())
783 .unwrap_or_default()
784 });
785
786 // Merge field-level indexes into the container's index list.
787 let mut all_indexes: Vec<IndexAttr> = container.indexes;
788 for field in &named.named {
789 let ident = field.ident.as_ref().expect("named");
790 let col = to_snake_case(&ident.to_string()); // column name fallback
791 // Re-parse field attrs to check for index flag
792 if let Ok(fa) = parse_field_attrs(field) {
793 if fa.index {
794 let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
795 let auto_name = if fa.index_unique {
796 format!("{table}_{col_name}_uq_idx")
797 } else {
798 format!("{table}_{col_name}_idx")
799 };
800 all_indexes.push(IndexAttr {
801 name: fa.index_name.or(Some(auto_name)),
802 columns: vec![col_name],
803 unique: fa.index_unique,
804 method: fa.index_method,
805 where_clause: None,
806 include: Vec::new(),
807 });
808 }
809 }
810 }
811
812 let model_impl = model_impl_tokens(
813 struct_name,
814 &model_name,
815 &table,
816 display.as_deref(),
817 app_label.as_deref(),
818 container.admin.as_ref(),
819 &container.default_order,
820 &collected.field_schemas,
821 collected.soft_delete_column.as_deref(),
822 container.permissions,
823 audit_track_names.as_deref(),
824 &container.m2m,
825 &all_indexes,
826 &container.checks,
827 &container.excludes,
828 &container.composite_fks,
829 &container.generic_fks,
830 container.scope.as_deref(),
831 container.is_view,
832 container.verbose_name.as_deref(),
833 container.verbose_name_plural.as_deref(),
834 container.managed,
835 container.base_manager_name.as_deref(),
836 container.order_with_respect_to.as_deref(),
837 container.proxy,
838 &container.required_db_features,
839 container.required_db_vendor.as_deref(),
840 container.default_related_name.as_deref(),
841 container.db_table_comment.as_deref(),
842 container
843 .get_latest_by
844 .as_ref()
845 .map(|(c, d)| (c.as_str(), *d)),
846 &container.extra_permissions,
847 &container.default_permissions,
848 &container.global_scopes,
849 &container.reverse_has_relations,
850 &container.generic_has_relations,
851 );
852 let module_ident = column_module_ident(struct_name);
853 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
854 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
855 let track_set: Option<std::collections::HashSet<&str>> = audit
856 .track
857 .as_ref()
858 .map(|(names, _)| names.iter().map(String::as_str).collect());
859 collected
860 .column_entries
861 .iter()
862 .filter(|c| {
863 track_set
864 .as_ref()
865 .map_or(true, |s| s.contains(c.name.as_str()))
866 })
867 .collect()
868 });
869 let inherent_impl = inherent_impl_tokens(
870 struct_name,
871 &collected,
872 collected.primary_key.as_ref(),
873 &column_consts,
874 audited_fields.as_deref(),
875 &all_indexes,
876 &container.manager_fns,
877 );
878 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
879 let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
880 let reverse_helpers = reverse_helper_tokens(
881 struct_name,
882 &collected.fk_relations,
883 container.default_related_name.as_deref(),
884 );
885 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
886 let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
887 // Issue #817 — `#[rustango(through(...))]` accessors.
888 let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
889 // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
890 let reverse_has_accessors =
891 reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
892 let generic_fk_accessors = generic_fk_accessor_tokens(
893 struct_name,
894 &container.generic_fks,
895 &collected.column_entries,
896 );
897
898 // Issue #271 / T1.9 — `#[rustango(manager(ext = "FooManagerExt"))]`
899 // emits an empty extension trait so users can add methods via
900 // `impl FooManagerExt for QuerySet<Foo>` without hand-writing the
901 // trait declaration. See `crates/rustango/src/manager.rs` for the
902 // pattern this replaces.
903 let manager_trait = container.manager_ext.as_ref().map(|name| {
904 let model_name_str = struct_name.to_string();
905 let doc = format!(
906 "Custom-Manager extension trait for [`{model_name_str}`]. \
907 Generated by `#[rustango(manager(ext = ...))]`. Add methods \
908 via `impl {name} for QuerySet<{model_name_str}> {{ ... }}`."
909 );
910 quote! {
911 #[doc = #doc]
912 pub trait #name: ::core::marker::Sized {}
913 }
914 });
915
916 Ok(quote! {
917 #model_impl
918 #inherent_impl
919 #from_row_impl
920 #column_module
921 #reverse_helpers
922 #m2m_accessors
923 #generic_m2m_accessors
924 #through_accessors
925 #reverse_has_accessors
926 #generic_fk_accessors
927 #manager_trait
928
929 #root::core::inventory::submit! {
930 #root::core::ModelEntry {
931 schema: <#struct_name as #root::core::Model>::SCHEMA,
932 // `module_path!()` evaluates at the registration site,
933 // so a Model declared in `crate::blog::models` records
934 // `"<crate>::blog::models"` and `resolved_app_label()`
935 // can infer "blog" without an explicit attribute.
936 module_path: ::core::module_path!(),
937 }
938 }
939 })
940}
941
942/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
943/// matches `field_name` against the model's FK fields and, for a
944/// match, decodes the FK target via the parent's macro-generated
945/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
946/// `ForeignKey::Loaded` on `self`.
947///
948/// Always emitted (with empty arms for FK-less models, which
949/// return `Ok(false)` for any field name) so the `T: LoadRelated`
950/// trait bound on `fetch_on` is universally satisfied — users
951/// never have to think about implementing it.
952fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
953 let root = rustango_root();
954 let arms = fk_relations.iter().map(|rel| {
955 let parent_ty = &rel.parent_type;
956 let fk_col = rel.fk_column.as_str();
957 // FK field's Rust ident matches its SQL column name in v0.8
958 // (no `column = "..."` rename ships on FK fields).
959 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
960 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
961 let assign = if rel.nullable {
962 quote! {
963 self.#field_ident = ::core::option::Option::Some(
964 #root::sql::ForeignKey::loaded(_pk, _parent),
965 );
966 }
967 } else {
968 quote! {
969 self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
970 }
971 };
972 quote! {
973 #fk_col => {
974 let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
975 // Audit #451 — multi-hop `select_related("a__b__c")`:
976 // stitch the deeper relation onto this parent first,
977 // decoding it at the accumulated `__next_alias`. The
978 // parent type also impls `LoadRelated`, so this recurses
979 // the FK chain to arbitrary depth.
980 if let ::core::option::Option::Some(__r) = __rest {
981 let _ = #root::sql::LoadRelated::__rustango_load_related(
982 &mut _parent, row, __r, &__next_alias,
983 )?;
984 }
985 // Loud-in-debug, default-in-release: a divergence
986 // between the FK field's declared `K` (drives the
987 // expected `SqlValue::<Variant>`) and the parent's
988 // `__rustango_pk_value` output is a macro-internal
989 // invariant break — surfacing the panic in dev
990 // catches it before users hit silent PK=0 corruption.
991 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
992 #root::core::SqlValue::#variant_ident(v) => v,
993 _other => {
994 ::core::debug_assert!(
995 false,
996 "rustango macro bug: load_related on FK `{}` expected \
997 SqlValue::{} from parent's __rustango_pk_value but got \
998 {:?} — file a bug at https://github.com/ujeenet/rustango",
999 #fk_col,
1000 ::core::stringify!(#variant_ident),
1001 _other,
1002 );
1003 #default_expr
1004 }
1005 };
1006 #assign
1007 ::core::result::Result::Ok(true)
1008 }
1009 }
1010 });
1011 quote! {
1012 #[cfg(feature = "postgres")]
1013 impl #root::sql::LoadRelated for #struct_name {
1014 #[allow(unused_variables)]
1015 fn __rustango_load_related(
1016 &mut self,
1017 row: &#root::sql::sqlx::postgres::PgRow,
1018 field_name: &str,
1019 alias: &str,
1020 ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1021 // Audit #451 — split the multi-hop path: `base` is the FK
1022 // on THIS model, `__rest` (if any) is the remaining chain
1023 // to stitch onto the loaded parent. `__next_alias` is the
1024 // accumulated join alias the parent's columns live under
1025 // (`{alias}__{next-hop}`), matching `lower_select_related`.
1026 let (__base, __rest): (&str, ::core::option::Option<&str>) =
1027 match field_name.split_once("__") {
1028 ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1029 ::core::option::Option::None => (field_name, ::core::option::Option::None),
1030 };
1031 let __next_alias: ::std::string::String = match __rest {
1032 ::core::option::Option::Some(__r) => {
1033 let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1034 ::std::format!("{}__{}", alias, __rb)
1035 }
1036 ::core::option::Option::None => ::std::string::String::new(),
1037 };
1038 match __base {
1039 #( #arms )*
1040 _ => ::core::result::Result::Ok(false),
1041 }
1042 }
1043 }
1044 }
1045}
1046
1047/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1048/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1049/// which expands to a `LoadRelatedMy` impl when rustango is built with
1050/// the `mysql` feature, and to nothing otherwise. The decoded parent
1051/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1052/// decoder, also batch8) so the dual emission is symmetric across
1053/// backends.
1054fn load_related_impl_my_tokens(
1055 struct_name: &syn::Ident,
1056 fk_relations: &[FkRelation],
1057) -> TokenStream2 {
1058 let root = rustango_root();
1059 let arms = fk_relations.iter().map(|rel| {
1060 let parent_ty = &rel.parent_type;
1061 let fk_col = rel.fk_column.as_str();
1062 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1063 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1064 let assign = if rel.nullable {
1065 quote! {
1066 __self.#field_ident = ::core::option::Option::Some(
1067 #root::sql::ForeignKey::loaded(_pk, _parent),
1068 );
1069 }
1070 } else {
1071 quote! {
1072 __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1073 }
1074 };
1075 // `self` IS hygiene-tracked through macro_rules — emitted from
1076 // a different context than the `&mut self` parameter inside
1077 // the macro_rules-expanded fn. Pass it through as `__self`
1078 // and let the macro_rules rebind it to the receiver.
1079 quote! {
1080 #fk_col => {
1081 let mut _parent: #parent_ty =
1082 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1083 // Audit #451 — multi-hop: stitch the deeper relation onto
1084 // the parent at the accumulated alias (see PG twin).
1085 if let ::core::option::Option::Some(__r) = __rest {
1086 let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1087 &mut _parent, row, __r, &__next_alias,
1088 )?;
1089 }
1090 // See note in `load_related_impl_tokens` (PG twin) —
1091 // the same loud-in-debug invariant guard.
1092 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1093 #root::core::SqlValue::#variant_ident(v) => v,
1094 _other => {
1095 ::core::debug_assert!(
1096 false,
1097 "rustango macro bug: load_related on FK `{}` expected \
1098 SqlValue::{} from parent's __rustango_pk_value but got \
1099 {:?} — file a bug at https://github.com/ujeenet/rustango",
1100 #fk_col,
1101 ::core::stringify!(#variant_ident),
1102 _other,
1103 );
1104 #default_expr
1105 }
1106 };
1107 #assign
1108 ::core::result::Result::Ok(true)
1109 }
1110 }
1111 });
1112 quote! {
1113 #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1114 #( #arms )*
1115 });
1116 }
1117}
1118
1119/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1120/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1121/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1122fn load_related_impl_sqlite_tokens(
1123 struct_name: &syn::Ident,
1124 fk_relations: &[FkRelation],
1125) -> TokenStream2 {
1126 let root = rustango_root();
1127 let arms = fk_relations.iter().map(|rel| {
1128 let parent_ty = &rel.parent_type;
1129 let fk_col = rel.fk_column.as_str();
1130 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1131 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1132 let assign = if rel.nullable {
1133 quote! {
1134 __self.#field_ident = ::core::option::Option::Some(
1135 #root::sql::ForeignKey::loaded(_pk, _parent),
1136 );
1137 }
1138 } else {
1139 quote! {
1140 __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1141 }
1142 };
1143 quote! {
1144 #fk_col => {
1145 let mut _parent: #parent_ty =
1146 <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1147 // Audit #451 — multi-hop: stitch the deeper relation onto
1148 // the parent at the accumulated alias (see PG twin).
1149 if let ::core::option::Option::Some(__r) = __rest {
1150 let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1151 &mut _parent, row, __r, &__next_alias,
1152 )?;
1153 }
1154 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1155 #root::core::SqlValue::#variant_ident(v) => v,
1156 _other => {
1157 ::core::debug_assert!(
1158 false,
1159 "rustango macro bug: load_related on FK `{}` expected \
1160 SqlValue::{} from parent's __rustango_pk_value but got \
1161 {:?} — file a bug at https://github.com/ujeenet/rustango",
1162 #fk_col,
1163 ::core::stringify!(#variant_ident),
1164 _other,
1165 );
1166 #default_expr
1167 }
1168 };
1169 #assign
1170 ::core::result::Result::Ok(true)
1171 }
1172 }
1173 });
1174 quote! {
1175 #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1176 #( #arms )*
1177 });
1178 }
1179}
1180
1181/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1182/// matches `field_name` against the model's FK fields and returns
1183/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1184/// group children by parent PK.
1185///
1186/// Always emitted (with `_ => None` for FK-less models) so the
1187/// trait bound on `fetch_with_prefetch` is universally satisfied.
1188fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1189 let root = rustango_root();
1190 let arms = fk_relations.iter().map(|rel| {
1191 let fk_col = rel.fk_column.as_str();
1192 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1193 if rel.pk_kind == DetectedKind::I64 {
1194 // i64 FK — return the stored PK so prefetch_related can
1195 // group children by it. Nullable variant unwraps via
1196 // `as_ref().map(...)`: an unset (NULL) FK column yields
1197 // `None` and that child sits out of the grouping (correct
1198 // semantics — it has no parent to attach to).
1199 if rel.nullable {
1200 quote! {
1201 #fk_col => self.#field_ident
1202 .as_ref()
1203 .map(|fk| #root::sql::ForeignKey::pk(fk)),
1204 }
1205 } else {
1206 quote! {
1207 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1208 }
1209 }
1210 } else {
1211 // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1212 // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1213 // i64-keyed grouping path — the trait signature is
1214 // `Option<i64>` and a non-i64 PK can't lower into it.
1215 // The FK still works for everything else (CRUD, lazy
1216 // load via `.get()`, select_related JOINs); only the
1217 // bulk prefetch grouper needs the integer key.
1218 quote! {
1219 #fk_col => ::core::option::Option::None,
1220 }
1221 }
1222 });
1223 // PK-type-agnostic version: every FK arm emits an
1224 // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1225 // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1226 // opt OUT of the legacy i64 method (it returns None) but opt IN
1227 // here.
1228 let value_arms = fk_relations.iter().map(|rel| {
1229 let fk_col = rel.fk_column.as_str();
1230 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1231 if rel.nullable {
1232 quote! {
1233 #fk_col => self.#field_ident
1234 .as_ref()
1235 .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1236 #root::sql::ForeignKey::pk(fk)
1237 )),
1238 }
1239 } else {
1240 quote! {
1241 #fk_col => ::core::option::Option::Some(
1242 ::core::convert::Into::<#root::core::SqlValue>::into(
1243 self.#field_ident.pk()
1244 )
1245 ),
1246 }
1247 }
1248 });
1249 quote! {
1250 impl #root::sql::FkPkAccess for #struct_name {
1251 #[allow(unused_variables)]
1252 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1253 match field_name {
1254 #( #arms )*
1255 _ => ::core::option::Option::None,
1256 }
1257 }
1258 #[allow(unused_variables)]
1259 fn __rustango_fk_pk_value(
1260 &self,
1261 field_name: &str,
1262 ) -> ::core::option::Option<#root::core::SqlValue> {
1263 match field_name {
1264 #( #value_arms )*
1265 _ => ::core::option::Option::None,
1266 }
1267 }
1268 }
1269 }
1270}
1271
1272/// For every `ForeignKey<Parent>` field on `Child`, emit
1273/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1274/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
1275/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
1276/// — the canonical reverse-FK fetch. One round trip, no N+1.
1277///
1278/// **PG-only emission**: the accessor is bounded on
1279/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1280/// both of which are gated behind the `postgres` cargo feature. The
1281/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1282/// model derive itself compiles on tri-dialect / sqlite-only
1283/// downstream builds — the accessor just isn't materialised. A tri-
1284/// dialect `_set_pool` variant is a separate follow-up.
1285fn reverse_helper_tokens(
1286 child_ident: &syn::Ident,
1287 fk_relations: &[FkRelation],
1288 default_related_name: Option<&str>,
1289) -> TokenStream2 {
1290 let root = rustango_root();
1291 if fk_relations.is_empty() {
1292 return TokenStream2::new();
1293 }
1294 // Method-name resolution per FK (issue #816 + follow-up):
1295 // 1. Field-level `#[rustango(related_name = "...")]` on the FK
1296 // itself — wins over everything else. Django's
1297 // `ForeignKey(related_name="...")`.
1298 // 2. Container-level `default_related_name = "..."` on the
1299 // child — Django's `class Meta: default_related_name`.
1300 // Applies to every FK on this model that didn't override.
1301 // 3. Fallback: `<child_snake>_set` — Django's `<child>_set`
1302 // convention. `Post` → `post_set`, `BlogComment` →
1303 // `blog_comment_set`. Avoids English-plural edge cases.
1304 //
1305 // The PG-on-executor variant keeps the resolved name; the
1306 // tri-dialect `_pool` variant appends `_pool` to it (matches the
1307 // framework's convention for the `&Pool` flavor of every helper).
1308 let default_pg_suffix = default_related_name
1309 .map(str::to_owned)
1310 .unwrap_or_else(|| format!("{}_set", to_snake_case(&child_ident.to_string())));
1311 let impls = fk_relations.iter().map(|rel| {
1312 let pg_suffix = rel
1313 .related_name
1314 .clone()
1315 .unwrap_or_else(|| default_pg_suffix.clone());
1316 let pool_suffix = format!("{}_pool", pg_suffix);
1317 let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1318 let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1319 let parent_ty = &rel.parent_type;
1320 let fk_col = rel.fk_column.as_str();
1321 let doc = format!(
1322 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1323 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1324 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1325 further `{child_ident}::objects()` filters via direct queryset use."
1326 );
1327 let pool_doc = format!(
1328 "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1329 [`#root::sql::Pool`] and dispatches per backend so the \
1330 reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1331 Use this from framework code that holds a `&Pool` (admin, \
1332 tenancy resolver, viewset handlers); reach for the executor- \
1333 bound variant when you already have a typed `sqlx::Executor`."
1334 );
1335 quote! {
1336 #[cfg(feature = "postgres")]
1337 impl #parent_ty {
1338 #[doc = #doc]
1339 ///
1340 /// # Errors
1341 /// Returns [`#root::sql::ExecError`] for SQL-writing
1342 /// or driver failures.
1343 pub async fn #pg_method_ident<'_c, _E>(
1344 &self,
1345 _executor: _E,
1346 ) -> ::core::result::Result<
1347 ::std::vec::Vec<#child_ident>,
1348 #root::sql::ExecError,
1349 >
1350 where
1351 _E: #root::sql::sqlx::Executor<
1352 '_c,
1353 Database = #root::sql::sqlx::Postgres,
1354 >,
1355 {
1356 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1357 #root::query::QuerySet::<#child_ident>::new()
1358 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1359 .fetch_on(_executor)
1360 .await
1361 }
1362 }
1363
1364 impl #parent_ty {
1365 #[doc = #pool_doc]
1366 ///
1367 /// # Errors
1368 /// Returns [`#root::sql::ExecError`] for SQL-writing
1369 /// or driver failures.
1370 pub async fn #pool_method_ident(
1371 &self,
1372 pool: &#root::sql::Pool,
1373 ) -> ::core::result::Result<
1374 ::std::vec::Vec<#child_ident>,
1375 #root::sql::ExecError,
1376 > {
1377 use #root::sql::FetcherPool as _;
1378 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1379 #root::query::QuerySet::<#child_ident>::new()
1380 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1381 .fetch(pool)
1382 .await
1383 }
1384 }
1385 }
1386 });
1387 quote! { #( #impls )* }
1388}
1389
1390/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1391/// relation declared on the model.
1392/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1393/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1394///
1395/// Closes #239 + #240 — the Django-shape `comment.content_object` /
1396/// `comment.content_object = post` ergonomics on top of the existing
1397/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1398///
1399/// `column_entries` is passed so we can resolve each `ct_column` /
1400/// `pk_column` SQL name back to its Rust field ident — the macro
1401/// only sees the column-side strings in the attribute, but the
1402/// emitted accessor needs to read the actual struct field.
1403fn generic_fk_accessor_tokens(
1404 struct_name: &syn::Ident,
1405 generic_fks: &[GenericFkAttr],
1406 column_entries: &[ColumnEntry],
1407) -> TokenStream2 {
1408 let root = rustango_root();
1409 if generic_fks.is_empty() {
1410 return TokenStream2::new();
1411 }
1412 let methods = generic_fks.iter().filter_map(|gfk| {
1413 // Resolve `ct_column` + `pk_column` to the struct's Rust
1414 // field idents. A typo (column name doesn't match any field)
1415 // emits no method for that registration — the user will see
1416 // the compiler reject the SCHEMA literal anyway, so there's
1417 // a clear error path without us double-reporting.
1418 let ct_ident = column_entries
1419 .iter()
1420 .find(|c| c.column == gfk.ct_column)
1421 .map(|c| c.ident.clone())?;
1422 let pk_ident = column_entries
1423 .iter()
1424 .find(|c| c.column == gfk.pk_column)
1425 .map(|c| c.ident.clone())?;
1426
1427 let accessor_ident =
1428 syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1429 let setter_ident =
1430 syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1431 let name_literal = gfk.name.as_str();
1432
1433 Some(quote! {
1434 #[doc = concat!(
1435 "Resolve the polymorphic `",
1436 #name_literal,
1437 "` relation. Reads `self.",
1438 stringify!(#ct_ident),
1439 "` + `self.",
1440 stringify!(#pk_ident),
1441 "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1442 "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1443 #name_literal,
1444 "\", ...))]`."
1445 )]
1446 pub async fn #accessor_ident(
1447 &self,
1448 pool: &#root::sql::Pool,
1449 ) -> ::core::result::Result<
1450 ::core::option::Option<#root::__serde_json::Value>,
1451 #root::sql::ExecError,
1452 > {
1453 let gfk = #root::contenttypes::GenericForeignKey::new(
1454 self.#ct_ident as i64,
1455 self.#pk_ident as i64,
1456 );
1457 gfk.get_object(pool).await
1458 }
1459
1460 #[doc = concat!(
1461 "Set the polymorphic `",
1462 #name_literal,
1463 "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1464 stringify!(#ct_ident),
1465 "` and `self.",
1466 stringify!(#pk_ident),
1467 "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1468 #name_literal,
1469 "\", ...))]`."
1470 )]
1471 pub async fn #setter_ident<T: #root::core::Model>(
1472 &mut self,
1473 pool: &#root::sql::Pool,
1474 target_pk: i64,
1475 ) -> ::core::result::Result<(), #root::sql::ExecError> {
1476 let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1477 pool,
1478 target_pk,
1479 ).await?;
1480 self.#ct_ident = gfk.content_type_id as _;
1481 self.#pk_ident = gfk.object_pk as _;
1482 ::core::result::Result::Ok(())
1483 }
1484 })
1485 });
1486 quote! {
1487 impl #struct_name {
1488 #( #methods )*
1489 }
1490 }
1491}
1492
1493fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1494 let root = rustango_root();
1495 if m2m_relations.is_empty() {
1496 return TokenStream2::new();
1497 }
1498 let methods = m2m_relations.iter().map(|rel| {
1499 let method_name = format!("{}_m2m", rel.name);
1500 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1501 let through = rel.through.as_str();
1502 let src_col = rel.src.as_str();
1503 let dst_col = rel.dst.as_str();
1504 quote! {
1505 pub fn #method_ident(&self) -> #root::sql::M2MManager {
1506 #root::sql::M2MManager {
1507 src_pk: self.__rustango_pk_value(),
1508 through: #through,
1509 src_col: #src_col,
1510 dst_col: #dst_col,
1511 }
1512 }
1513 }
1514 });
1515 quote! {
1516 impl #struct_name {
1517 #( #methods )*
1518 }
1519 }
1520}
1521
1522/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1523/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1524fn generic_m2m_accessor_tokens(
1525 struct_name: &syn::Ident,
1526 relations: &[GenericM2MAttr],
1527) -> TokenStream2 {
1528 let root = rustango_root();
1529 if relations.is_empty() {
1530 return TokenStream2::new();
1531 }
1532 let methods = relations.iter().map(|rel| {
1533 let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1534 let through = rel.through.as_str();
1535 let pk_col = rel.pk_column.as_str();
1536 let ct_col = rel.ct_column.as_str();
1537 let related_col = rel.related_column.as_str();
1538 quote! {
1539 pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1540 #root::sql::GenericM2MManager {
1541 src_pk: self.__rustango_pk_value(),
1542 src_schema: <Self as #root::core::Model>::SCHEMA,
1543 through: #through,
1544 pk_col: #pk_col,
1545 ct_col: #ct_col,
1546 dst_col: #related_col,
1547 }
1548 }
1549 }
1550 });
1551 quote! {
1552 impl #struct_name {
1553 #( #methods )*
1554 }
1555 }
1556}
1557
1558/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1559/// associated functions for each `#[rustango(reverse_has(...))]`
1560/// attribute. Issue #830.
1561///
1562/// The two emitted functions return ready-to-use `WhereExpr` nodes
1563/// that downstream callers drop into
1564/// `QuerySet::<Self>::where_raw(...)`:
1565///
1566/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1567/// <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1568/// `whereHas` parity (without the closure-style sub-predicate
1569/// refinement — that's a follow-up; users can layer additional
1570/// predicates by constructing the SelectQuery themselves and
1571/// calling `where_raw(exists(query))` from `crate::core::subquery`).
1572/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1573/// `whereDoesntHave` parity.
1574///
1575/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1576/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1577/// machinery threads the outer-table reference through automatically
1578/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1579fn reverse_has_accessor_tokens(
1580 struct_name: &syn::Ident,
1581 reverse_has_relations: &[ReverseHasAttr],
1582) -> TokenStream2 {
1583 let root = rustango_root();
1584 if reverse_has_relations.is_empty() {
1585 return TokenStream2::new();
1586 }
1587 let methods = reverse_has_relations.iter().map(|rel| {
1588 let exists_name = format!("{}_exists_expr", rel.name);
1589 let not_exists_name = format!("{}_not_exists_expr", rel.name);
1590 let count_name = format!("{}_count", rel.name);
1591 let fetch_name = format!("{}_fetch", rel.name);
1592 let first_name = format!("{}_first", rel.name);
1593 let pluck_name = format!("{}_pluck", rel.name);
1594 let accessor_name = rel.name.as_str();
1595 let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1596 let not_exists_ident = syn::Ident::new(¬_exists_name, struct_name.span());
1597 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1598 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1599 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1600 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1601 let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1602 let child = &rel.child;
1603 let child_fk_column = rel.child_fk_column.as_str();
1604 let self_pk_column = rel.self_pk_column.as_str();
1605 let exists_doc = format!(
1606 "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1607 FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1608 Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1609 filter to {struct_name}s with at least one matching child.",
1610 );
1611 let not_exists_doc = format!(
1612 "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1613 (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1614 <outer>.{self_pk_column})`. Drop into \
1615 `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1616 {struct_name}s with **no** matching child.",
1617 );
1618 let count_doc = format!(
1619 "Eloquent `$model->{name}->count()` analog — returns \
1620 the number of `{child}` rows whose `{child_fk_column}` \
1621 matches this `{struct_name}` instance's primary key. \
1622 Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1623 {child_fk_column} = <self.pk>`.",
1624 name = rel.name,
1625 );
1626 let accessor_doc = format!(
1627 "Eloquent `$model->{name}` accessor — returns a \
1628 `QuerySet<{child}>` filtered to rows whose \
1629 `{child_fk_column}` matches this `{struct_name}` \
1630 instance's primary key. **Chainable**: compose `.filter()` \
1631 / `.order_by()` / `.limit()` etc. on top, then call \
1632 `.fetch(&pool)` (the QuerySet trait method) when \
1633 done. For the simple \"fetch all\" hot path with no \
1634 further composition, prefer the bare-name \
1635 `{name}_fetch(&pool)` companion.",
1636 name = rel.name,
1637 );
1638 let fetch_doc = format!(
1639 "Eloquent `$model->{name}->get()` — bare-name hot-path \
1640 over `{name}(&self).fetch(&pool)`. Use this when \
1641 you don't need further `.filter()` / `.order_by()` \
1642 composition; falls back to the chainable accessor when \
1643 you do. Avoids the `_pool` suffix on the most common \
1644 call-site shape.",
1645 name = rel.name,
1646 );
1647 quote! {
1648 #[doc = #accessor_doc]
1649 pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1650 #root::query::QuerySet::<#child>::new()
1651 .filter(#child_fk_column, self.__rustango_pk_value())
1652 }
1653
1654 #[doc = #fetch_doc]
1655 pub async fn #fetch_ident(
1656 &self,
1657 pool: &#root::sql::Pool,
1658 ) -> ::core::result::Result<
1659 ::std::vec::Vec<#child>,
1660 #root::sql::ExecError,
1661 > {
1662 use #root::sql::FetcherPool as _;
1663 self.#accessor_ident().fetch(pool).await
1664 }
1665
1666 /// Eloquent `$model->relation->first()` / `hasOne`
1667 /// semantics — bare-name shortcut over
1668 /// `self.<name>().first(&pool)`. Returns `None` when no
1669 /// child rows match. Useful when the relation is
1670 /// nominally many-to-one in shape but at most one row is
1671 /// expected (latest comment, primary tag, etc.).
1672 pub async fn #first_ident(
1673 &self,
1674 pool: &#root::sql::Pool,
1675 ) -> ::core::result::Result<
1676 ::core::option::Option<#child>,
1677 #root::sql::ExecError,
1678 > {
1679 self.#accessor_ident().first(pool).await
1680 }
1681
1682 /// Eloquent `$model->relation->pluck($col)` — project a
1683 /// single column from the child rows into a `Vec<U>`.
1684 /// Skips the typed `Child` decode, which is cheaper when
1685 /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1686 pub async fn #pluck_ident<U>(
1687 &self,
1688 col: &'static str,
1689 pool: &#root::sql::Pool,
1690 ) -> ::core::result::Result<
1691 ::std::vec::Vec<U>,
1692 #root::sql::ExecError,
1693 >
1694 where
1695 U: #root::sql::MaybePgScalar
1696 + #root::sql::MaybeMyScalar
1697 + #root::sql::MaybeSqliteScalar
1698 + ::core::marker::Send
1699 + ::core::marker::Unpin,
1700 {
1701 self.#accessor_ident()
1702 .values_list_flat(col)
1703 .fetch::<U>(pool)
1704 .await
1705 }
1706
1707 #[doc = #exists_doc]
1708 pub fn #exists_ident() -> #root::core::WhereExpr {
1709 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1710 let child_schema =
1711 <#child as #root::core::Model>::SCHEMA;
1712 let inner = SelectQuery {
1713 where_clause: WhereExpr::ExprCompare {
1714 lhs: Expr::Column(#child_fk_column),
1715 op: Op::Eq,
1716 rhs: Expr::OuterRef(#self_pk_column),
1717 },
1718 ..SelectQuery::new(child_schema)
1719 };
1720 WhereExpr::Exists(::std::boxed::Box::new(inner))
1721 }
1722
1723 #[doc = #not_exists_doc]
1724 pub fn #not_exists_ident() -> #root::core::WhereExpr {
1725 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1726 let child_schema =
1727 <#child as #root::core::Model>::SCHEMA;
1728 let inner = SelectQuery {
1729 where_clause: WhereExpr::ExprCompare {
1730 lhs: Expr::Column(#child_fk_column),
1731 op: Op::Eq,
1732 rhs: Expr::OuterRef(#self_pk_column),
1733 },
1734 ..SelectQuery::new(child_schema)
1735 };
1736 WhereExpr::NotExists(::std::boxed::Box::new(inner))
1737 }
1738
1739 #[doc = #count_doc]
1740 pub async fn #count_ident(
1741 &self,
1742 pool: &#root::sql::Pool,
1743 ) -> ::core::result::Result<
1744 i64,
1745 #root::sql::ExecError,
1746 > {
1747 use #root::sql::CounterPool as _;
1748 #root::query::QuerySet::<#child>::new()
1749 .filter(#child_fk_column, self.__rustango_pk_value())
1750 .count(pool)
1751 .await
1752 }
1753 }
1754 });
1755 quote! {
1756 impl #struct_name {
1757 #( #methods )*
1758 }
1759 }
1760}
1761
1762/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1763/// `#[rustango(through(...))]` attribute. Issue #817.
1764///
1765/// Each accessor builds a correlated subquery via
1766/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1767/// intermediate table, filters on the FK column pointing at this
1768/// model, and projects the intermediate PK. The outer queryset
1769/// filters the far model by its FK-to-intermediate column being in
1770/// that set.
1771///
1772/// The returned `QuerySet<Far>` is **chainable** — the subquery
1773/// lives inside a `where_raw` clause so the user's later
1774/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1775/// disturb it.
1776///
1777/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1778/// SQLite — no LATERAL or backend-specific syntax involved.
1779fn through_accessor_tokens(
1780 struct_name: &syn::Ident,
1781 through_relations: &[ThroughAttr],
1782) -> TokenStream2 {
1783 let root = rustango_root();
1784 if through_relations.is_empty() {
1785 return TokenStream2::new();
1786 }
1787 let methods = through_relations.iter().map(|rel| {
1788 let method_name = format!("{}_through", rel.name);
1789 let count_name = format!("{}_through_count", rel.name);
1790 let fetch_name = format!("{}_through_fetch", rel.name);
1791 let first_name = format!("{}_through_first", rel.name);
1792 let pluck_name = format!("{}_through_pluck", rel.name);
1793 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1794 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1795 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1796 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1797 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1798 let far = &rel.far;
1799 let intermediate = &rel.intermediate;
1800 let far_fk_column = rel.far_fk_column.as_str();
1801 let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1802 let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1803 let doc = format!(
1804 "Eloquent `hasManyThrough` accessor — returns a \
1805 `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1806 instance through the intermediate `{intermediate}` table. \
1807 Generated SQL shape: \
1808 `… WHERE {far_fk_column} IN (SELECT \
1809 {intermediate_pk_column} FROM <{intermediate}> WHERE \
1810 {intermediate_fk_column} = self.pk)`. Chainable like any \
1811 other QuerySet — compose `.filter()` / `.order_by()` / \
1812 `.limit()` etc. on top.",
1813 );
1814 let count_doc = format!(
1815 "Eloquent `$model->{name}->count()` analog for the \
1816 through-relation — returns the number of `{far}` rows \
1817 reachable through `{intermediate}`. Equivalent to \
1818 `self.{name}_through().count(pool)` but spelled \
1819 as a bare instance method for parity with the \
1820 `reverse_has` `<name>_count` shape.",
1821 name = rel.name,
1822 );
1823 quote! {
1824 #[doc = #doc]
1825 pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1826 use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1827 let intermediate_schema =
1828 <#intermediate as #root::core::Model>::SCHEMA;
1829 let sub = SelectQuery {
1830 where_clause: WhereExpr::Predicate(Filter {
1831 column: #intermediate_fk_column,
1832 op: Op::Eq,
1833 value: self.__rustango_pk_value(),
1834 }),
1835 projection: ::core::option::Option::Some(
1836 ::std::vec![#intermediate_pk_column],
1837 ),
1838 ..SelectQuery::new(intermediate_schema)
1839 };
1840 #root::query::QuerySet::<#far>::new().where_raw(
1841 WhereExpr::InSubquery {
1842 column: #far_fk_column,
1843 negated: false,
1844 subquery: ::std::boxed::Box::new(sub),
1845 },
1846 )
1847 }
1848
1849 #[doc = #count_doc]
1850 pub async fn #count_ident(
1851 &self,
1852 pool: &#root::sql::Pool,
1853 ) -> ::core::result::Result<
1854 i64,
1855 #root::sql::ExecError,
1856 > {
1857 use #root::sql::CounterPool as _;
1858 self.#method_ident().count(pool).await
1859 }
1860
1861 /// Eloquent `$model->relation->get()` for the
1862 /// through-relation — bare-name hot-path over
1863 /// `self.<name>_through().fetch(&pool)`. Use this
1864 /// when you don't need further `.filter()` /
1865 /// `.order_by()` composition; falls back to the
1866 /// chainable accessor when you do. Avoids the `_pool`
1867 /// suffix on the most common call-site shape.
1868 pub async fn #fetch_ident(
1869 &self,
1870 pool: &#root::sql::Pool,
1871 ) -> ::core::result::Result<
1872 ::std::vec::Vec<#far>,
1873 #root::sql::ExecError,
1874 > {
1875 use #root::sql::FetcherPool as _;
1876 self.#method_ident().fetch(pool).await
1877 }
1878
1879 /// Eloquent `hasOneThrough` analog — bare-name shortcut
1880 /// over `self.<name>_through().first(&pool)`. Returns
1881 /// `None` when no far rows are reachable through the
1882 /// intermediate. Useful when at most one row is
1883 /// expected (latest comment by country, primary tag,
1884 /// etc.).
1885 pub async fn #first_ident(
1886 &self,
1887 pool: &#root::sql::Pool,
1888 ) -> ::core::result::Result<
1889 ::core::option::Option<#far>,
1890 #root::sql::ExecError,
1891 > {
1892 self.#method_ident().first(pool).await
1893 }
1894
1895 /// Pluck a single column from the far rows into `Vec<U>`
1896 /// — cheaper than the typed `<Far>` decode when only one
1897 /// scalar column is needed.
1898 pub async fn #pluck_ident<U>(
1899 &self,
1900 col: &'static str,
1901 pool: &#root::sql::Pool,
1902 ) -> ::core::result::Result<
1903 ::std::vec::Vec<U>,
1904 #root::sql::ExecError,
1905 >
1906 where
1907 U: #root::sql::MaybePgScalar
1908 + #root::sql::MaybeMyScalar
1909 + #root::sql::MaybeSqliteScalar
1910 + ::core::marker::Send
1911 + ::core::marker::Unpin,
1912 {
1913 self.#method_ident()
1914 .values_list_flat(col)
1915 .fetch::<U>(pool)
1916 .await
1917 }
1918 }
1919 });
1920 quote! {
1921 impl #struct_name {
1922 #( #methods )*
1923 }
1924 }
1925}
1926
1927struct ColumnEntry {
1928 /// The struct field ident, used both for the inherent const name on
1929 /// the model and for the inner column type's name.
1930 ident: syn::Ident,
1931 /// The struct's field type, used as `Column::Value`.
1932 value_ty: Type,
1933 /// Rust-side field name (e.g. `"id"`).
1934 name: String,
1935 /// SQL-side column name (e.g. `"user_id"`).
1936 column: String,
1937 /// `#root::core::FieldType::I64` etc.
1938 field_type_tokens: TokenStream2,
1939}
1940
1941struct CollectedFields {
1942 field_schemas: Vec<TokenStream2>,
1943 from_row_inits: Vec<TokenStream2>,
1944 /// Aliased counterparts of `from_row_inits` — read columns via
1945 /// `format!("{prefix}__{col}")` aliases so a Model can be
1946 /// decoded from a JOINed row's projected target columns.
1947 from_aliased_row_inits: Vec<TokenStream2>,
1948 /// Static column-name list — used by the simple insert path
1949 /// (no `Auto<T>` fields). Aligned with `insert_values`.
1950 insert_columns: Vec<TokenStream2>,
1951 /// Static `Into<SqlValue>` expressions, one per field. Aligned
1952 /// with `insert_columns`. Used by the simple insert path only.
1953 insert_values: Vec<TokenStream2>,
1954 /// Per-field push expressions for the dynamic (Auto-aware)
1955 /// insert path. Each statement either unconditionally pushes
1956 /// `(column, value)` or, for an `Auto<T>` field, conditionally
1957 /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
1958 insert_pushes: Vec<TokenStream2>,
1959 /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
1960 /// when `has_auto == false`.
1961 returning_cols: Vec<TokenStream2>,
1962 /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
1963 /// field. Run after `insert_returning` to populate the model.
1964 auto_assigns: Vec<TokenStream2>,
1965 /// `(ident, column_literal)` pairs for every Auto field. Used by
1966 /// the bulk_insert codegen to rebuild assigns against `_row_mut`
1967 /// instead of `self`.
1968 auto_field_idents: Vec<(syn::Ident, String)>,
1969 /// #1028 — `(ident, column)` for each `generated_as` field. Drives
1970 /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
1971 /// back into the struct after insert (MySQL has no RETURNING → the
1972 /// field stays at its placeholder, deferred, matching Django 6.0).
1973 generated_field_idents: Vec<(syn::Ident, String)>,
1974 /// Inner `T` of the first `Auto<T>` field, for the MySQL
1975 /// `LAST_INSERT_ID()` assignment in `AssignAutoPkPool`.
1976 first_auto_value_ty: Option<Type>,
1977 /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
1978 /// by the all-Auto-Unset bulk path (Auto cols dropped from
1979 /// `columns`).
1980 bulk_pushes_no_auto: Vec<TokenStream2>,
1981 /// Bulk-insert per-row pushes for **all fields including Auto**.
1982 /// Used by the all-Auto-Set bulk path (Auto col included with the
1983 /// caller-supplied value).
1984 bulk_pushes_all: Vec<TokenStream2>,
1985 /// Column-name literals for non-Auto fields only (paired with
1986 /// `bulk_pushes_no_auto`).
1987 bulk_columns_no_auto: Vec<TokenStream2>,
1988 /// Column-name literals for every field including Auto (paired
1989 /// with `bulk_pushes_all`).
1990 bulk_columns_all: Vec<TokenStream2>,
1991 /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
1992 /// + the loop that asserts every row matches. One pair per Auto
1993 /// field. Empty when `has_auto == false`.
1994 bulk_auto_uniformity: Vec<TokenStream2>,
1995 /// Identifier of the first Auto field, used as the witness for
1996 /// "all rows agree on Set vs Unset". Set only when `has_auto`.
1997 first_auto_ident: Option<syn::Ident>,
1998 /// `true` if any field on the struct is `Auto<T>`.
1999 has_auto: bool,
2000 /// `true` when the primary-key field's Rust type is `Auto<T>`.
2001 /// Gates `save()` codegen — only Auto PKs let us infer
2002 /// insert-vs-update from the in-memory value.
2003 pk_is_auto: bool,
2004 /// `Assignment` constructors for every non-PK column. Drives the
2005 /// UPDATE branch of `save()`.
2006 update_assignments: Vec<TokenStream2>,
2007 /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2008 /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2009 upsert_update_columns: Vec<TokenStream2>,
2010 primary_key: Option<(syn::Ident, String)>,
2011 column_entries: Vec<ColumnEntry>,
2012 /// Rust-side field names, in declaration order. Used to validate
2013 /// container attributes like `display = "…"`.
2014 field_names: Vec<String>,
2015 /// FK fields on this child model. Drives the reverse-relation
2016 /// helper emit — for each FK, the macro adds an inherent
2017 /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2018 /// method on the parent type.
2019 fk_relations: Vec<FkRelation>,
2020 /// SQL column name of the `#[rustango(soft_delete)]` field, if
2021 /// the model has one. Drives emission of the `soft_delete_on` /
2022 /// `restore_on` inherent methods. At most one such column per
2023 /// model is allowed; collect_fields rejects duplicates.
2024 soft_delete_column: Option<String>,
2025 /// Rust field ident of the `#[rustango(soft_delete)]` field —
2026 /// companion to `soft_delete_column` for emitting predicates
2027 /// that need to read the field off `&self` (e.g. `trashed()`).
2028 soft_delete_field_ident: Option<syn::Ident>,
2029}
2030
2031#[derive(Clone)]
2032struct FkRelation {
2033 /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2034 /// helper is emitted as `impl <ParentType> { … }`.
2035 parent_type: Type,
2036 /// SQL column name on the child table for this FK (e.g. `"author"`).
2037 /// Used in the generated `WHERE <fk_column> = $1` clause.
2038 fk_column: String,
2039 /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2040 /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2041 /// default `ForeignKey<T>` (no explicit K); other kinds when the
2042 /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2043 pk_kind: DetectedKind,
2044 /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2045 /// FK column). Drives the `Some(...)` wrapping in load_related
2046 /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2047 /// the codegen matches the field's declared shape.
2048 nullable: bool,
2049 /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2050 /// override. When set, the reverse helper picks this name instead
2051 /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2052 /// #816 (issue's "Related" note re: per-FK override).
2053 related_name: Option<String>,
2054}
2055
2056fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2057 let root = rustango_root();
2058 let cap = named.named.len();
2059 let mut out = CollectedFields {
2060 field_schemas: Vec::with_capacity(cap),
2061 from_row_inits: Vec::with_capacity(cap),
2062 from_aliased_row_inits: Vec::with_capacity(cap),
2063 insert_columns: Vec::with_capacity(cap),
2064 insert_values: Vec::with_capacity(cap),
2065 insert_pushes: Vec::with_capacity(cap),
2066 returning_cols: Vec::new(),
2067 auto_assigns: Vec::new(),
2068 auto_field_idents: Vec::new(),
2069 generated_field_idents: Vec::new(),
2070 first_auto_value_ty: None,
2071 bulk_pushes_no_auto: Vec::with_capacity(cap),
2072 bulk_pushes_all: Vec::with_capacity(cap),
2073 bulk_columns_no_auto: Vec::with_capacity(cap),
2074 bulk_columns_all: Vec::with_capacity(cap),
2075 bulk_auto_uniformity: Vec::new(),
2076 first_auto_ident: None,
2077 has_auto: false,
2078 pk_is_auto: false,
2079 update_assignments: Vec::with_capacity(cap),
2080 upsert_update_columns: Vec::with_capacity(cap),
2081 primary_key: None,
2082 column_entries: Vec::with_capacity(cap),
2083 field_names: Vec::with_capacity(cap),
2084 fk_relations: Vec::new(),
2085 soft_delete_column: None,
2086 soft_delete_field_ident: None,
2087 };
2088
2089 for field in &named.named {
2090 let info = process_field(field, table)?;
2091 out.field_names.push(info.ident.to_string());
2092 out.field_schemas.push(info.schema);
2093 out.from_row_inits.push(info.from_row_init);
2094 out.from_aliased_row_inits.push(info.from_aliased_row_init);
2095 if let Some(parent_ty) = info.fk_inner.clone() {
2096 out.fk_relations.push(FkRelation {
2097 parent_type: parent_ty,
2098 fk_column: info.column.clone(),
2099 pk_kind: info.fk_pk_kind,
2100 nullable: info.nullable,
2101 related_name: info.related_name.clone(),
2102 });
2103 }
2104 if info.soft_delete {
2105 if out.soft_delete_column.is_some() {
2106 return Err(syn::Error::new_spanned(
2107 field,
2108 "only one field may be marked `#[rustango(soft_delete)]`",
2109 ));
2110 }
2111 out.soft_delete_column = Some(info.column.clone());
2112 out.soft_delete_field_ident = Some(info.ident.clone());
2113 }
2114 let column = info.column.as_str();
2115 let ident = info.ident;
2116 // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2117 // skip every write path — Postgres recomputes the value
2118 // from EXPR. Push only the column-entry record (so typed
2119 // column constants still exist for filtering / projection)
2120 // and the schema literal (already pushed above) and move
2121 // on. No insert_columns/values, no insert_pushes, no
2122 // bulk_*, no update_assignments, no upsert_update_columns,
2123 // no returning_cols.
2124 if info.generated_as.is_some() {
2125 out.column_entries.push(ColumnEntry {
2126 ident: ident.clone(),
2127 value_ty: info.value_ty.clone(),
2128 name: ident.to_string(),
2129 column: info.column.clone(),
2130 field_type_tokens: info.field_type_tokens,
2131 });
2132 // #1028 — refresh the DB-computed value after insert on
2133 // RETURNING-capable backends. The column joins `returning_cols`
2134 // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2135 // ident is recorded so the `AssignAutoPkPool` impl decodes it
2136 // back into the struct. MySQL has no `INSERT … RETURNING`, so
2137 // it keeps the placeholder (deferred refresh, matching Django).
2138 out.returning_cols.push(quote!(#column));
2139 out.generated_field_idents
2140 .push((ident.clone(), info.column.clone()));
2141 continue;
2142 }
2143 out.insert_columns.push(quote!(#column));
2144 out.insert_values.push(quote! {
2145 ::core::convert::Into::<#root::core::SqlValue>::into(
2146 ::core::clone::Clone::clone(&self.#ident)
2147 )
2148 });
2149 if info.auto {
2150 out.has_auto = true;
2151 if out.first_auto_ident.is_none() {
2152 out.first_auto_ident = Some(ident.clone());
2153 out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
2154 }
2155 // `default_uuid_v7` (issue #823) generates the PK Rust-side
2156 // before binding, so the value is already in
2157 // `self.#ident` after the insert_push — RETURNING is
2158 // redundant. Skip adding this column to returning_cols /
2159 // auto_assigns / auto_field_idents to avoid (a) an
2160 // unnecessary RETURNING column on every dialect, and (b)
2161 // the MySQL `LAST_INSERT_ID()` path that can only fill an
2162 // integer PK.
2163 if !info.default_uuid_v7 {
2164 out.returning_cols.push(quote!(#column));
2165 out.auto_field_idents
2166 .push((ident.clone(), info.column.clone()));
2167 out.auto_assigns.push(quote! {
2168 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2169 });
2170 }
2171 if info.default_uuid_v7 {
2172 // Rust-side UUIDv7 generation (issue #823, Eloquent
2173 // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2174 // and bind; Auto::Set → bind the user's value. The
2175 // column is ALWAYS present in the INSERT statement —
2176 // no RETURNING / no DB DEFAULT needed.
2177 out.insert_pushes.push(quote! {
2178 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2179 self.#ident = #root::sql::Auto::Set(
2180 #root::__uuid::Uuid::now_v7(),
2181 );
2182 }
2183 if let #root::sql::Auto::Set(_v) = &self.#ident {
2184 _columns.push(#column);
2185 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2186 ::core::clone::Clone::clone(_v)
2187 ));
2188 }
2189 });
2190 } else {
2191 out.insert_pushes.push(quote! {
2192 if let #root::sql::Auto::Set(_v) = &self.#ident {
2193 _columns.push(#column);
2194 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2195 ::core::clone::Clone::clone(_v)
2196 ));
2197 }
2198 });
2199 }
2200 // Bulk: Auto fields appear only in the all-Set path,
2201 // never in the Unset path (we drop them from `columns`).
2202 out.bulk_columns_all.push(quote!(#column));
2203 out.bulk_pushes_all.push(quote! {
2204 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2205 ::core::clone::Clone::clone(&_row.#ident)
2206 ));
2207 });
2208 // Uniformity check: every row's Auto state must match the
2209 // first row's. Mixed Set/Unset within one bulk_insert is
2210 // rejected here so the column list stays consistent.
2211 let ident_clone = ident.clone();
2212 out.bulk_auto_uniformity.push(quote! {
2213 for _r in rows.iter().skip(1) {
2214 if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2215 return ::core::result::Result::Err(
2216 #root::sql::ExecError::Sql(
2217 #root::sql::SqlError::BulkAutoMixed
2218 )
2219 );
2220 }
2221 }
2222 });
2223 } else {
2224 out.insert_pushes.push(quote! {
2225 _columns.push(#column);
2226 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2227 ::core::clone::Clone::clone(&self.#ident)
2228 ));
2229 });
2230 // Bulk: non-Auto fields appear in BOTH paths.
2231 out.bulk_columns_no_auto.push(quote!(#column));
2232 out.bulk_columns_all.push(quote!(#column));
2233 let push_expr = quote! {
2234 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2235 ::core::clone::Clone::clone(&_row.#ident)
2236 ));
2237 };
2238 out.bulk_pushes_no_auto.push(push_expr.clone());
2239 out.bulk_pushes_all.push(push_expr);
2240 }
2241 if info.primary_key {
2242 if out.primary_key.is_some() {
2243 return Err(syn::Error::new_spanned(
2244 field,
2245 "only one field may be marked `#[rustango(primary_key)]`",
2246 ));
2247 }
2248 out.primary_key = Some((ident.clone(), info.column.clone()));
2249 if info.auto {
2250 out.pk_is_auto = true;
2251 }
2252 } else if info.auto_now_add {
2253 // Immutable post-insert: skip from UPDATE entirely.
2254 } else if info.auto_now {
2255 // `auto_now` columns: bind `chrono::Utc::now()` on every
2256 // UPDATE so the column is always overridden with the
2257 // wall-clock at write time, regardless of what value the
2258 // user left in the struct field.
2259 out.update_assignments.push(quote! {
2260 #root::core::Assignment {
2261 column: #column,
2262 value: ::core::convert::Into::<#root::core::Expr>::into(
2263 ::core::convert::Into::<#root::core::SqlValue>::into(
2264 #root::__chrono::Utc::now()
2265 )
2266 ),
2267 }
2268 });
2269 out.upsert_update_columns.push(quote!(#column));
2270 } else {
2271 out.update_assignments.push(quote! {
2272 #root::core::Assignment {
2273 column: #column,
2274 value: ::core::convert::Into::<#root::core::Expr>::into(
2275 ::core::convert::Into::<#root::core::SqlValue>::into(
2276 ::core::clone::Clone::clone(&self.#ident)
2277 )
2278 ),
2279 }
2280 });
2281 out.upsert_update_columns.push(quote!(#column));
2282 }
2283 out.column_entries.push(ColumnEntry {
2284 ident: ident.clone(),
2285 value_ty: info.value_ty.clone(),
2286 name: ident.to_string(),
2287 column: info.column.clone(),
2288 field_type_tokens: info.field_type_tokens,
2289 });
2290 }
2291 Ok(out)
2292}
2293
2294fn model_impl_tokens(
2295 struct_name: &syn::Ident,
2296 model_name: &str,
2297 table: &str,
2298 display: Option<&str>,
2299 app_label: Option<&str>,
2300 admin: Option<&AdminAttrs>,
2301 default_order: &[(String, bool, proc_macro2::Span)],
2302 field_schemas: &[TokenStream2],
2303 soft_delete_column: Option<&str>,
2304 permissions: bool,
2305 audit_track: Option<&[String]>,
2306 m2m_relations: &[M2MAttr],
2307 indexes: &[IndexAttr],
2308 checks: &[CheckAttr],
2309 excludes: &[ExcludeAttr],
2310 composite_fks: &[CompositeFkAttr],
2311 generic_fks: &[GenericFkAttr],
2312 scope: Option<&str>,
2313 is_view: bool,
2314 verbose_name: Option<&str>,
2315 verbose_name_plural: Option<&str>,
2316 managed: bool,
2317 base_manager_name: Option<&str>,
2318 order_with_respect_to: Option<&str>,
2319 proxy: bool,
2320 required_db_features: &[String],
2321 required_db_vendor: Option<&str>,
2322 default_related_name: Option<&str>,
2323 db_table_comment: Option<&str>,
2324 get_latest_by: Option<(&str, bool)>,
2325 extra_permissions: &[(String, String)],
2326 default_permissions: &[String],
2327 global_scopes: &[GlobalScopeAttr],
2328 reverse_has_relations: &[ReverseHasAttr],
2329 generic_has_relations: &[GenericHasAttr],
2330) -> TokenStream2 {
2331 let root = rustango_root();
2332 let display_tokens = if let Some(name) = display {
2333 quote!(::core::option::Option::Some(#name))
2334 } else {
2335 quote!(::core::option::Option::None)
2336 };
2337 let app_label_tokens = if let Some(name) = app_label {
2338 quote!(::core::option::Option::Some(#name))
2339 } else {
2340 quote!(::core::option::Option::None)
2341 };
2342 let soft_delete_tokens = if let Some(col) = soft_delete_column {
2343 quote!(::core::option::Option::Some(#col))
2344 } else {
2345 quote!(::core::option::Option::None)
2346 };
2347 let audit_track_tokens = match audit_track {
2348 None => quote!(::core::option::Option::None),
2349 Some(names) => {
2350 let lits = names.iter().map(|n| n.as_str());
2351 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2352 }
2353 };
2354 let admin_tokens = admin_config_tokens(admin);
2355 // Default `tenant` so single-tenant projects (no `scope` attr
2356 // anywhere) keep the v0.24.x behavior. Container-attr parser
2357 // already validated the value is "registry" or "tenant".
2358 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2359 Some("registry") => quote!(#root::core::ModelScope::Registry),
2360 _ => quote!(#root::core::ModelScope::Tenant),
2361 };
2362 let verbose_name_tokens = optional_str(verbose_name);
2363 let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2364 let base_manager_name_tokens = optional_str(base_manager_name);
2365 let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2366 let required_db_features_lits: Vec<&str> =
2367 required_db_features.iter().map(String::as_str).collect();
2368 let required_db_vendor_tokens = optional_str(required_db_vendor);
2369 let default_related_name_tokens = optional_str(default_related_name);
2370 let db_table_comment_tokens = optional_str(db_table_comment);
2371 let get_latest_by_tokens = match get_latest_by {
2372 Some((col, desc)) => {
2373 quote!(::core::option::Option::Some((#col, #desc)))
2374 }
2375 None => quote!(::core::option::Option::None),
2376 };
2377 let extra_permission_tokens: Vec<_> = extra_permissions
2378 .iter()
2379 .map(|(c, l)| quote!((#c, #l)))
2380 .collect();
2381 let default_permission_tokens: Vec<_> = default_permissions
2382 .iter()
2383 .map(|action| quote!(#action))
2384 .collect();
2385 let indexes_tokens = indexes.iter().map(|idx| {
2386 // When no explicit `name = "..."` was given, derive a stable,
2387 // collision-free name from the table + columns (Django-shape
2388 // `<table>_<col>_<col>_idx`) instead of a shared literal that
2389 // would clash the moment a model declares two unnamed indexes.
2390 // Capped at Postgres's 63-char identifier limit.
2391 let derived_name = idx.name.clone().unwrap_or_else(|| {
2392 let mut n = format!("{table}_{}_idx", idx.columns.join("_"));
2393 if n.len() > 63 {
2394 n.truncate(63);
2395 }
2396 n
2397 });
2398 let name = derived_name.as_str();
2399 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2400 let unique = idx.unique;
2401 // Map the parsed method string onto the IndexMethod enum
2402 // variant — kept at the codegen layer so the IR doesn't
2403 // carry the string form.
2404 let method_variant = match idx.method.as_str() {
2405 "gin" => quote!(#root::core::IndexMethod::Gin),
2406 "gist" => quote!(#root::core::IndexMethod::Gist),
2407 "brin" => quote!(#root::core::IndexMethod::Brin),
2408 "spgist" => quote!(#root::core::IndexMethod::SpGist),
2409 "hash" => quote!(#root::core::IndexMethod::Hash),
2410 "bloom" => quote!(#root::core::IndexMethod::Bloom),
2411 _ => quote!(#root::core::IndexMethod::BTree),
2412 };
2413 let where_clause = match &idx.where_clause {
2414 Some(s) => quote!(::core::option::Option::Some(#s)),
2415 None => quote!(::core::option::Option::None),
2416 };
2417 let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2418 quote! {
2419 #root::core::IndexSchema {
2420 name: #name,
2421 columns: &[ #(#cols),* ],
2422 unique: #unique,
2423 method: #method_variant,
2424 where_clause: #where_clause,
2425 include: &[ #(#include_lits),* ],
2426 }
2427 }
2428 });
2429 let checks_tokens = checks.iter().map(|c| {
2430 let name = c.name.as_str();
2431 let expr = c.expr.as_str();
2432 quote! {
2433 #root::core::CheckConstraint {
2434 name: #name,
2435 expr: #expr,
2436 }
2437 }
2438 });
2439 let excludes_tokens = excludes.iter().map(|e| {
2440 let name = e.name.as_str();
2441 let using = e.using.as_str();
2442 let element_tokens = e.elements.iter().map(|(col, op)| {
2443 let col_s = col.as_str();
2444 let op_s = op.as_str();
2445 quote!((#col_s, #op_s))
2446 });
2447 let where_tokens = match e.where_clause.as_deref() {
2448 Some(w) => quote!(::core::option::Option::Some(#w)),
2449 None => quote!(::core::option::Option::None),
2450 };
2451 quote! {
2452 #root::core::ExclusionConstraint {
2453 name: #name,
2454 using: #using,
2455 elements: &[ #(#element_tokens),* ],
2456 where_clause: #where_tokens,
2457 }
2458 }
2459 });
2460 let composite_fk_tokens = composite_fks.iter().map(|rel| {
2461 let name = rel.name.as_str();
2462 let to = rel.to.as_str();
2463 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2464 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2465 quote! {
2466 #root::core::CompositeFkRelation {
2467 name: #name,
2468 to: #to,
2469 from: &[ #(#from_cols),* ],
2470 on: &[ #(#on_cols),* ],
2471 }
2472 }
2473 });
2474 let generic_fk_tokens = generic_fks.iter().map(|rel| {
2475 let name = rel.name.as_str();
2476 let ct_col = rel.ct_column.as_str();
2477 let pk_col = rel.pk_column.as_str();
2478 quote! {
2479 #root::core::GenericRelation {
2480 name: #name,
2481 ct_column: #ct_col,
2482 pk_column: #pk_col,
2483 }
2484 }
2485 });
2486 // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2487 // no `#[rustango(default_order = "...")]` attribute was supplied.
2488 let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2489 let col_lit = col.as_str();
2490 quote! { (#col_lit, #desc) }
2491 });
2492
2493 // Issue #820 — `global_scopes` slice literal. Empty when no
2494 // `#[rustango(global_scope(...))]` attribute was supplied. The
2495 // `apply` path is re-emitted verbatim so it resolves in the
2496 // consumer's scope; the name is stored as a string literal.
2497 let global_scope_tokens = global_scopes.iter().map(|s| {
2498 let name = s.name.as_str();
2499 let apply = &s.apply;
2500 quote! {
2501 #root::core::GlobalScope {
2502 name: #name,
2503 apply: #apply,
2504 }
2505 }
2506 });
2507
2508 let m2m_tokens = m2m_relations.iter().map(|rel| {
2509 let name = rel.name.as_str();
2510 let to = rel.to.as_str();
2511 let through = rel.through.as_str();
2512 let src = rel.src.as_str();
2513 let dst = rel.dst.as_str();
2514 let auto_create = rel.auto_create;
2515 quote! {
2516 #root::core::M2MRelation {
2517 name: #name,
2518 to: #to,
2519 through: #through,
2520 src_col: #src,
2521 dst_col: #dst,
2522 auto_create: #auto_create,
2523 }
2524 }
2525 });
2526 // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2527 // when the model declares `#[rustango(reverse_has(...))]`. Each
2528 // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2529 // a const expression. Models without reverse_has fall through to
2530 // the trait's empty default — no override emitted.
2531 let reverse_relations_override = if reverse_has_relations.is_empty() {
2532 quote!()
2533 } else {
2534 let entries = reverse_has_relations.iter().map(|rel| {
2535 let name = rel.name.as_str();
2536 let child = &rel.child;
2537 let child_fk_column = rel.child_fk_column.as_str();
2538 let self_pk_column = rel.self_pk_column.as_str();
2539 quote! {
2540 #root::core::ReverseRelation {
2541 name: #name,
2542 child_schema: <#child as #root::core::Model>::SCHEMA,
2543 child_fk_column: #child_fk_column,
2544 self_pk_column: #self_pk_column,
2545 }
2546 }
2547 });
2548 quote! {
2549 fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2550 const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2551 RELS
2552 }
2553 }
2554 };
2555 // Issue #830 — `Model::generic_reverse_relations()` override for
2556 // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2557 let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2558 quote!()
2559 } else {
2560 let entries = generic_has_relations.iter().map(|rel| {
2561 let name = rel.name.as_str();
2562 let child = &rel.child;
2563 let ct_column = rel.ct_column.as_str();
2564 let pk_column = rel.pk_column.as_str();
2565 let self_pk_column = rel.self_pk_column.as_str();
2566 quote! {
2567 #root::core::GenericReverseRelation {
2568 name: #name,
2569 child_schema: <#child as #root::core::Model>::SCHEMA,
2570 ct_column: #ct_column,
2571 pk_column: #pk_column,
2572 self_pk_column: #self_pk_column,
2573 }
2574 }
2575 });
2576 quote! {
2577 fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2578 const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2579 RELS
2580 }
2581 }
2582 };
2583 quote! {
2584 impl #root::core::Model for #struct_name {
2585 const SCHEMA: &'static #root::core::ModelSchema = &#root::core::ModelSchema {
2586 name: #model_name,
2587 table: #table,
2588 fields: &[ #(#field_schemas),* ],
2589 display: #display_tokens,
2590 app_label: #app_label_tokens,
2591 admin: #admin_tokens,
2592 soft_delete_column: #soft_delete_tokens,
2593 permissions: #permissions,
2594 audit_track: #audit_track_tokens,
2595 m2m: &[ #(#m2m_tokens),* ],
2596 indexes: &[ #(#indexes_tokens),* ],
2597 check_constraints: &[ #(#checks_tokens),* ],
2598 exclusion_constraints: &[ #(#excludes_tokens),* ],
2599 composite_relations: &[ #(#composite_fk_tokens),* ],
2600 generic_relations: &[ #(#generic_fk_tokens),* ],
2601 scope: #scope_tokens,
2602 default_order: &[ #(#default_order_tokens),* ],
2603 is_view: #is_view,
2604 verbose_name: #verbose_name_tokens,
2605 verbose_name_plural: #verbose_name_plural_tokens,
2606 managed: #managed,
2607 base_manager_name: #base_manager_name_tokens,
2608 order_with_respect_to: #order_with_respect_to_tokens,
2609 proxy: #proxy,
2610 required_db_features: &[ #(#required_db_features_lits),* ],
2611 required_db_vendor: #required_db_vendor_tokens,
2612 default_related_name: #default_related_name_tokens,
2613 db_table_comment: #db_table_comment_tokens,
2614 get_latest_by: #get_latest_by_tokens,
2615 extra_permissions: &[ #(#extra_permission_tokens),* ],
2616 default_permissions: &[ #(#default_permission_tokens),* ],
2617 global_scopes: &[ #(#global_scope_tokens),* ],
2618 };
2619
2620 #reverse_relations_override
2621 #generic_reverse_relations_override
2622 }
2623 }
2624}
2625
2626/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2627/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2628/// otherwise a static reference to a populated `AdminConfig`.
2629fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2630 let root = rustango_root();
2631 let Some(admin) = admin else {
2632 return quote!(::core::option::Option::None);
2633 };
2634
2635 let list_display = admin
2636 .list_display
2637 .as_ref()
2638 .map(|(v, _)| v.as_slice())
2639 .unwrap_or(&[]);
2640 let list_display_lits = list_display.iter().map(|s| s.as_str());
2641
2642 let search_fields = admin
2643 .search_fields
2644 .as_ref()
2645 .map(|(v, _)| v.as_slice())
2646 .unwrap_or(&[]);
2647 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2648
2649 let readonly_fields = admin
2650 .readonly_fields
2651 .as_ref()
2652 .map(|(v, _)| v.as_slice())
2653 .unwrap_or(&[]);
2654 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2655
2656 let list_filter = admin
2657 .list_filter
2658 .as_ref()
2659 .map(|(v, _)| v.as_slice())
2660 .unwrap_or(&[]);
2661 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2662
2663 let actions = admin
2664 .actions
2665 .as_ref()
2666 .map(|(v, _)| v.as_slice())
2667 .unwrap_or(&[]);
2668 let actions_lits = actions.iter().map(|s| s.as_str());
2669
2670 let fieldsets = admin
2671 .fieldsets
2672 .as_ref()
2673 .map(|(v, _)| v.as_slice())
2674 .unwrap_or(&[]);
2675 let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
2676 let title = title.as_str();
2677 let field_lits = fields.iter().map(|s| s.as_str());
2678 quote!(#root::core::Fieldset {
2679 title: #title,
2680 fields: &[ #( #field_lits ),* ],
2681 })
2682 });
2683
2684 let list_display_links = admin
2685 .list_display_links
2686 .as_ref()
2687 .map(|(v, _)| v.as_slice())
2688 .unwrap_or(&[]);
2689 let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2690
2691 let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2692 let actions_on_top = admin.actions_on_top.unwrap_or(true);
2693 let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2694 let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2695
2696 let prepopulated = admin
2697 .prepopulated_fields
2698 .as_ref()
2699 .map(|(v, _)| v.as_slice())
2700 .unwrap_or(&[]);
2701 let prepopulated_tokens = prepopulated.iter().map(|(target, sources)| {
2702 let target = target.as_str();
2703 let source_lits = sources.iter().map(|s| s.as_str());
2704 quote!(#root::core::PrepopulatedField {
2705 target: #target,
2706 sources: &[ #( #source_lits ),* ],
2707 })
2708 });
2709
2710 let raw_id_fields = admin
2711 .raw_id_fields
2712 .as_ref()
2713 .map(|(v, _)| v.as_slice())
2714 .unwrap_or(&[]);
2715 let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2716
2717 let autocomplete_fields = admin
2718 .autocomplete_fields
2719 .as_ref()
2720 .map(|(v, _)| v.as_slice())
2721 .unwrap_or(&[]);
2722 let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
2723
2724 // #352 — list_select_related accepts "all" | "none" | "field, field, …".
2725 let list_select_related_tokens = match admin.list_select_related.as_deref() {
2726 None | Some("all") => quote!(#root::core::ListSelectRelated::All),
2727 Some("none") => quote!(#root::core::ListSelectRelated::None),
2728 Some(raw) => {
2729 let names: Vec<&str> = raw
2730 .split(',')
2731 .map(str::trim)
2732 .filter(|s| !s.is_empty())
2733 .collect();
2734 quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
2735 }
2736 };
2737
2738 // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
2739 // a Vec<(String, String)>. Empty / unset → no overrides.
2740 let formfield_pairs: Vec<(&str, &str)> = admin
2741 .formfield_overrides
2742 .as_ref()
2743 .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
2744 .unwrap_or_default();
2745 let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
2746 let field = *field;
2747 let widget = *widget;
2748 quote!((#field, #widget))
2749 });
2750
2751 let list_per_page = admin.list_per_page.unwrap_or(0);
2752
2753 let ordering_pairs = admin
2754 .ordering
2755 .as_ref()
2756 .map(|(v, _)| v.as_slice())
2757 .unwrap_or(&[]);
2758 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
2759 let name = name.as_str();
2760 let desc = *desc;
2761 quote!((#name, #desc))
2762 });
2763
2764 quote! {
2765 ::core::option::Option::Some(&#root::core::AdminConfig {
2766 list_display: &[ #( #list_display_lits ),* ],
2767 search_fields: &[ #( #search_fields_lits ),* ],
2768 list_per_page: #list_per_page,
2769 ordering: &[ #( #ordering_tokens ),* ],
2770 readonly_fields: &[ #( #readonly_fields_lits ),* ],
2771 list_filter: &[ #( #list_filter_lits ),* ],
2772 actions: &[ #( #actions_lits ),* ],
2773 fieldsets: &[ #( #fieldset_tokens ),* ],
2774 list_display_links: &[ #( #list_display_links_lits ),* ],
2775 search_help_text: #search_help_text,
2776 actions_on_top: #actions_on_top,
2777 actions_on_bottom: #actions_on_bottom,
2778 date_hierarchy: #date_hierarchy,
2779 prepopulated_fields: &[ #( #prepopulated_tokens ),* ],
2780 raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
2781 autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
2782 list_select_related: #list_select_related_tokens,
2783 formfield_overrides: &[ #( #formfield_tokens ),* ],
2784 })
2785 }
2786}
2787
2788fn inherent_impl_tokens(
2789 struct_name: &syn::Ident,
2790 fields: &CollectedFields,
2791 primary_key: Option<&(syn::Ident, String)>,
2792 column_consts: &TokenStream2,
2793 audited_fields: Option<&[&ColumnEntry]>,
2794 indexes: &[IndexAttr],
2795 manager_fns: &[syn::Ident],
2796) -> TokenStream2 {
2797 let root = rustango_root();
2798 // Audit-emit fragments threaded into write paths. Non-empty only
2799 // when the model carries `#[rustango(audit(...))]`. They reborrow
2800 // `_executor` (a `&mut PgConnection` for audited models — the
2801 // macro switches the signature below) so the data write and the
2802 // audit INSERT both run on the same caller-supplied connection.
2803 let executor_passes_to_data_write = if audited_fields.is_some() {
2804 quote!(&mut *_executor)
2805 } else {
2806 quote!(_executor)
2807 };
2808 let executor_param = if audited_fields.is_some() {
2809 quote!(_executor: &mut #root::sql::sqlx::PgConnection)
2810 } else {
2811 quote!(_executor: _E)
2812 };
2813 let executor_generics = if audited_fields.is_some() {
2814 quote!()
2815 } else {
2816 quote!(<'_c, _E>)
2817 };
2818 let executor_where = if audited_fields.is_some() {
2819 quote!()
2820 } else {
2821 quote! {
2822 where
2823 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
2824 }
2825 };
2826 // For audited models the `_on` methods take `&mut PgConnection`, so
2827 // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
2828 // must acquire a connection first. Non-audited models keep the
2829 // direct delegation since `&PgPool` IS an Executor.
2830 let pool_to_save_on = if audited_fields.is_some() {
2831 quote! {
2832 let mut _conn = pool.acquire().await?;
2833 self.save_on(&mut *_conn).await
2834 }
2835 } else {
2836 quote!(self.save_on(pool).await)
2837 };
2838 let pool_to_insert_on = if audited_fields.is_some() {
2839 quote! {
2840 let mut _conn = pool.acquire().await?;
2841 self.insert_on(&mut *_conn).await
2842 }
2843 } else {
2844 quote!(self.insert_on(pool).await)
2845 };
2846 let pool_to_delete_on = if audited_fields.is_some() {
2847 quote! {
2848 let mut _conn = pool.acquire().await?;
2849 self.delete_on(&mut *_conn).await
2850 }
2851 } else {
2852 quote!(self.delete_on(pool).await)
2853 };
2854 let pool_to_bulk_insert_on = if audited_fields.is_some() {
2855 quote! {
2856 let mut _conn = pool.acquire().await?;
2857 Self::bulk_insert_on(rows, &mut *_conn).await
2858 }
2859 } else {
2860 quote!(Self::bulk_insert_on(rows, pool).await)
2861 };
2862 // Pre-existing bug surfaced by batch 22's first audited Auto<T>
2863 // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
2864 // directly, but `upsert_on` for audited models takes
2865 // `&mut PgConnection` (the audit emit needs a real connection).
2866 // Add the missing acquire shim to keep audited Auto-PK upsert
2867 // compiling.
2868 let pool_to_upsert_on = if audited_fields.is_some() {
2869 quote! {
2870 let mut _conn = pool.acquire().await?;
2871 self.upsert_on(&mut *_conn).await
2872 }
2873 } else {
2874 quote!(self.upsert_on(pool).await)
2875 };
2876
2877 // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
2878 // (audit-on-connection over &Pool needs a bi-dialect transaction
2879 // helper, deferred). Two body shapes:
2880 // - has_auto: build InsertQuery skipping Auto::Unset columns,
2881 // request Auto cols in `returning`, dispatch via
2882 // `insert_returning_pool`, then on the returned `PgRow` /
2883 // `MySqlAutoId(id)` enum — pull each Auto field from the PG
2884 // row OR drop the single i64 into the first Auto field on MySQL
2885 // (multi-Auto models on MySQL error at runtime since
2886 // `LAST_INSERT_ID()` only reports one)
2887 // - non-Auto: build InsertQuery with explicit columns/values and
2888 // call `insert_pool` (no returning needed)
2889 // pool_insert_method body for the audited Auto-PK case is moved
2890 // to after audit_pair_tokens / audit_pk_to_string (they live
2891 // ~150 lines below). This block keeps the non-audited and
2892 // non-Auto branches in place — the audited Auto-PK arm is
2893 // computed below and merged via the dispatch helper variable.
2894 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
2895 // Audited models with explicit (non-Auto) PKs go through
2896 // the non-Auto insert path below — the audit emit is one
2897 // round-trip after the INSERT inside the same tx via
2898 // audit::save_one_with_audit? No, INSERT semantics
2899 // differ. For non-Auto PK + audited, route through a
2900 // dedicated insert + audit emit on the same tx, but defer
2901 // the macro emission to the audit-bundle-aware block below
2902 // — this `quote!()` placeholder gets overwritten there.
2903 quote!()
2904 } else if audited_fields.is_some() && fields.has_auto {
2905 // Audited Auto-PK insert_pool — assembled after the audit
2906 // bundles. Placeholder; real emission below.
2907 quote!()
2908 } else if fields.has_auto {
2909 let pushes = &fields.insert_pushes;
2910 let returning_cols = &fields.returning_cols;
2911 // When every `Auto<T>` field is filled Rust-side
2912 // (`default_uuid_v7`, issue #823), there is no column to read
2913 // back from the database — `returning_cols` is empty. Route
2914 // through plain `insert_pool` instead of
2915 // `insert_returning_pool` to skip the redundant RETURNING /
2916 // LAST_INSERT_ID round-trip.
2917 if fields.returning_cols.is_empty() {
2918 quote! {
2919 /// Insert this row against either backend. Every
2920 /// `Auto<T>` PK on this model is filled Rust-side
2921 /// (e.g. `default_uuid_v7`) before binding, so no
2922 /// RETURNING round-trip is needed.
2923 ///
2924 /// # Errors
2925 /// As [`Self::insert`].
2926 pub async fn insert_pool(
2927 &mut self,
2928 pool: &#root::sql::Pool,
2929 ) -> ::core::result::Result<(), #root::sql::ExecError> {
2930 let mut _columns: ::std::vec::Vec<&'static str> =
2931 ::std::vec::Vec::new();
2932 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2933 ::std::vec::Vec::new();
2934 #( #pushes )*
2935 let _query = #root::core::InsertQuery {
2936 model: <Self as #root::core::Model>::SCHEMA,
2937 columns: _columns,
2938 values: _values,
2939 returning: ::std::vec::Vec::new(),
2940 on_conflict: ::core::option::Option::None,
2941 };
2942 #root::sql::insert_pool(pool, &_query).await
2943 }
2944
2945 /// Eloquent `Model::insertOrIgnore()` — same shape
2946 /// as the auto-PK branch above. Returns `Ok(true)`
2947 /// when inserted, `Ok(false)` when a conflict caused
2948 /// the INSERT to silently skip.
2949 ///
2950 /// # Errors
2951 /// As [`Self::insert`].
2952 pub async fn insert_or_ignore(
2953 &mut self,
2954 pool: &#root::sql::Pool,
2955 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
2956 let mut _columns: ::std::vec::Vec<&'static str> =
2957 ::std::vec::Vec::new();
2958 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2959 ::std::vec::Vec::new();
2960 #( #pushes )*
2961 let _query = #root::core::InsertQuery {
2962 model: <Self as #root::core::Model>::SCHEMA,
2963 columns: _columns,
2964 values: _values,
2965 returning: ::std::vec::Vec::new(),
2966 on_conflict: ::core::option::Option::Some(
2967 #root::core::ConflictClause::DoNothing,
2968 ),
2969 };
2970 let dialect = pool.dialect();
2971 let stmt = dialect.compile_insert(&_query)?;
2972 let rows = #root::sql::raw_execute_pool(
2973 pool, &stmt.sql, stmt.params,
2974 ).await?;
2975 ::core::result::Result::Ok(rows > 0)
2976 }
2977 }
2978 } else {
2979 quote! {
2980 /// Insert this row against either backend, populating any
2981 /// `Auto<T>` PK from the auto-assigned value.
2982 ///
2983 /// # Errors
2984 /// As [`Self::insert`].
2985 pub async fn insert_pool(
2986 &mut self,
2987 pool: &#root::sql::Pool,
2988 ) -> ::core::result::Result<(), #root::sql::ExecError> {
2989 let mut _columns: ::std::vec::Vec<&'static str> =
2990 ::std::vec::Vec::new();
2991 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2992 ::std::vec::Vec::new();
2993 #( #pushes )*
2994 let _query = #root::core::InsertQuery {
2995 model: <Self as #root::core::Model>::SCHEMA,
2996 columns: _columns,
2997 values: _values,
2998 returning: ::std::vec![ #( #returning_cols ),* ],
2999 on_conflict: ::core::option::Option::None,
3000 };
3001 let _result = #root::sql::insert_returning_pool(
3002 pool, &_query,
3003 ).await?;
3004 #root::sql::apply_auto_pk(_result, self)
3005 }
3006
3007 /// Eloquent `Model::insertOrIgnore()` — INSERT this
3008 /// row or silently skip on unique-constraint
3009 /// violation. Returns `Ok(true)` when a row was
3010 /// inserted, `Ok(false)` when a conflict caused the
3011 /// INSERT to silently skip.
3012 ///
3013 /// **Caveat on auto-PK models**: when the row is
3014 /// skipped (conflict), this instance's `Auto<T>`
3015 /// fields stay `Unset` — no PK is back-populated
3016 /// because the server didn't auto-assign one. For
3017 /// "insert then read back the PK or the existing
3018 /// row's PK", use the `upsert` family or
3019 /// `get_or_create`.
3020 ///
3021 /// # Errors
3022 /// As [`Self::insert`].
3023 pub async fn insert_or_ignore(
3024 &mut self,
3025 pool: &#root::sql::Pool,
3026 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3027 let mut _columns: ::std::vec::Vec<&'static str> =
3028 ::std::vec::Vec::new();
3029 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3030 ::std::vec::Vec::new();
3031 #( #pushes )*
3032 let _query = #root::core::InsertQuery {
3033 model: <Self as #root::core::Model>::SCHEMA,
3034 columns: _columns,
3035 values: _values,
3036 returning: ::std::vec::Vec::new(),
3037 on_conflict: ::core::option::Option::Some(
3038 #root::core::ConflictClause::DoNothing,
3039 ),
3040 };
3041 let dialect = pool.dialect();
3042 let stmt = dialect.compile_insert(&_query)?;
3043 let rows = #root::sql::raw_execute_pool(
3044 pool, &stmt.sql, stmt.params,
3045 ).await?;
3046 ::core::result::Result::Ok(rows > 0)
3047 }
3048 }
3049 }
3050 } else {
3051 let insert_columns = &fields.insert_columns;
3052 let insert_values = &fields.insert_values;
3053 quote! {
3054 /// Insert this row into its table against either backend.
3055 /// Equivalent to [`Self::insert`] but takes
3056 /// [`#root::sql::Pool`].
3057 ///
3058 /// # Errors
3059 /// As [`Self::insert`].
3060 pub async fn insert_pool(
3061 &self,
3062 pool: &#root::sql::Pool,
3063 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3064 let _query = #root::core::InsertQuery {
3065 model: <Self as #root::core::Model>::SCHEMA,
3066 columns: ::std::vec![ #( #insert_columns ),* ],
3067 values: ::std::vec![ #( #insert_values ),* ],
3068 returning: ::std::vec::Vec::new(),
3069 on_conflict: ::core::option::Option::None,
3070 };
3071 #root::sql::insert_pool(pool, &_query).await
3072 }
3073
3074 /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3075 /// or silently skip on unique-constraint violation. Maps
3076 /// to per-dialect "INSERT ... DO NOTHING on conflict":
3077 /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3078 /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL
3079 /// `INSERT IGNORE INTO …`.
3080 ///
3081 /// Returns `Ok(true)` when a row was inserted,
3082 /// `Ok(false)` when a conflict caused the INSERT to
3083 /// silently skip.
3084 ///
3085 /// Use for "create-if-absent" patterns where you don't
3086 /// need the row back. For "find-or-create with the row
3087 /// returned", use the queryset-level
3088 /// `crate::sql::get_or_create` free function.
3089 ///
3090 /// # Errors
3091 /// As [`Self::insert`], plus any dialect-specific
3092 /// translation error from the ConflictClause writer.
3093 pub async fn insert_or_ignore(
3094 &self,
3095 pool: &#root::sql::Pool,
3096 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3097 let _query = #root::core::InsertQuery {
3098 model: <Self as #root::core::Model>::SCHEMA,
3099 columns: ::std::vec![ #( #insert_columns ),* ],
3100 values: ::std::vec![ #( #insert_values ),* ],
3101 returning: ::std::vec::Vec::new(),
3102 on_conflict: ::core::option::Option::Some(
3103 #root::core::ConflictClause::DoNothing,
3104 ),
3105 };
3106 let dialect = pool.dialect();
3107 let stmt = dialect.compile_insert(&_query)?;
3108 let rows = #root::sql::raw_execute_pool(pool, &stmt.sql, stmt.params).await?;
3109 ::core::result::Result::Ok(rows > 0)
3110 }
3111 }
3112 };
3113
3114 // pool_save_method moved to after audit_pair_tokens /
3115 // audit_pk_to_string (they live ~70 lines below) — needed for
3116 // the audited branch which builds an UpdateQuery + PendingEntry
3117 // and dispatches via audit::save_one_with_audit.
3118
3119 // pool_delete_method moved to after audit_pair_tokens / audit_pk_to_string
3120 // are computed (they live ~80 lines below).
3121
3122 // Build the (column, JSON value) pair list used by every
3123 // snapshot-style audit emission. Reused across delete_on,
3124 // soft_delete_on, restore_on, and (later) bulk paths. Empty
3125 // when the model isn't audited.
3126 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3127 .map(|tracked| {
3128 tracked
3129 .iter()
3130 .map(|c| {
3131 let column_lit = c.column.as_str();
3132 let ident = &c.ident;
3133 quote! {
3134 (
3135 #column_lit,
3136 #root::__serde_json::to_value(&self.#ident)
3137 .unwrap_or(#root::__serde_json::Value::Null),
3138 )
3139 }
3140 })
3141 .collect()
3142 })
3143 .unwrap_or_default();
3144 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3145 if fields.pk_is_auto {
3146 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3147 } else {
3148 quote!(::std::format!("{}", &self.#pk_ident))
3149 }
3150 } else {
3151 quote!(::std::string::String::new())
3152 };
3153 let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
3154 if audited_fields.is_some() {
3155 let pairs = audit_pair_tokens.iter();
3156 let pk_str = audit_pk_to_string.clone();
3157 quote! {
3158 let _audit_entry = #root::audit::PendingEntry {
3159 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3160 entity_pk: #pk_str,
3161 operation: #op_path,
3162 source: #root::audit::current_source(),
3163 changes: #root::audit::snapshot_changes(&[
3164 #( #pairs ),*
3165 ]),
3166 };
3167 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3168 }
3169 } else {
3170 quote!()
3171 }
3172 };
3173 let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create));
3174 let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete));
3175 let audit_softdelete_emit = make_op_emit(quote!(#root::audit::AuditOp::SoftDelete));
3176 let audit_restore_emit = make_op_emit(quote!(#root::audit::AuditOp::Restore));
3177
3178 // `save_pool(&Pool)` — emitted for every model with a PK.
3179 // Audited Auto-PK models are deferred (the Auto::Unset →
3180 // insert_pool path needs the audited-insert flow from a future
3181 // batch). Three body shapes:
3182 // - non-audited, plain PK: build UpdateQuery + dispatch through
3183 // sql::update_pool
3184 // - non-audited, Auto-PK: same, but Auto::Unset routes to
3185 // self.insert_pool which already handles RETURNING / LAST_INSERT_ID
3186 // - audited, plain PK: build UpdateQuery + PendingEntry, dispatch
3187 // through audit::save_one_with_audit (per-backend tx wraps
3188 // UPDATE + audit emit atomically). Snapshot-style audit (post-
3189 // write field values) — diff-style audit (with pre-UPDATE
3190 // SELECT for `before` values) needs per-tracked-column codegen
3191 // that doesn't fit the runtime-helper pattern; legacy &PgPool
3192 // `save` keeps the diff for now.
3193 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3194 let pk_column_lit = pk_col.as_str();
3195 let assignments = &fields.update_assignments;
3196 if audited_fields.is_some() {
3197 if fields.pk_is_auto {
3198 // Auto-PK + audited: defer. The Auto::Unset insert
3199 // path needs a transactional INSERT + LAST_INSERT_ID
3200 // + audit emit flow — that's a follow-up batch.
3201 quote!()
3202 } else {
3203 let pairs = audit_pair_tokens.iter();
3204 let pairs2 = audit_pair_tokens.iter();
3205 let pk_str = audit_pk_to_string.clone();
3206 let pk_str2 = audit_pk_to_string.clone();
3207 quote! {
3208 /// Save (UPDATE) this row against either backend
3209 /// with audit emission inside the same transaction.
3210 /// Bi-dialect counterpart of [`Self::save`] for
3211 /// audited models with non-`Auto<T>` PKs.
3212 ///
3213 /// Captures **post-write** field state (snapshot
3214 /// audit). The legacy &PgPool [`Self::save`]
3215 /// captures BEFORE+AFTER for true diff audit;
3216 /// porting that to the &Pool path needs runtime
3217 /// per-tracked-column decoding and is deferred.
3218 ///
3219 /// # Errors
3220 /// As [`Self::save`].
3221 pub async fn save_pool(
3222 &mut self,
3223 pool: &#root::sql::Pool,
3224 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3225 let _query = #root::core::UpdateQuery {
3226 model: <Self as #root::core::Model>::SCHEMA,
3227 set: ::std::vec![ #( #assignments ),* ],
3228 where_clause: #root::core::WhereExpr::Predicate(
3229 #root::core::Filter {
3230 column: #pk_column_lit,
3231 op: #root::core::Op::Eq,
3232 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3233 ::core::clone::Clone::clone(&self.#pk_ident)
3234 ),
3235 }
3236 ),
3237 };
3238 let _audit_entry = #root::audit::PendingEntry {
3239 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3240 entity_pk: #pk_str,
3241 operation: #root::audit::AuditOp::Update,
3242 source: #root::audit::current_source(),
3243 changes: #root::audit::snapshot_changes(&[
3244 #( #pairs ),*
3245 ]),
3246 };
3247 let _affected = #root::audit::save_one_with_audit(
3248 pool, &_query, &_audit_entry,
3249 ).await?;
3250 ::core::result::Result::Ok(_affected)
3251 }
3252
3253 /// `save_pool` narrowed to a Rust-field allowlist — issue #66
3254 /// (Django `Model.save(update_fields=[...])`).
3255 /// Audit emission shrinks to the same column set so
3256 /// the audit log reflects exactly what was written.
3257 ///
3258 /// # Errors
3259 /// As [`Self::save_pool`], plus
3260 /// [`#root::core::QueryError::UnknownField`] wrapped
3261 /// in `ExecError::Query` for unknown field names.
3262 pub async fn save_partial(
3263 &mut self,
3264 fields: &[&str],
3265 pool: &#root::sql::Pool,
3266 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3267 if fields.is_empty() {
3268 #root::__tracing::warn!(
3269 target: "rustango::save_partial",
3270 model = <Self as #root::core::Model>::SCHEMA.name,
3271 "save_partial called with empty field list — no-op"
3272 );
3273 return ::core::result::Result::Ok(0);
3274 }
3275 let _schema = <Self as #root::core::Model>::SCHEMA;
3276 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3277 ::std::collections::HashSet::with_capacity(fields.len());
3278 for f in fields {
3279 match _schema.field(f) {
3280 ::core::option::Option::Some(fs) => {
3281 _wanted_cols.insert(fs.column);
3282 }
3283 ::core::option::Option::None => {
3284 return ::core::result::Result::Err(
3285 #root::sql::ExecError::Query(
3286 #root::core::QueryError::UnknownField {
3287 model: _schema.name,
3288 field: (*f).to_owned(),
3289 }
3290 )
3291 );
3292 }
3293 }
3294 }
3295 let _full: ::std::vec::Vec<#root::core::Assignment> =
3296 ::std::vec![ #( #assignments ),* ];
3297 let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3298 .into_iter()
3299 .filter(|a| _wanted_cols.contains(a.column))
3300 .collect();
3301 if _filtered.is_empty() {
3302 #root::__tracing::warn!(
3303 target: "rustango::save_partial",
3304 model = _schema.name,
3305 "save_partial: every named field maps to a non-assignable column — no-op"
3306 );
3307 return ::core::result::Result::Ok(0);
3308 }
3309 let _query = #root::core::UpdateQuery {
3310 model: _schema,
3311 set: _filtered,
3312 where_clause: #root::core::WhereExpr::Predicate(
3313 #root::core::Filter {
3314 column: #pk_column_lit,
3315 op: #root::core::Op::Eq,
3316 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3317 ::core::clone::Clone::clone(&self.#pk_ident)
3318 ),
3319 }
3320 ),
3321 };
3322 // Narrow the audit snapshot to the same column set.
3323 let _all_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3324 ::std::vec![ #( #pairs2 ),* ];
3325 let _narrowed: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3326 _all_pairs
3327 .into_iter()
3328 .filter(|(col, _)| _wanted_cols.contains(col))
3329 .collect();
3330 let _audit_entry = #root::audit::PendingEntry {
3331 entity_table: _schema.table,
3332 entity_pk: #pk_str2,
3333 operation: #root::audit::AuditOp::Update,
3334 source: #root::audit::current_source(),
3335 changes: #root::audit::snapshot_changes(&_narrowed),
3336 };
3337 let _affected = #root::audit::save_one_with_audit(
3338 pool, &_query, &_audit_entry,
3339 ).await?;
3340 ::core::result::Result::Ok(_affected)
3341 }
3342
3343 /// Typed-column counterpart of [`Self::save_partial`] —
3344 /// issue #67. `fields` is a tuple of [`Column`]
3345 /// constants whose `Model` matches `Self`; typos and
3346 /// model mismatches surface at *compile time*
3347 /// (`Author::name` inside a `Post::save_partial_typed`
3348 /// call is a type error, no runtime check).
3349 ///
3350 /// ```ignore
3351 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3352 /// ```
3353 ///
3354 /// Lowers to [`Self::save_partial`] under the hood;
3355 /// audit narrowing + every other semantic is identical.
3356 ///
3357 /// [`Column`]: #root::core::Column
3358 ///
3359 /// # Errors
3360 /// As [`Self::save_partial`].
3361 pub async fn save_partial_typed<
3362 L: #root::core::TypedFieldList<Self>,
3363 >(
3364 &mut self,
3365 fields: L,
3366 pool: &#root::sql::Pool,
3367 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3368 let _names = fields.rust_field_names();
3369 let _refs: ::std::vec::Vec<&str> =
3370 _names.iter().copied().collect();
3371 self.save_partial(&_refs, pool).await
3372 }
3373 }
3374 }
3375 } else {
3376 let dispatch_unset = if fields.pk_is_auto {
3377 quote! {
3378 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3379 return self.insert_pool(pool).await.map(|()| 1u64);
3380 }
3381 }
3382 } else {
3383 quote!()
3384 };
3385 quote! {
3386 /// Save this row to its table against either backend.
3387 /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3388 /// `UPDATE` keyed on the PK.
3389 ///
3390 /// # Errors
3391 /// As [`Self::save`].
3392 pub async fn save_pool(
3393 &mut self,
3394 pool: &#root::sql::Pool,
3395 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3396 #dispatch_unset
3397 let _query = #root::core::UpdateQuery {
3398 model: <Self as #root::core::Model>::SCHEMA,
3399 set: ::std::vec![ #( #assignments ),* ],
3400 where_clause: #root::core::WhereExpr::Predicate(
3401 #root::core::Filter {
3402 column: #pk_column_lit,
3403 op: #root::core::Op::Eq,
3404 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3405 ::core::clone::Clone::clone(&self.#pk_ident)
3406 ),
3407 }
3408 ),
3409 };
3410 let _affected = #root::sql::update_pool(pool, &_query).await?;
3411 ::core::result::Result::Ok(_affected)
3412 }
3413
3414 /// Save (UPDATE) only the listed Rust-side fields,
3415 /// leaving every other column untouched. Issue #66 —
3416 /// Django's `Model.save(update_fields=[...])` shape.
3417 ///
3418 /// `fields` are Rust-side struct field names; the macro
3419 /// resolves each to its SQL column. Unknown field
3420 /// names return [`#root::core::QueryError::UnknownField`]
3421 /// wrapped in `ExecError::Query`. An empty list is a
3422 /// no-op (returns `Ok(())` and logs a `tracing::warn!`),
3423 /// matching Django's "nothing to do" semantic.
3424 ///
3425 /// Use this when:
3426 /// * you only mutated a couple of fields on a wide row
3427 /// (avoid re-writing every column on every save), or
3428 /// * two writers diverged after their initial read and
3429 /// you want to preserve the other writer's changes to
3430 /// columns you didn't touch.
3431 ///
3432 /// Auto-PK models with an unset PK return
3433 /// [`#root::core::QueryError::UnknownField`] with
3434 /// field name `<pk>` — `save_partial` is an
3435 /// UPDATE-only path. Call [`Self::insert_pool`]
3436 /// (or [`Self::save_pool`] which dispatches based on
3437 /// PK state) for the INSERT case.
3438 ///
3439 /// # Errors
3440 /// As [`Self::save_pool`], plus `UnknownField` for
3441 /// unknown / empty / Auto-Unset cases.
3442 pub async fn save_partial(
3443 &mut self,
3444 fields: &[&str],
3445 pool: &#root::sql::Pool,
3446 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3447 if fields.is_empty() {
3448 #root::__tracing::warn!(
3449 target: "rustango::save_partial",
3450 model = <Self as #root::core::Model>::SCHEMA.name,
3451 "save_partial called with empty field list — no-op"
3452 );
3453 return ::core::result::Result::Ok(0);
3454 }
3455 let _schema = <Self as #root::core::Model>::SCHEMA;
3456 // Validate field names against the schema.
3457 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3458 ::std::collections::HashSet::with_capacity(fields.len());
3459 for f in fields {
3460 match _schema.field(f) {
3461 ::core::option::Option::Some(fs) => {
3462 _wanted_cols.insert(fs.column);
3463 }
3464 ::core::option::Option::None => {
3465 return ::core::result::Result::Err(
3466 #root::sql::ExecError::Query(
3467 #root::core::QueryError::UnknownField {
3468 model: _schema.name,
3469 field: (*f).to_owned(),
3470 }
3471 )
3472 );
3473 }
3474 }
3475 }
3476 // Build the full assignment vec, then keep only the
3477 // assignments whose column is in `_wanted_cols`.
3478 let _full: ::std::vec::Vec<#root::core::Assignment> =
3479 ::std::vec![ #( #assignments ),* ];
3480 let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3481 .into_iter()
3482 .filter(|a| _wanted_cols.contains(a.column))
3483 .collect();
3484 if _filtered.is_empty() {
3485 // All field names valid, but they all map to
3486 // non-assignable slots (PK column, computed/
3487 // virtual fields, relations without an
3488 // assignment). Same no-op semantic as Django.
3489 #root::__tracing::warn!(
3490 target: "rustango::save_partial",
3491 model = _schema.name,
3492 "save_partial: every named field maps to a non-assignable column — no-op"
3493 );
3494 return ::core::result::Result::Ok(0);
3495 }
3496 let _query = #root::core::UpdateQuery {
3497 model: _schema,
3498 set: _filtered,
3499 where_clause: #root::core::WhereExpr::Predicate(
3500 #root::core::Filter {
3501 column: #pk_column_lit,
3502 op: #root::core::Op::Eq,
3503 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3504 ::core::clone::Clone::clone(&self.#pk_ident)
3505 ),
3506 }
3507 ),
3508 };
3509 let _affected = #root::sql::update_pool(pool, &_query).await?;
3510 ::core::result::Result::Ok(_affected)
3511 }
3512
3513 /// Typed-column counterpart of [`Self::save_partial`] —
3514 /// issue #67. `fields` is a tuple of [`Column`]
3515 /// constants whose `Model` matches `Self`; typos and
3516 /// model mismatches surface at *compile time*
3517 /// (`Author::name` inside a `Post::save_partial_typed`
3518 /// call is a type error, no runtime check).
3519 ///
3520 /// ```ignore
3521 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3522 /// ```
3523 ///
3524 /// Lowers to [`Self::save_partial`] under the hood — the
3525 /// tuple is reduced to a `&[&str]` slice of Rust-side
3526 /// field names and forwarded.
3527 ///
3528 /// [`Column`]: #root::core::Column
3529 ///
3530 /// # Errors
3531 /// As [`Self::save_partial`].
3532 pub async fn save_partial_typed<
3533 L: #root::core::TypedFieldList<Self>,
3534 >(
3535 &mut self,
3536 fields: L,
3537 pool: &#root::sql::Pool,
3538 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3539 let _names = fields.rust_field_names();
3540 let _refs: ::std::vec::Vec<&str> =
3541 _names.iter().copied().collect();
3542 self.save_partial(&_refs, pool).await
3543 }
3544 }
3545 }
3546 } else {
3547 quote!()
3548 };
3549
3550 // Audited `insert_pool` (overrides the placeholder set higher up
3551 // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3552 // audited models get insert_pool routing through
3553 // audit::insert_one_with_audit (per-backend tx wraps INSERT
3554 // + auto-PK readback + audit emit). Snapshot-style audit (the
3555 // PendingEntry's `changes` carries post-write field values).
3556 let pool_insert_method = if audited_fields.is_some() {
3557 if let Some(_) = primary_key {
3558 let pushes = if fields.has_auto {
3559 fields.insert_pushes.clone()
3560 } else {
3561 // For non-Auto-PK models, the macro normally builds
3562 // {columns, values} from fields.insert_columns +
3563 // fields.insert_values rather than insert_pushes.
3564 // Map those into the pushes shape.
3565 fields
3566 .insert_columns
3567 .iter()
3568 .zip(&fields.insert_values)
3569 .map(|(col, val)| {
3570 quote! {
3571 _columns.push(#col);
3572 _values.push(#val);
3573 }
3574 })
3575 .collect()
3576 };
3577 let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
3578 fields.returning_cols.clone()
3579 } else {
3580 // Non-Auto-PK: still need RETURNING something for the
3581 // audit helper's contract (it errors on empty
3582 // returning). Return the PK column so the audit row
3583 // can carry the assigned PK back. Some non-Auto PKs
3584 // are server-side-default (e.g. UUIDv4 default), so
3585 // RETURNING is genuinely useful.
3586 primary_key
3587 .map(|(_, col)| {
3588 let lit = col.as_str();
3589 vec![quote!(#lit)]
3590 })
3591 .unwrap_or_default()
3592 };
3593 let pairs = audit_pair_tokens.iter();
3594 let pk_str = audit_pk_to_string.clone();
3595 quote! {
3596 /// Insert this row against either backend with audit
3597 /// emission inside the same transaction. Bi-dialect
3598 /// counterpart of [`Self::insert`] for audited models.
3599 ///
3600 /// Snapshot-style audit (post-write field values).
3601 ///
3602 /// # Errors
3603 /// As [`Self::insert`].
3604 pub async fn insert_pool(
3605 &mut self,
3606 pool: &#root::sql::Pool,
3607 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3608 let mut _columns: ::std::vec::Vec<&'static str> =
3609 ::std::vec::Vec::new();
3610 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3611 ::std::vec::Vec::new();
3612 #( #pushes )*
3613 let _query = #root::core::InsertQuery {
3614 model: <Self as #root::core::Model>::SCHEMA,
3615 columns: _columns,
3616 values: _values,
3617 returning: ::std::vec![ #( #returning_cols ),* ],
3618 on_conflict: ::core::option::Option::None,
3619 };
3620 let _audit_entry = #root::audit::PendingEntry {
3621 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3622 entity_pk: #pk_str,
3623 operation: #root::audit::AuditOp::Create,
3624 source: #root::audit::current_source(),
3625 changes: #root::audit::snapshot_changes(&[
3626 #( #pairs ),*
3627 ]),
3628 };
3629 let _result = #root::audit::insert_one_with_audit(
3630 pool, &_query, &_audit_entry,
3631 ).await?;
3632 #root::sql::apply_auto_pk(_result, self)
3633 }
3634 }
3635 } else {
3636 quote!()
3637 }
3638 } else {
3639 // Keep the non-audited pool_insert_method we built earlier.
3640 pool_insert_method
3641 };
3642
3643 // Update audited save_pool: now that insert_pool is wired for
3644 // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3645 // insert_pool. Non-audited save_pool already does this.
3646 // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3647 // Replaces the snapshot-only emission with a per-backend transaction
3648 // body that:
3649 // 1. SELECTs the tracked columns by PK (typed Row::try_get per
3650 // column), capturing BEFORE values
3651 // 2. compiles the UPDATE via pool.dialect() and runs it on the tx
3652 // 3. builds AFTER pairs from &self
3653 // 4. diffs BEFORE/AFTER, emits one PendingEntry with
3654 // AuditOp::Update + diff_changes(...) on the same tx connection
3655 // 5. commits
3656 //
3657 // Per-backend arms inline the SQL string + placeholder shape, then
3658 // share the `audit_before_pair_tokens` decoder block (Row::try_get
3659 // is polymorphic over Row type — the same tokens work against
3660 // PgRow and MySqlRow as long as the field's Rust type implements
3661 // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3662 // primitives + chrono/uuid/serde_json::Value all do).
3663 let pool_save_method = if let Some(tracked) = audited_fields {
3664 if let Some((pk_ident, pk_col)) = primary_key {
3665 let pk_column_lit = pk_col.as_str();
3666 // Two iterators — quote!'s `#(#var)*` consumes the
3667 // iterator, and we need to splice the same after-pairs
3668 // sequence into both per-backend arms.
3669 let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
3670 let pk_str = audit_pk_to_string.clone();
3671 // Per-tracked-column BEFORE-pair token list. Each entry
3672 // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3673 // The Row alias resolves to PgRow / MySqlRow per call site,
3674 // so the same template generates both the PG and MySQL bodies.
3675 let mk_before_pairs =
3676 |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
3677 tracked
3678 .iter()
3679 .map(|c| {
3680 let column_lit = c.column.as_str();
3681 let value_ty = &c.value_ty;
3682 quote! {
3683 (
3684 #column_lit,
3685 match #getter::<#value_ty>(
3686 _audit_before_row, #column_lit,
3687 ) {
3688 ::core::result::Result::Ok(v) => {
3689 #root::__serde_json::to_value(&v)
3690 .unwrap_or(#root::__serde_json::Value::Null)
3691 }
3692 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3693 },
3694 )
3695 }
3696 })
3697 .collect()
3698 };
3699 let before_pairs_pg: Vec<proc_macro2::TokenStream> =
3700 mk_before_pairs(quote!(#root::sql::try_get_returning));
3701 let before_pairs_my: Vec<proc_macro2::TokenStream> =
3702 mk_before_pairs(quote!(#root::sql::try_get_returning_my));
3703 let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
3704 mk_before_pairs(quote!(#root::sql::try_get_returning_sqlite));
3705 let pg_select_cols: String = tracked
3706 .iter()
3707 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
3708 .collect::<Vec<_>>()
3709 .join(", ");
3710 let my_select_cols: String = tracked
3711 .iter()
3712 .map(|c| format!("`{}`", c.column.replace('`', "``")))
3713 .collect::<Vec<_>>()
3714 .join(", ");
3715 // SQLite uses double-quote identifier quoting (same as
3716 // Postgres in default config), so the column-list shape
3717 // matches PG.
3718 let sqlite_select_cols: String = pg_select_cols.clone();
3719 let pk_value_for_bind = if fields.pk_is_auto {
3720 quote!(self.#pk_ident.get().copied().unwrap_or_default())
3721 } else {
3722 quote!(::core::clone::Clone::clone(&self.#pk_ident))
3723 };
3724 let assignments = &fields.update_assignments;
3725 let unset_dispatch = if fields.has_auto {
3726 quote! {
3727 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3728 return self.insert_pool(pool).await.map(|()| 1u64);
3729 }
3730 }
3731 } else {
3732 quote!()
3733 };
3734 quote! {
3735 /// Save this row against either backend with audit
3736 /// emission (diff-style: BEFORE+AFTER) inside the
3737 /// same transaction. Auto::Unset PK routes to
3738 /// insert_pool. Bi-dialect counterpart of
3739 /// [`Self::save`] for audited models.
3740 ///
3741 /// The audit row's `changes` JSON contains one
3742 /// `{ "field": { "before": …, "after": … } }` entry
3743 /// per tracked column whose value actually changed
3744 /// — same shape as the existing &PgPool save() emits.
3745 ///
3746 /// # Errors
3747 /// As [`Self::save`].
3748 pub async fn save_pool(
3749 &mut self,
3750 pool: &#root::sql::Pool,
3751 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3752 #unset_dispatch
3753 let _query = #root::core::UpdateQuery {
3754 model: <Self as #root::core::Model>::SCHEMA,
3755 set: ::std::vec![ #( #assignments ),* ],
3756 where_clause: #root::core::WhereExpr::Predicate(
3757 #root::core::Filter {
3758 column: #pk_column_lit,
3759 op: #root::core::Op::Eq,
3760 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3761 ::core::clone::Clone::clone(&self.#pk_ident)
3762 ),
3763 }
3764 ),
3765 };
3766 let _after_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3767 ::std::vec![ #( #after_pairs_pg ),* ];
3768 #root::audit::save_one_with_diff(
3769 pool,
3770 &_query,
3771 #pk_column_lit,
3772 ::core::convert::Into::<#root::core::SqlValue>::into(
3773 #pk_value_for_bind,
3774 ),
3775 <Self as #root::core::Model>::SCHEMA.table,
3776 #pk_str,
3777 _after_pairs,
3778 #pg_select_cols,
3779 #my_select_cols,
3780 #sqlite_select_cols,
3781 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
3782 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
3783 |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
3784 ).await
3785 }
3786 }
3787 } else {
3788 quote!()
3789 }
3790 } else {
3791 pool_save_method
3792 };
3793
3794 // `delete_pool(&Pool)` — emitted for every model with a PK. Two
3795 // body shapes:
3796 // - non-audited: simple dispatch through `sql::delete_pool`
3797 // - audited: routes through `audit::delete_one_with_audit`,
3798 // which opens a per-backend transaction wrapping DELETE +
3799 // audit emit so the data write and audit row commit atomically.
3800 let pool_delete_method = {
3801 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
3802 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
3803 if let Some(pk_ident) = pk_ident_for_pool {
3804 if audited_fields.is_some() {
3805 let pairs = audit_pair_tokens.iter();
3806 let pk_str = audit_pk_to_string.clone();
3807 quote! {
3808 /// Delete this row against either backend with audit
3809 /// emission inside the same transaction. Bi-dialect
3810 /// counterpart of [`Self::delete`] for audited models.
3811 ///
3812 /// # Errors
3813 /// As [`Self::delete`].
3814 pub async fn delete_pool(
3815 &self,
3816 pool: &#root::sql::Pool,
3817 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3818 let _query = #root::core::DeleteQuery {
3819 model: <Self as #root::core::Model>::SCHEMA,
3820 where_clause: #root::core::WhereExpr::Predicate(
3821 #root::core::Filter {
3822 column: #pk_column_lit,
3823 op: #root::core::Op::Eq,
3824 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3825 ::core::clone::Clone::clone(&self.#pk_ident)
3826 ),
3827 }
3828 ),
3829 };
3830 let _audit_entry = #root::audit::PendingEntry {
3831 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3832 entity_pk: #pk_str,
3833 operation: #root::audit::AuditOp::Delete,
3834 source: #root::audit::current_source(),
3835 changes: #root::audit::snapshot_changes(&[
3836 #( #pairs ),*
3837 ]),
3838 };
3839 #root::audit::delete_one_with_audit(
3840 pool, &_query, &_audit_entry,
3841 ).await
3842 }
3843 }
3844 } else {
3845 quote! {
3846 /// Delete the row identified by this instance's primary key
3847 /// against either backend. Equivalent to [`Self::delete`] but
3848 /// takes [`#root::sql::Pool`] and dispatches per backend.
3849 ///
3850 /// # Errors
3851 /// As [`Self::delete`].
3852 pub async fn delete_pool(
3853 &self,
3854 pool: &#root::sql::Pool,
3855 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3856 let _query = #root::core::DeleteQuery {
3857 model: <Self as #root::core::Model>::SCHEMA,
3858 where_clause: #root::core::WhereExpr::Predicate(
3859 #root::core::Filter {
3860 column: #pk_column_lit,
3861 op: #root::core::Op::Eq,
3862 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3863 ::core::clone::Clone::clone(&self.#pk_ident)
3864 ),
3865 }
3866 ),
3867 };
3868 #root::sql::delete_pool(pool, &_query).await
3869 }
3870 }
3871 }
3872 } else {
3873 quote!()
3874 }
3875 };
3876
3877 // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
3878 // matching this instance's PK and overwrite the in-memory state
3879 // with the freshly-fetched columns. Django's `refresh_from_db`.
3880 // Issue #825. Only emitted when the model declares a PK; non-PK
3881 // models can't address a specific row.
3882 //
3883 // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
3884 // every field from `self`; resets the PK to `Auto::Unset` when
3885 // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
3886 // a fresh autoincrement value. Non-Auto PKs preserve the source
3887 // PK — the caller must overwrite before insert. Pure-Rust, no
3888 // I/O, no dialect surface.
3889 let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
3890 let other_field_clones: Vec<TokenStream2> = fields
3891 .column_entries
3892 .iter()
3893 .filter(|c| &c.ident != pk_ident)
3894 .map(|c| {
3895 let ident = &c.ident;
3896 quote! {
3897 #ident: ::core::clone::Clone::clone(&self.#ident)
3898 }
3899 })
3900 .collect();
3901 let pk_clone_token = if fields.pk_is_auto {
3902 quote! { #pk_ident: #root::sql::Auto::Unset }
3903 } else {
3904 quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
3905 };
3906 let replicate_doc = if fields.pk_is_auto {
3907 quote! {
3908 /// Eloquent-style `replicate()` — return a clone of this
3909 /// row with the primary key reset to [`Auto::Unset`] so
3910 /// the copy is ready for `insert_pool` / `save_pool` to
3911 /// allocate a fresh autoincrement value. Every other
3912 /// field is `Clone`d verbatim. Issue #825.
3913 ///
3914 /// `auto_now_add` / `auto_now` timestamp fields are
3915 /// **not** reset (Eloquent's `replicate` doesn't reset
3916 /// them either) — pass them through the normal insert
3917 /// path if you want fresh values, or assign them
3918 /// explicitly after the call.
3919 }
3920 } else {
3921 quote! {
3922 /// Eloquent-style `replicate()` — clone this row
3923 /// verbatim. Because the primary key is **not** an
3924 /// `Auto<T>`, the clone keeps the source PK; the
3925 /// caller must overwrite `copy.<pk>` before inserting
3926 /// to avoid a unique-key violation. Issue #825.
3927 }
3928 };
3929 // 2026-06-07 — field-name / shortcut collision guard.
3930 //
3931 // The macro emits both `pub const <field>: <field>_col = ...`
3932 // (per-field typed-column const, used by the typed-builder
3933 // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
3934 // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
3935 // model has a field named e.g. `count`, both items would land
3936 // in the same inherent impl with the same name, and the
3937 // compiler rejects the derive with "duplicate definitions".
3938 //
3939 // Drop the conflicting shortcut for that model. Callers can
3940 // still reach the same behavior via
3941 // `QuerySet::<Model>::default().count(&pool)`.
3942 let column_names: ::std::collections::HashSet<String> = fields
3943 .column_entries
3944 .iter()
3945 .map(|c| c.ident.to_string())
3946 .collect();
3947 let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
3948 if column_names.contains(name) {
3949 quote! {}
3950 } else {
3951 tokens
3952 }
3953 };
3954 let count_method = emit_if_no_field_collision(
3955 "count",
3956 quote! {
3957 /// Count rows of this model — `SELECT COUNT(*) FROM
3958 /// <table>`. Eloquent `Model::count()` parity.
3959 ///
3960 /// Skipped on models that already declare a field named
3961 /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
3962 /// in that case.
3963 ///
3964 /// # Errors
3965 /// As [`CounterPool::count`].
3966 ///
3967 /// [`CounterPool::count`]: rustango::sql::CounterPool::count
3968 pub async fn count(
3969 pool: &#root::sql::Pool,
3970 ) -> ::core::result::Result<i64, #root::sql::ExecError> {
3971 use #root::sql::CounterPool as _;
3972 #root::query::QuerySet::<Self>::default()
3973 .count(pool)
3974 .await
3975 }
3976 },
3977 );
3978 let value_method = emit_if_no_field_collision(
3979 "value",
3980 quote! {
3981 /// Pluck a single scalar from the first row.
3982 /// Eloquent `Model::query()->value($col)` parity.
3983 ///
3984 /// Skipped on models that already declare a field named
3985 /// `value`. Drop into
3986 /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
3987 ///
3988 /// # Errors
3989 /// As `ValuesFlatQuerySet::first`.
3990 pub async fn value<U>(
3991 col: &str,
3992 pool: &#root::sql::Pool,
3993 ) -> ::core::result::Result<
3994 ::core::option::Option<U>,
3995 #root::sql::ExecError,
3996 >
3997 where
3998 U: #root::sql::MaybePgScalar
3999 + #root::sql::MaybeMyScalar
4000 + #root::sql::MaybeSqliteScalar
4001 + ::core::marker::Send
4002 + ::core::marker::Unpin,
4003 {
4004 let _col_static: &'static str = Self::__resolve_col(col)?;
4005 #root::query::QuerySet::<Self>::default()
4006 .values_list_flat(_col_static)
4007 .first::<U>(pool)
4008 .await
4009 }
4010 },
4011 );
4012 let sum_method = emit_if_no_field_collision(
4013 "sum",
4014 quote! {
4015 /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4016 /// Skipped on models that already declare a field named
4017 /// `sum`.
4018 ///
4019 /// # Errors
4020 /// As [`#root::sql::fetch_aggregate_pool`].
4021 pub async fn sum<U>(
4022 col: &str,
4023 pool: &#root::sql::Pool,
4024 ) -> ::core::result::Result<
4025 ::core::option::Option<U>,
4026 #root::sql::ExecError,
4027 >
4028 where
4029 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4030 + #root::sql::MaybeMyFromRow
4031 + #root::sql::MaybeSqliteFromRow
4032 + ::core::marker::Send
4033 + ::core::marker::Unpin,
4034 {
4035 Self::__aggregate_one_pool::<U>(
4036 col,
4037 |c| #root::core::AggregateExpr::Sum(c),
4038 pool,
4039 )
4040 .await
4041 }
4042 },
4043 );
4044 let avg_method = emit_if_no_field_collision(
4045 "avg",
4046 quote! {
4047 /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4048 /// models that already declare a field named `avg`.
4049 ///
4050 /// # Errors
4051 /// As [`#root::sql::fetch_aggregate_pool`].
4052 pub async fn avg<U>(
4053 col: &str,
4054 pool: &#root::sql::Pool,
4055 ) -> ::core::result::Result<
4056 ::core::option::Option<U>,
4057 #root::sql::ExecError,
4058 >
4059 where
4060 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4061 + #root::sql::MaybeMyFromRow
4062 + #root::sql::MaybeSqliteFromRow
4063 + ::core::marker::Send
4064 + ::core::marker::Unpin,
4065 {
4066 Self::__aggregate_one_pool::<U>(
4067 col,
4068 |c| #root::core::AggregateExpr::Avg(c),
4069 pool,
4070 )
4071 .await
4072 }
4073 },
4074 );
4075 let min_method = emit_if_no_field_collision(
4076 "min",
4077 quote! {
4078 /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4079 /// models that already declare a field named `min`.
4080 ///
4081 /// # Errors
4082 /// As [`#root::sql::fetch_aggregate_pool`].
4083 pub async fn min<U>(
4084 col: &str,
4085 pool: &#root::sql::Pool,
4086 ) -> ::core::result::Result<
4087 ::core::option::Option<U>,
4088 #root::sql::ExecError,
4089 >
4090 where
4091 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4092 + #root::sql::MaybeMyFromRow
4093 + #root::sql::MaybeSqliteFromRow
4094 + ::core::marker::Send
4095 + ::core::marker::Unpin,
4096 {
4097 Self::__aggregate_one_pool::<U>(
4098 col,
4099 |c| #root::core::AggregateExpr::Min(c),
4100 pool,
4101 )
4102 .await
4103 }
4104 },
4105 );
4106 let max_method = emit_if_no_field_collision(
4107 "max",
4108 quote! {
4109 /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4110 /// models that already declare a field named `max`.
4111 ///
4112 /// # Errors
4113 /// As [`#root::sql::fetch_aggregate_pool`].
4114 pub async fn max<U>(
4115 col: &str,
4116 pool: &#root::sql::Pool,
4117 ) -> ::core::result::Result<
4118 ::core::option::Option<U>,
4119 #root::sql::ExecError,
4120 >
4121 where
4122 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4123 + #root::sql::MaybeMyFromRow
4124 + #root::sql::MaybeSqliteFromRow
4125 + ::core::marker::Send
4126 + ::core::marker::Unpin,
4127 {
4128 Self::__aggregate_one_pool::<U>(
4129 col,
4130 |c| #root::core::AggregateExpr::Max(c),
4131 pool,
4132 )
4133 .await
4134 }
4135 },
4136 );
4137 let first_method = emit_if_no_field_collision(
4138 "first",
4139 quote! {
4140 /// First row of this model. Eloquent `Model::first()`.
4141 /// Skipped on models that already declare a field named
4142 /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4143 ///
4144 /// # Errors
4145 /// As `QuerySet::first`.
4146 pub async fn first(
4147 pool: &#root::sql::Pool,
4148 ) -> ::core::result::Result<
4149 ::core::option::Option<Self>,
4150 #root::sql::ExecError,
4151 > {
4152 #root::query::QuerySet::<Self>::default()
4153 .first(pool)
4154 .await
4155 }
4156 },
4157 );
4158 let last_method = emit_if_no_field_collision(
4159 "last",
4160 quote! {
4161 /// Last row of this model by primary-key DESC.
4162 /// Eloquent `Model::query()->latest('id')->first()`
4163 /// parity — fetches the highest-PK row without
4164 /// requiring the caller to spell the PK column.
4165 /// Returns `None` on an empty table.
4166 ///
4167 /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4168 /// Skipped on models that already declare a field
4169 /// named `last`.
4170 ///
4171 /// # Errors
4172 /// As `QuerySet::last`.
4173 pub async fn last(
4174 pool: &#root::sql::Pool,
4175 ) -> ::core::result::Result<
4176 ::core::option::Option<Self>,
4177 #root::sql::ExecError,
4178 > {
4179 #root::query::QuerySet::<Self>::default()
4180 .last(pool)
4181 .await
4182 }
4183 },
4184 );
4185 quote! {
4186 /// Re-SELECT this row by its primary key and overwrite
4187 /// every in-memory field with the freshly-fetched value.
4188 /// Django's [`Model.refresh_from_db`]. Issue #825.
4189 ///
4190 /// Use this when the row may have been modified by another
4191 /// process / connection / job since you read it — e.g. after
4192 /// a queued task callback, or to re-sync stale UI state
4193 /// before re-saving.
4194 ///
4195 /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4196 /// when the primary key no longer matches any row (e.g.
4197 /// the row was deleted concurrently).
4198 ///
4199 /// # Errors
4200 /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4201 /// the PK no longer exists.
4202 ///
4203 /// [`Model.refresh_from_db`]: https://docs.djangoproject.com/en/5.1/ref/models/instances/#django.db.models.Model.refresh_from_db
4204 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4205 pub async fn refresh_from_db(
4206 &mut self,
4207 pool: &#root::sql::Pool,
4208 ) -> ::core::result::Result<(), #root::sql::ExecError> {
4209 use #root::sql::FetcherPool as _;
4210 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4211 ::core::clone::Clone::clone(&self.#pk_ident),
4212 );
4213 let mut _rows: ::std::vec::Vec<Self> =
4214 #root::query::QuerySet::<Self>::default()
4215 .filter(::core::stringify!(#pk_ident), _pk_val)
4216 .limit(1)
4217 .fetch(pool)
4218 .await?;
4219 match _rows.into_iter().next() {
4220 ::core::option::Option::Some(_fresh) => {
4221 *self = _fresh;
4222 ::core::result::Result::Ok(())
4223 }
4224 ::core::option::Option::None => ::core::result::Result::Err(
4225 #root::sql::ExecError::Driver(
4226 #root::sql::sqlx::Error::RowNotFound,
4227 ),
4228 ),
4229 }
4230 }
4231
4232 /// Atomically increment the integer column `col` by
4233 /// `by` for this row. Equivalent to
4234 /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4235 /// Eloquent `Model::increment($col, $by)` / Django
4236 /// `Model.objects.filter(pk=…).update(col=F('col')+$by)`
4237 /// parity.
4238 ///
4239 /// **Doesn't mutate `self`** — the in-memory copy is now
4240 /// stale; call [`Self::refresh_from_db_pool`] /
4241 /// [`Self::fresh_pool`] to re-sync. Returns the rows-
4242 /// affected count (0 when the PK doesn't match any row,
4243 /// 1 on success).
4244 ///
4245 /// `col` is the Rust field name as a string; unknown
4246 /// fields surface as `UnknownField` at runtime. Negative
4247 /// `by` values atomically decrement (see also
4248 /// [`Self::decrement_pool`]).
4249 ///
4250 /// # Errors
4251 /// As [`UpdaterPool::execute_pool`].
4252 ///
4253 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4254 pub async fn increment(
4255 &self,
4256 col: &str,
4257 by: i64,
4258 pool: &#root::sql::Pool,
4259 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4260 Self::__increment_one(self, col, by, pool).await
4261 }
4262
4263 /// Sibling of [`Self::increment`] — atomically
4264 /// decrement this row's `col` by `by`. Eloquent
4265 /// `$model->decrement($col, $by)` parity. Equivalent to
4266 /// `self.increment(col, -by, &pool)`; the separate name
4267 /// keeps call sites readable.
4268 ///
4269 /// # Errors
4270 /// As [`Self::increment`].
4271 pub async fn decrement(
4272 &self,
4273 col: &str,
4274 by: i64,
4275 pool: &#root::sql::Pool,
4276 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4277 Self::__increment_one(self, col, -by, pool).await
4278 }
4279
4280 /// Bulk-increment: add `by` to `col` on every row of the
4281 /// table. Eloquent `Model::query()->increment($col, $by)`
4282 /// parity. Use for counters, score adjustments, view
4283 /// rollups.
4284 ///
4285 /// # Errors
4286 /// As [`UpdaterPool::execute_pool`].
4287 ///
4288 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4289 pub async fn increment_each(
4290 col: &str,
4291 by: i64,
4292 pool: &#root::sql::Pool,
4293 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4294 Self::__increment_all(col, by, pool).await
4295 }
4296
4297 /// Sibling of [`Self::increment_each`] — bulk-decrement.
4298 ///
4299 /// # Errors
4300 /// As [`Self::increment_each`].
4301 pub async fn decrement_each(
4302 col: &str,
4303 by: i64,
4304 pool: &#root::sql::Pool,
4305 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4306 Self::__increment_all(col, -by, pool).await
4307 }
4308
4309 /// Internal: forward to
4310 /// [`#root::sql::model_shortcuts::increment_one_pool`].
4311 /// One-line wrapper kept as a per-Model method so the
4312 /// macro's emitted `increment` / `decrement` instance
4313 /// calls don't have to thread `Self` through manually.
4314 #[doc(hidden)]
4315 pub async fn __increment_one(
4316 this: &Self,
4317 col: &str,
4318 by: i64,
4319 pool: &#root::sql::Pool,
4320 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4321 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4322 ::core::clone::Clone::clone(&this.#pk_ident),
4323 );
4324 #root::sql::model_shortcuts::increment_one_pool::<Self>(
4325 ::core::stringify!(#pk_ident),
4326 _pk_val,
4327 col,
4328 by,
4329 pool,
4330 )
4331 .await
4332 }
4333
4334 /// Internal: forward to
4335 /// [`#root::sql::model_shortcuts::increment_all_pool`].
4336 #[doc(hidden)]
4337 pub async fn __increment_all(
4338 col: &str,
4339 by: i64,
4340 pool: &#root::sql::Pool,
4341 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4342 #root::sql::model_shortcuts::increment_all_pool::<Self>(col, by, pool).await
4343 }
4344
4345 /// Internal: forward to
4346 /// [`#root::sql::model_shortcuts::resolve_col`].
4347 #[doc(hidden)]
4348 pub fn __resolve_col(
4349 col: &str,
4350 ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4351 #root::sql::model_shortcuts::resolve_col::<Self>(col)
4352 }
4353
4354 /// Internal: forward to
4355 /// [`#root::sql::model_shortcuts::add_signed_expr`].
4356 #[doc(hidden)]
4357 #[must_use]
4358 pub fn __add_signed_expr(
4359 col_static: &'static str,
4360 signed_by: i64,
4361 ) -> #root::core::Expr {
4362 #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4363 }
4364
4365 /// Re-SELECT this row by its primary key and return a
4366 /// **new** instance with the freshly-fetched fields.
4367 /// Eloquent `Model::fresh()` parity — non-mutating
4368 /// counterpart of [`Self::refresh_from_db_pool`].
4369 ///
4370 /// Returns `Ok(None)` when the row was deleted
4371 /// concurrently — vs [`Self::refresh_from_db_pool`]
4372 /// which surfaces that as `RowNotFound` because
4373 /// in-place mutation has nothing to write to.
4374 ///
4375 /// Useful when you want to compare the in-memory
4376 /// instance against the persisted state (audit-style
4377 /// diffs, conflict detection) without mutating the
4378 /// reference you already hold.
4379 ///
4380 /// # Errors
4381 /// As [`FetcherPool::fetch`].
4382 ///
4383 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4384 pub async fn fresh(
4385 &self,
4386 pool: &#root::sql::Pool,
4387 ) -> ::core::result::Result<
4388 ::core::option::Option<Self>,
4389 #root::sql::ExecError,
4390 > {
4391 use #root::sql::FetcherPool as _;
4392 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4393 ::core::clone::Clone::clone(&self.#pk_ident),
4394 );
4395 let _rows: ::std::vec::Vec<Self> =
4396 #root::query::QuerySet::<Self>::default()
4397 .filter(::core::stringify!(#pk_ident), _pk_val)
4398 .limit(1)
4399 .fetch(pool)
4400 .await?;
4401 ::core::result::Result::Ok(_rows.into_iter().next())
4402 }
4403
4404 #replicate_doc
4405 #[must_use]
4406 pub fn replicate(&self) -> Self {
4407 Self {
4408 #pk_clone_token,
4409 #( #other_field_clones, )*
4410 }
4411 }
4412
4413 #first_method
4414
4415 #last_method
4416
4417 /// Throwing counterpart of [`Self::first_pool`] —
4418 /// errors with `RowNotFound` when the table is empty.
4419 /// Eloquent `Model::firstOrFail()` parity.
4420 ///
4421 /// # Errors
4422 /// As [`Self::first_pool`]; additionally
4423 /// [`sqlx::Error::RowNotFound`] on empty tables.
4424 ///
4425 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4426 pub async fn first_or_fail(
4427 pool: &#root::sql::Pool,
4428 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4429 // Route through the queryset rather than `Self::first`,
4430 // which is suppressed on models with a field named
4431 // `first` (field/shortcut collision guard).
4432 match #root::query::QuerySet::<Self>::default().first(pool).await? {
4433 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4434 ::core::option::Option::None => ::core::result::Result::Err(
4435 #root::sql::ExecError::Driver(
4436 #root::sql::sqlx::Error::RowNotFound,
4437 ),
4438 ),
4439 }
4440 }
4441
4442 /// Single-column projection — `SELECT <col> FROM
4443 /// <table>`. Returns `Vec<U>` where each element is the
4444 /// decoded value of the column. Eloquent
4445 /// `Model::pluck($column)` / Django
4446 /// `Model.objects.values_list('col', flat=True)` parity.
4447 ///
4448 /// Thin wrapper over `QuerySet::<Self>::default()
4449 /// .values_list_flat(col).fetch::<U>(pool)`. `U` must
4450 /// be decodable from the column's SQL type on every
4451 /// dialect the binary targets (common picks: `i64` /
4452 /// `i32` / `String` / `bool` / `f64`).
4453 ///
4454 /// # Errors
4455 /// As `ValuesFlatQuerySet::fetch`.
4456 pub async fn pluck<U>(
4457 col: &'static str,
4458 pool: &#root::sql::Pool,
4459 ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4460 where
4461 U: #root::sql::MaybePgScalar
4462 + #root::sql::MaybeMyScalar
4463 + #root::sql::MaybeSqliteScalar
4464 + ::core::marker::Send
4465 + ::core::marker::Unpin,
4466 {
4467 #root::query::QuerySet::<Self>::default()
4468 .values_list_flat(col)
4469 .fetch::<U>(pool)
4470 .await
4471 }
4472
4473 /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4474 /// stream every row of this model in batches of `n`,
4475 /// invoking the callback once per batch. Stable PK-ASC
4476 /// ordering so the LIMIT/OFFSET pagination is
4477 /// deterministic across drivers.
4478 ///
4479 /// The callback is async — it can do further DB work
4480 /// (writes, related-row lookups, queue dispatch) per
4481 /// batch. Return `Err(...)` from the callback to abort
4482 /// the iteration early; the error bubbles up.
4483 ///
4484 /// **When to use**: bulk processing flows that can't
4485 /// fit the whole table in memory — sending newsletters,
4486 /// running data migrations, computing summary
4487 /// statistics. For small / known-bounded tables, plain
4488 /// `Self::all(&pool)` is simpler.
4489 ///
4490 /// **Caveat**: ascending OFFSET pagination is O(N²) on
4491 /// large tables. For multi-million-row scans prefer
4492 /// keyset-by-PK (the standard "WHERE id > last_seen"
4493 /// shape) over `chunk(...)`.
4494 ///
4495 /// Skipped on models without a primary key — chunking
4496 /// needs a stable order to avoid skipping / repeating
4497 /// rows across batches.
4498 pub async fn chunk<F, Fut>(
4499 n: i64,
4500 pool: &#root::sql::Pool,
4501 mut cb: F,
4502 ) -> ::core::result::Result<(), #root::sql::ExecError>
4503 where
4504 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4505 Fut: ::core::future::Future<
4506 Output = ::core::result::Result<(), #root::sql::ExecError>,
4507 >,
4508 {
4509 use #root::sql::FetcherPool as _;
4510 let pk_col = match Self::primary_key_column() {
4511 ::core::option::Option::Some(c) => c,
4512 ::core::option::Option::None => {
4513 return ::core::result::Result::Ok(());
4514 }
4515 };
4516 let mut offset: i64 = 0;
4517 loop {
4518 let rows: ::std::vec::Vec<Self> =
4519 #root::query::QuerySet::<Self>::default()
4520 .order_by(&[(pk_col, false)])
4521 .limit(n)
4522 .offset(offset)
4523 .fetch(pool)
4524 .await?;
4525 if rows.is_empty() {
4526 return ::core::result::Result::Ok(());
4527 }
4528 let len = rows.len() as i64;
4529 cb(rows).await?;
4530 if len < n {
4531 return ::core::result::Result::Ok(());
4532 }
4533 offset += n;
4534 }
4535 }
4536
4537 /// Eloquent `Model::chunkById($n, fn (...))` — same
4538 /// per-batch callback shape as [`Self::chunk`], but uses
4539 /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4540 /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4541 /// — the right choice for multi-million-row sweeps.
4542 ///
4543 /// Requires the primary key to be a signed integer type
4544 /// (`i64` / `i32`); the keyset comparison rides on
4545 /// `__rustango_pk_value()` lowering through
4546 /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4547 /// non-integer PKs (UUID, String) — those should use
4548 /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4549 /// keyset loop.
4550 ///
4551 /// Callback errors abort iteration; the error bubbles up
4552 /// unchanged. Empty table → callback invoked zero times.
4553 pub async fn chunk_by_id<F, Fut>(
4554 n: i64,
4555 pool: &#root::sql::Pool,
4556 mut cb: F,
4557 ) -> ::core::result::Result<(), #root::sql::ExecError>
4558 where
4559 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4560 Fut: ::core::future::Future<
4561 Output = ::core::result::Result<(), #root::sql::ExecError>,
4562 >,
4563 {
4564 use #root::sql::FetcherPool as _;
4565 let pk_col = match Self::primary_key_column() {
4566 ::core::option::Option::Some(c) => c,
4567 ::core::option::Option::None => {
4568 return ::core::result::Result::Ok(());
4569 }
4570 };
4571 // Track the largest PK seen so the next batch picks
4572 // up from there. `i64::MIN` as the sentinel — the
4573 // very first iteration's `> MIN` matches every row,
4574 // so the loop entry is uniform with subsequent
4575 // iterations.
4576 let mut last_seen: i64 = i64::MIN;
4577 loop {
4578 let key = ::std::format!("{}__gt", pk_col);
4579 let rows: ::std::vec::Vec<Self> =
4580 #root::query::QuerySet::<Self>::default()
4581 .filter(key.as_str(), last_seen)
4582 .order_by(&[(pk_col, false)])
4583 .limit(n)
4584 .fetch(pool)
4585 .await?;
4586 if rows.is_empty() {
4587 return ::core::result::Result::Ok(());
4588 }
4589 let len = rows.len() as i64;
4590 // Capture the last row's PK BEFORE moving rows
4591 // into the callback.
4592 let max_pk = match rows
4593 .last()
4594 .map(|r| r.__rustango_pk_value())
4595 {
4596 ::core::option::Option::Some(
4597 #root::core::SqlValue::I64(v),
4598 ) => v,
4599 ::core::option::Option::Some(
4600 #root::core::SqlValue::I32(v),
4601 ) => i64::from(v),
4602 _ => return ::core::result::Result::Ok(()),
4603 };
4604 cb(rows).await?;
4605 if len < n {
4606 return ::core::result::Result::Ok(());
4607 }
4608 last_seen = max_pk;
4609 }
4610 }
4611
4612 /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4613 /// per-row callback companion to [`Self::chunk`].
4614 /// Streams every row in keyset-paginated batches of
4615 /// `batch` size, calling `cb` once per row.
4616 ///
4617 /// Inherits the keyset-paginated scan of
4618 /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4619 /// non-integer PKs are silently a no-op).
4620 ///
4621 /// ```ignore
4622 /// Post::each(500, &pool, |p| async move {
4623 /// reindex(p).await?;
4624 /// Ok(())
4625 /// }).await?;
4626 /// ```
4627 ///
4628 /// Return `Err(...)` from the callback to abort
4629 /// iteration; the error bubbles up unchanged.
4630 pub async fn each<F, Fut>(
4631 batch: i64,
4632 pool: &#root::sql::Pool,
4633 mut cb: F,
4634 ) -> ::core::result::Result<(), #root::sql::ExecError>
4635 where
4636 F: ::core::ops::FnMut(Self) -> Fut,
4637 Fut: ::core::future::Future<
4638 Output = ::core::result::Result<(), #root::sql::ExecError>,
4639 >,
4640 {
4641 use #root::sql::FetcherPool as _;
4642 let pk_col = match Self::primary_key_column() {
4643 ::core::option::Option::Some(c) => c,
4644 ::core::option::Option::None => {
4645 return ::core::result::Result::Ok(());
4646 }
4647 };
4648 let mut last_seen: i64 = i64::MIN;
4649 loop {
4650 let key = ::std::format!("{}__gt", pk_col);
4651 let rows: ::std::vec::Vec<Self> =
4652 #root::query::QuerySet::<Self>::default()
4653 .filter(key.as_str(), last_seen)
4654 .order_by(&[(pk_col, false)])
4655 .limit(batch)
4656 .fetch(pool)
4657 .await?;
4658 if rows.is_empty() {
4659 return ::core::result::Result::Ok(());
4660 }
4661 let len = rows.len() as i64;
4662 let max_pk = match rows
4663 .last()
4664 .map(|r| r.__rustango_pk_value())
4665 {
4666 ::core::option::Option::Some(
4667 #root::core::SqlValue::I64(v),
4668 ) => v,
4669 ::core::option::Option::Some(
4670 #root::core::SqlValue::I32(v),
4671 ) => i64::from(v),
4672 _ => return ::core::result::Result::Ok(()),
4673 };
4674 for row in rows {
4675 cb(row).await?;
4676 }
4677 if len < batch {
4678 return ::core::result::Result::Ok(());
4679 }
4680 last_seen = max_pk;
4681 }
4682 }
4683
4684 /// Delete every row of this model — `TRUNCATE TABLE
4685 /// <table> RESTART IDENTITY CASCADE` on Postgres,
4686 /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4687 /// support `TRUNCATE` inside foreign-key constraints
4688 /// or — for SQLite — at all). Eloquent `Model::truncate()`
4689 /// / Django `Model.objects.all().delete()` parity.
4690 ///
4691 /// **Use only in tests / fixture-reset flows.** Production
4692 /// writes through this would silently bypass the
4693 /// `pre_delete` / `post_delete` signals (no per-row hooks
4694 /// fire on a TRUNCATE / bulk DELETE FROM) and lose every
4695 /// row's audit-log entry.
4696 ///
4697 /// # Errors
4698 /// As [`raw_execute_pool`].
4699 ///
4700 /// [`raw_execute_pool`]: rustango::sql::raw_execute_pool
4701 pub async fn truncate(
4702 pool: &#root::sql::Pool,
4703 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4704 let _table = <Self as #root::core::Model>::SCHEMA.table;
4705 let _dialect = pool.dialect();
4706 let _quoted = _dialect.quote_ident(_table);
4707 let _sql = if _dialect.name() == "postgres" {
4708 ::std::format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", _quoted)
4709 } else {
4710 ::std::format!("DELETE FROM {}", _quoted)
4711 };
4712 #root::sql::raw_execute_pool(pool, &_sql, ::std::vec::Vec::new()).await
4713 }
4714
4715 /// Bulk-delete every row whose primary key is in
4716 /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4717 /// Returns the affected row count.
4718 ///
4719 /// Eloquent `Model::destroy([1, 2, 3])` / Django
4720 /// `Model.objects.filter(pk__in=[...]).delete()` parity.
4721 /// Empty `pks` is a no-op (returns 0).
4722 ///
4723 /// Accepts any iterable whose elements are
4724 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4725 /// `[i64; N]`, etc.
4726 ///
4727 /// # Errors
4728 /// As `delete_pool`.
4729 pub async fn destroy<V>(
4730 pks: impl ::core::iter::IntoIterator<Item = V>,
4731 pool: &#root::sql::Pool,
4732 ) -> ::core::result::Result<u64, #root::sql::ExecError>
4733 where
4734 V: ::core::convert::Into<#root::core::SqlValue>,
4735 {
4736 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4737 pks.into_iter().map(::core::convert::Into::into).collect();
4738 if _values.is_empty() {
4739 return ::core::result::Result::Ok(0);
4740 }
4741 let _query = #root::core::DeleteQuery {
4742 model: <Self as #root::core::Model>::SCHEMA,
4743 where_clause: #root::core::WhereExpr::Predicate(
4744 #root::core::Filter {
4745 column: <Self as #root::core::Model>::SCHEMA
4746 .primary_key()
4747 .ok_or_else(|| {
4748 #root::sql::ExecError::Sql(
4749 #root::sql::SqlError::MissingPrimaryKey,
4750 )
4751 })?
4752 .column,
4753 op: #root::core::Op::In,
4754 value: #root::core::SqlValue::List(_values),
4755 },
4756 ),
4757 };
4758 #root::sql::delete_pool(pool, &_query).await
4759 }
4760
4761 /// Fetch every row where `<col> = <val>`. Eloquent
4762 /// `Model::where($col, $val)->get()` / Django
4763 /// `Model.objects.filter(col=val).all()` parity.
4764 ///
4765 /// Thin wrapper over `QuerySet::<Self>::default()
4766 /// .filter(col, val).fetch(pool)`. For one row,
4767 /// use [`Self::first_where_pool`]; for a chain that
4768 /// needs further `.filter()` / `.order_by()` /
4769 /// `.limit()`, drop down to `Self::query().filter(...)`
4770 /// directly.
4771 ///
4772 /// `val` accepts any value `Into<SqlValue>` so plain
4773 /// strings, ints, UUIDs, etc. all work.
4774 ///
4775 /// # Errors
4776 /// As [`FetcherPool::fetch`].
4777 ///
4778 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4779 pub async fn where_(
4780 col: &str,
4781 val: impl ::core::convert::Into<#root::core::SqlValue>,
4782 pool: &#root::sql::Pool,
4783 ) -> ::core::result::Result<
4784 ::std::vec::Vec<Self>,
4785 #root::sql::ExecError,
4786 > {
4787 use #root::sql::FetcherPool as _;
4788 #root::query::QuerySet::<Self>::default()
4789 .filter(col, val)
4790 .fetch(pool)
4791 .await
4792 }
4793
4794 /// Fetch every row where `<col> IN (vals)`. Eloquent
4795 /// `Model::whereIn($col, $vals)->get()` parity. Empty
4796 /// `vals` returns no rows (matches SQL's empty-IN
4797 /// semantics).
4798 ///
4799 /// `vals` accepts any iterable whose items are
4800 /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
4801 /// etc.
4802 ///
4803 /// # Errors
4804 /// As [`FetcherPool::fetch`].
4805 ///
4806 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4807 pub async fn where_in<V>(
4808 col: &str,
4809 vals: impl ::core::iter::IntoIterator<Item = V>,
4810 pool: &#root::sql::Pool,
4811 ) -> ::core::result::Result<
4812 ::std::vec::Vec<Self>,
4813 #root::sql::ExecError,
4814 >
4815 where
4816 V: ::core::convert::Into<#root::core::SqlValue>,
4817 {
4818 use #root::sql::FetcherPool as _;
4819 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4820 vals.into_iter().map(::core::convert::Into::into).collect();
4821 if _values.is_empty() {
4822 return ::core::result::Result::Ok(::std::vec::Vec::new());
4823 }
4824 let _key = ::std::format!("{}__in", col);
4825 #root::query::QuerySet::<Self>::default()
4826 .filter(&_key, #root::core::SqlValue::List(_values))
4827 .fetch(pool)
4828 .await
4829 }
4830
4831 /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
4832 /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
4833 /// `vals` returns every row (matches SQL's empty-NOT-IN
4834 /// semantics — vacuously true for every row).
4835 ///
4836 /// # Errors
4837 /// As [`FetcherPool::fetch`].
4838 ///
4839 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4840 pub async fn where_not_in<V>(
4841 col: &str,
4842 vals: impl ::core::iter::IntoIterator<Item = V>,
4843 pool: &#root::sql::Pool,
4844 ) -> ::core::result::Result<
4845 ::std::vec::Vec<Self>,
4846 #root::sql::ExecError,
4847 >
4848 where
4849 V: ::core::convert::Into<#root::core::SqlValue>,
4850 {
4851 use #root::sql::FetcherPool as _;
4852 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4853 vals.into_iter().map(::core::convert::Into::into).collect();
4854 if _values.is_empty() {
4855 return #root::query::QuerySet::<Self>::default()
4856 .fetch(pool)
4857 .await;
4858 }
4859 let _key = ::std::format!("{}__not_in", col);
4860 #root::query::QuerySet::<Self>::default()
4861 .filter(&_key, #root::core::SqlValue::List(_values))
4862 .fetch(pool)
4863 .await
4864 }
4865
4866 /// Fetch every row where `<col> IS NULL`. Eloquent
4867 /// `Model::whereNull($col)->get()` parity.
4868 ///
4869 /// # Errors
4870 /// As [`FetcherPool::fetch`].
4871 ///
4872 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4873 pub async fn where_null(
4874 col: &str,
4875 pool: &#root::sql::Pool,
4876 ) -> ::core::result::Result<
4877 ::std::vec::Vec<Self>,
4878 #root::sql::ExecError,
4879 > {
4880 use #root::sql::FetcherPool as _;
4881 let _key = ::std::format!("{}__isnull", col);
4882 #root::query::QuerySet::<Self>::default()
4883 .filter(&_key, #root::core::SqlValue::Bool(true))
4884 .fetch(pool)
4885 .await
4886 }
4887
4888 /// Fetch every row where `<col> IS NOT NULL`. Eloquent
4889 /// `Model::whereNotNull($col)->get()` parity.
4890 ///
4891 /// # Errors
4892 /// As [`FetcherPool::fetch`].
4893 ///
4894 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4895 pub async fn where_not_null(
4896 col: &str,
4897 pool: &#root::sql::Pool,
4898 ) -> ::core::result::Result<
4899 ::std::vec::Vec<Self>,
4900 #root::sql::ExecError,
4901 > {
4902 use #root::sql::FetcherPool as _;
4903 let _key = ::std::format!("{}__isnull", col);
4904 #root::query::QuerySet::<Self>::default()
4905 .filter(&_key, #root::core::SqlValue::Bool(false))
4906 .fetch(pool)
4907 .await
4908 }
4909
4910 /// Fetch up to `n` rows in random order. Eloquent
4911 /// `Model::inRandomOrder()->limit($n)->get()` /
4912 /// `Model::query()->inRandomOrder()->get()->take($n)`
4913 /// parity. **Performance caveat**: random ordering
4914 /// forces a full table scan + per-row random key sort;
4915 /// the optimizer cannot use an index. Prefer a
4916 /// `pk >= rand_offset LIMIT N` walk for huge tables.
4917 ///
4918 /// # Errors
4919 /// As [`FetcherPool::fetch`].
4920 ///
4921 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4922 pub async fn random_n(
4923 n: i64,
4924 pool: &#root::sql::Pool,
4925 ) -> ::core::result::Result<
4926 ::std::vec::Vec<Self>,
4927 #root::sql::ExecError,
4928 > {
4929 use #root::sql::FetcherPool as _;
4930 #root::query::QuerySet::<Self>::default()
4931 .order_random()
4932 .limit(n)
4933 .fetch(pool)
4934 .await
4935 }
4936
4937 /// Fetch one row in random order. Eloquent
4938 /// `Model::inRandomOrder()->first()` parity. Same
4939 /// performance caveat as [`Self::random_n_pool`].
4940 ///
4941 /// # Errors
4942 /// As [`FetcherPool::fetch`].
4943 ///
4944 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4945 pub async fn random(
4946 pool: &#root::sql::Pool,
4947 ) -> ::core::result::Result<
4948 ::core::option::Option<Self>,
4949 #root::sql::ExecError,
4950 > {
4951 ::core::result::Result::Ok(
4952 Self::random_n(1, pool).await?.into_iter().next(),
4953 )
4954 }
4955
4956 /// Fetch every row ordered ASC by `field`. Eloquent
4957 /// `Model::oldest($field)->get()` parity — the multi-row
4958 /// counterpart of [`Self::earliest_pool`].
4959 ///
4960 /// # Errors
4961 /// As [`FetcherPool::fetch`].
4962 ///
4963 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4964 pub async fn oldest(
4965 field: &str,
4966 pool: &#root::sql::Pool,
4967 ) -> ::core::result::Result<
4968 ::std::vec::Vec<Self>,
4969 #root::sql::ExecError,
4970 > {
4971 use #root::sql::FetcherPool as _;
4972 #root::query::QuerySet::<Self>::default()
4973 .order_by(&[(field, false)])
4974 .fetch(pool)
4975 .await
4976 }
4977
4978 /// Fetch every row ordered DESC by `field`. Eloquent
4979 /// `Model::latest($field)->get()` parity — the multi-row
4980 /// counterpart of [`Self::latest_pool`].
4981 ///
4982 /// # Errors
4983 /// As [`FetcherPool::fetch`].
4984 ///
4985 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4986 pub async fn newest(
4987 field: &str,
4988 pool: &#root::sql::Pool,
4989 ) -> ::core::result::Result<
4990 ::std::vec::Vec<Self>,
4991 #root::sql::ExecError,
4992 > {
4993 use #root::sql::FetcherPool as _;
4994 #root::query::QuerySet::<Self>::default()
4995 .order_by(&[(field, true)])
4996 .fetch(pool)
4997 .await
4998 }
4999
5000 /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5001 /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5002 /// Routes through the existing `__year` lookup suffix
5003 /// (issue #829).
5004 ///
5005 /// # Errors
5006 /// As [`FetcherPool::fetch`].
5007 ///
5008 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5009 pub async fn where_year(
5010 col: &str,
5011 year: i64,
5012 pool: &#root::sql::Pool,
5013 ) -> ::core::result::Result<
5014 ::std::vec::Vec<Self>,
5015 #root::sql::ExecError,
5016 > {
5017 use #root::sql::FetcherPool as _;
5018 let _key = ::std::format!("{}__year", col);
5019 #root::query::QuerySet::<Self>::default()
5020 .filter(&_key, #root::core::SqlValue::I64(year))
5021 .fetch(pool)
5022 .await
5023 }
5024
5025 /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5026 /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5027 /// `month` is 1–12.
5028 ///
5029 /// # Errors
5030 /// As [`FetcherPool::fetch`].
5031 ///
5032 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5033 pub async fn where_month(
5034 col: &str,
5035 month: i64,
5036 pool: &#root::sql::Pool,
5037 ) -> ::core::result::Result<
5038 ::std::vec::Vec<Self>,
5039 #root::sql::ExecError,
5040 > {
5041 use #root::sql::FetcherPool as _;
5042 let _key = ::std::format!("{}__month", col);
5043 #root::query::QuerySet::<Self>::default()
5044 .filter(&_key, #root::core::SqlValue::I64(month))
5045 .fetch(pool)
5046 .await
5047 }
5048
5049 /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5050 /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5051 /// `day` is 1–31.
5052 ///
5053 /// # Errors
5054 /// As [`FetcherPool::fetch`].
5055 ///
5056 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5057 pub async fn where_day(
5058 col: &str,
5059 day: i64,
5060 pool: &#root::sql::Pool,
5061 ) -> ::core::result::Result<
5062 ::std::vec::Vec<Self>,
5063 #root::sql::ExecError,
5064 > {
5065 use #root::sql::FetcherPool as _;
5066 let _key = ::std::format!("{}__day", col);
5067 #root::query::QuerySet::<Self>::default()
5068 .filter(&_key, #root::core::SqlValue::I64(day))
5069 .fetch(pool)
5070 .await
5071 }
5072
5073 /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5074 /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5075 /// `hour` is 0–23.
5076 ///
5077 /// # Errors
5078 /// As [`FetcherPool::fetch`].
5079 ///
5080 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5081 pub async fn where_hour(
5082 col: &str,
5083 hour: i64,
5084 pool: &#root::sql::Pool,
5085 ) -> ::core::result::Result<
5086 ::std::vec::Vec<Self>,
5087 #root::sql::ExecError,
5088 > {
5089 use #root::sql::FetcherPool as _;
5090 let _key = ::std::format!("{}__hour", col);
5091 #root::query::QuerySet::<Self>::default()
5092 .filter(&_key, #root::core::SqlValue::I64(hour))
5093 .fetch(pool)
5094 .await
5095 }
5096
5097 /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5098 /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5099 /// `minute` is 0–59.
5100 ///
5101 /// # Errors
5102 /// As [`FetcherPool::fetch`].
5103 ///
5104 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5105 pub async fn where_minute(
5106 col: &str,
5107 minute: i64,
5108 pool: &#root::sql::Pool,
5109 ) -> ::core::result::Result<
5110 ::std::vec::Vec<Self>,
5111 #root::sql::ExecError,
5112 > {
5113 use #root::sql::FetcherPool as _;
5114 let _key = ::std::format!("{}__minute", col);
5115 #root::query::QuerySet::<Self>::default()
5116 .filter(&_key, #root::core::SqlValue::I64(minute))
5117 .fetch(pool)
5118 .await
5119 }
5120
5121 /// Fetch every row where `<col> LIKE <pattern>` —
5122 /// caller-supplied pattern (must include `%` / `_`
5123 /// wildcards manually). Eloquent `Model::whereLike`
5124 /// parity.
5125 ///
5126 /// # Errors
5127 /// As [`FetcherPool::fetch`].
5128 ///
5129 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5130 pub async fn where_like(
5131 col: &str,
5132 pattern: impl ::core::convert::Into<::std::string::String>,
5133 pool: &#root::sql::Pool,
5134 ) -> ::core::result::Result<
5135 ::std::vec::Vec<Self>,
5136 #root::sql::ExecError,
5137 > {
5138 use #root::sql::FetcherPool as _;
5139 let _key = ::std::format!("{}__like", col);
5140 #root::query::QuerySet::<Self>::default()
5141 .filter(
5142 &_key,
5143 #root::core::SqlValue::String(pattern.into()),
5144 )
5145 .fetch(pool)
5146 .await
5147 }
5148
5149 /// Fetch every row where `<col> ILIKE <pattern>` —
5150 /// case-insensitive LIKE (PG native, MySQL/SQLite
5151 /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5152 ///
5153 /// # Errors
5154 /// As [`FetcherPool::fetch`].
5155 ///
5156 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5157 pub async fn where_ilike(
5158 col: &str,
5159 pattern: impl ::core::convert::Into<::std::string::String>,
5160 pool: &#root::sql::Pool,
5161 ) -> ::core::result::Result<
5162 ::std::vec::Vec<Self>,
5163 #root::sql::ExecError,
5164 > {
5165 use #root::sql::FetcherPool as _;
5166 let _key = ::std::format!("{}__ilike", col);
5167 #root::query::QuerySet::<Self>::default()
5168 .filter(
5169 &_key,
5170 #root::core::SqlValue::String(pattern.into()),
5171 )
5172 .fetch(pool)
5173 .await
5174 }
5175
5176 /// Fetch every row where `<col>` starts with `prefix`
5177 /// (auto-appends `%`). Django `__startswith` / Eloquent
5178 /// `whereLike("col", "$prefix%")` parity.
5179 ///
5180 /// # Errors
5181 /// As [`FetcherPool::fetch`].
5182 ///
5183 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5184 pub async fn where_starts_with(
5185 col: &str,
5186 prefix: impl ::core::convert::Into<::std::string::String>,
5187 pool: &#root::sql::Pool,
5188 ) -> ::core::result::Result<
5189 ::std::vec::Vec<Self>,
5190 #root::sql::ExecError,
5191 > {
5192 use #root::sql::FetcherPool as _;
5193 let _key = ::std::format!("{}__startswith", col);
5194 #root::query::QuerySet::<Self>::default()
5195 .filter(
5196 &_key,
5197 #root::core::SqlValue::String(prefix.into()),
5198 )
5199 .fetch(pool)
5200 .await
5201 }
5202
5203 /// Fetch every row where `<col>` ends with `suffix`
5204 /// (auto-prepends `%`). Django `__endswith` / Eloquent
5205 /// `whereLike("col", "%$suffix")` parity.
5206 ///
5207 /// # Errors
5208 /// As [`FetcherPool::fetch`].
5209 ///
5210 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5211 pub async fn where_ends_with(
5212 col: &str,
5213 suffix: impl ::core::convert::Into<::std::string::String>,
5214 pool: &#root::sql::Pool,
5215 ) -> ::core::result::Result<
5216 ::std::vec::Vec<Self>,
5217 #root::sql::ExecError,
5218 > {
5219 use #root::sql::FetcherPool as _;
5220 let _key = ::std::format!("{}__endswith", col);
5221 #root::query::QuerySet::<Self>::default()
5222 .filter(
5223 &_key,
5224 #root::core::SqlValue::String(suffix.into()),
5225 )
5226 .fetch(pool)
5227 .await
5228 }
5229
5230 /// Fetch every row where `<col>` contains `substr`
5231 /// (auto-wraps with `%`). Django `__contains` /
5232 /// Eloquent `whereLike("col", "%$substr%")` parity.
5233 ///
5234 /// # Errors
5235 /// As [`FetcherPool::fetch`].
5236 ///
5237 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5238 pub async fn where_contains(
5239 col: &str,
5240 substr: impl ::core::convert::Into<::std::string::String>,
5241 pool: &#root::sql::Pool,
5242 ) -> ::core::result::Result<
5243 ::std::vec::Vec<Self>,
5244 #root::sql::ExecError,
5245 > {
5246 use #root::sql::FetcherPool as _;
5247 let _key = ::std::format!("{}__contains", col);
5248 #root::query::QuerySet::<Self>::default()
5249 .filter(
5250 &_key,
5251 #root::core::SqlValue::String(substr.into()),
5252 )
5253 .fetch(pool)
5254 .await
5255 }
5256
5257 /// Fetch every row where `<col> > val`. Eloquent
5258 /// `Model::where($col, ">", $val)->get()` parity.
5259 ///
5260 /// # Errors
5261 /// As [`FetcherPool::fetch`].
5262 ///
5263 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5264 pub async fn where_gt(
5265 col: &str,
5266 val: impl ::core::convert::Into<#root::core::SqlValue>,
5267 pool: &#root::sql::Pool,
5268 ) -> ::core::result::Result<
5269 ::std::vec::Vec<Self>,
5270 #root::sql::ExecError,
5271 > {
5272 use #root::sql::FetcherPool as _;
5273 let _key = ::std::format!("{}__gt", col);
5274 #root::query::QuerySet::<Self>::default()
5275 .filter(&_key, val)
5276 .fetch(pool)
5277 .await
5278 }
5279
5280 /// Fetch every row where `<col> >= val`. Eloquent
5281 /// `Model::where($col, ">=", $val)->get()` parity.
5282 ///
5283 /// # Errors
5284 /// As [`FetcherPool::fetch`].
5285 ///
5286 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5287 pub async fn where_gte(
5288 col: &str,
5289 val: impl ::core::convert::Into<#root::core::SqlValue>,
5290 pool: &#root::sql::Pool,
5291 ) -> ::core::result::Result<
5292 ::std::vec::Vec<Self>,
5293 #root::sql::ExecError,
5294 > {
5295 use #root::sql::FetcherPool as _;
5296 let _key = ::std::format!("{}__gte", col);
5297 #root::query::QuerySet::<Self>::default()
5298 .filter(&_key, val)
5299 .fetch(pool)
5300 .await
5301 }
5302
5303 /// Fetch every row where `<col> < val`. Eloquent
5304 /// `Model::where($col, "<", $val)->get()` parity.
5305 ///
5306 /// # Errors
5307 /// As [`FetcherPool::fetch`].
5308 ///
5309 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5310 pub async fn where_lt(
5311 col: &str,
5312 val: impl ::core::convert::Into<#root::core::SqlValue>,
5313 pool: &#root::sql::Pool,
5314 ) -> ::core::result::Result<
5315 ::std::vec::Vec<Self>,
5316 #root::sql::ExecError,
5317 > {
5318 use #root::sql::FetcherPool as _;
5319 let _key = ::std::format!("{}__lt", col);
5320 #root::query::QuerySet::<Self>::default()
5321 .filter(&_key, val)
5322 .fetch(pool)
5323 .await
5324 }
5325
5326 /// Fetch every row where `<col> <= val`. Eloquent
5327 /// `Model::where($col, "<=", $val)->get()` parity.
5328 ///
5329 /// # Errors
5330 /// As [`FetcherPool::fetch`].
5331 ///
5332 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5333 pub async fn where_lte(
5334 col: &str,
5335 val: impl ::core::convert::Into<#root::core::SqlValue>,
5336 pool: &#root::sql::Pool,
5337 ) -> ::core::result::Result<
5338 ::std::vec::Vec<Self>,
5339 #root::sql::ExecError,
5340 > {
5341 use #root::sql::FetcherPool as _;
5342 let _key = ::std::format!("{}__lte", col);
5343 #root::query::QuerySet::<Self>::default()
5344 .filter(&_key, val)
5345 .fetch(pool)
5346 .await
5347 }
5348
5349 /// Fetch every row where `<col> <> val`. Eloquent
5350 /// `Model::where($col, "!=", $val)->get()` parity.
5351 ///
5352 /// # Errors
5353 /// As [`FetcherPool::fetch`].
5354 ///
5355 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5356 pub async fn where_ne(
5357 col: &str,
5358 val: impl ::core::convert::Into<#root::core::SqlValue>,
5359 pool: &#root::sql::Pool,
5360 ) -> ::core::result::Result<
5361 ::std::vec::Vec<Self>,
5362 #root::sql::ExecError,
5363 > {
5364 use #root::sql::FetcherPool as _;
5365 let _key = ::std::format!("{}__ne", col);
5366 #root::query::QuerySet::<Self>::default()
5367 .filter(&_key, val)
5368 .fetch(pool)
5369 .await
5370 }
5371
5372 /// Fetch every row matching `<col> = val` for ANY of the
5373 /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5374 /// parity. Empty `cols` returns no rows.
5375 ///
5376 /// Resolves each `&str` column to its SCHEMA-registered
5377 /// `&'static str` once and builds a single OR-composed
5378 /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5379 ///
5380 /// # Errors
5381 /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5382 /// when any column is not declared on the model.
5383 ///
5384 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5385 pub async fn where_any(
5386 cols: &[&str],
5387 val: impl ::core::convert::Into<#root::core::SqlValue>,
5388 pool: &#root::sql::Pool,
5389 ) -> ::core::result::Result<
5390 ::std::vec::Vec<Self>,
5391 #root::sql::ExecError,
5392 > {
5393 Self::__where_multi(cols, val, false, pool).await
5394 }
5395
5396 /// Fetch every row matching `<col> = val` for ALL listed
5397 /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5398 /// Empty `cols` returns every row (vacuous AND).
5399 ///
5400 /// # Errors
5401 /// As [`Self::where_any`].
5402 pub async fn where_all(
5403 cols: &[&str],
5404 val: impl ::core::convert::Into<#root::core::SqlValue>,
5405 pool: &#root::sql::Pool,
5406 ) -> ::core::result::Result<
5407 ::std::vec::Vec<Self>,
5408 #root::sql::ExecError,
5409 > {
5410 Self::__where_multi(cols, val, true, pool).await
5411 }
5412
5413 /// Internal: build a Q expression composing `cols` via
5414 /// AND (`all`) or OR (`!all`), then fetch. Backs
5415 /// `where_any` / `where_all`.
5416 #[doc(hidden)]
5417 pub async fn __where_multi(
5418 cols: &[&str],
5419 val: impl ::core::convert::Into<#root::core::SqlValue>,
5420 all: bool,
5421 pool: &#root::sql::Pool,
5422 ) -> ::core::result::Result<
5423 ::std::vec::Vec<Self>,
5424 #root::sql::ExecError,
5425 > {
5426 #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5427 .await
5428 }
5429
5430 /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5431 /// parity / Django `Model.objects.all()[:n]`. PK-ordered
5432 /// is NOT guaranteed without an explicit `order_by` —
5433 /// drop into `Self::query()` for that.
5434 ///
5435 /// # Errors
5436 /// As [`FetcherPool::fetch`].
5437 ///
5438 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5439 pub async fn take(
5440 n: i64,
5441 pool: &#root::sql::Pool,
5442 ) -> ::core::result::Result<
5443 ::std::vec::Vec<Self>,
5444 #root::sql::ExecError,
5445 > {
5446 use #root::sql::FetcherPool as _;
5447 #root::query::QuerySet::<Self>::default()
5448 .limit(n)
5449 .fetch(pool)
5450 .await
5451 }
5452
5453 /// Fetch the page-th window of `per_page` rows
5454 /// (1-indexed). Eloquent
5455 /// `Model::query()->forPage($page, $perPage)->get()`
5456 /// parity. The DB scans an `OFFSET (page - 1) * per_page
5457 /// LIMIT per_page`; for large offsets this is O(N) —
5458 /// prefer keyset pagination via PK on hot paths.
5459 ///
5460 /// # Errors
5461 /// As [`FetcherPool::fetch`].
5462 ///
5463 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5464 pub async fn for_page(
5465 page: i64,
5466 per_page: i64,
5467 pool: &#root::sql::Pool,
5468 ) -> ::core::result::Result<
5469 ::std::vec::Vec<Self>,
5470 #root::sql::ExecError,
5471 > {
5472 use #root::sql::FetcherPool as _;
5473 let _offset = if page > 1 { (page - 1) * per_page } else { 0 };
5474 #root::query::QuerySet::<Self>::default()
5475 .limit(per_page)
5476 .offset(_offset)
5477 .fetch(pool)
5478 .await
5479 }
5480
5481 /// Eloquent `Model::paginate($per_page, $page)` — fetch
5482 /// one page of rows AND the total row count in one
5483 /// call. Returns `(rows, total)`. Two queries: a
5484 /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5485 /// for the total.
5486 ///
5487 /// Useful for paginated UIs that need both the visible
5488 /// rows AND a "Page X of Y" / total-count widget. For
5489 /// large tables prefer keyset pagination + cached count;
5490 /// every call to `paginate` re-counts the full table.
5491 ///
5492 /// Same 1-indexed `page` convention as [`Self::for_page`].
5493 ///
5494 /// # Errors
5495 /// As [`Self::for_page`] and [`Self::count`].
5496 pub async fn paginate(
5497 page: i64,
5498 per_page: i64,
5499 pool: &#root::sql::Pool,
5500 ) -> ::core::result::Result<
5501 (::std::vec::Vec<Self>, i64),
5502 #root::sql::ExecError,
5503 > {
5504 // Route through the queryset rather than `Self::count`:
5505 // the `count` inherent method is suppressed on models
5506 // that declare a field named `count` (field/shortcut
5507 // collision guard), and `paginate` must still compile
5508 // for those models.
5509 let total = {
5510 use #root::sql::CounterPool as _;
5511 #root::query::QuerySet::<Self>::default()
5512 .count(pool)
5513 .await?
5514 };
5515 let rows = Self::for_page(page, per_page, pool).await?;
5516 ::core::result::Result::Ok((rows, total))
5517 }
5518
5519 /// Bulk-update — set `set_col = set_val` on every row
5520 /// matching `where_col = where_val`. Returns affected row
5521 /// count. Eloquent
5522 /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5523 /// parity.
5524 ///
5525 /// For multi-column updates drop into the queryset
5526 /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5527 ///
5528 /// # Errors
5529 /// As [`UpdaterPool::execute_pool`].
5530 ///
5531 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5532 pub async fn update_where(
5533 where_col: &str,
5534 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5535 set_col: &str,
5536 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5537 pool: &#root::sql::Pool,
5538 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5539 use #root::sql::UpdaterPool as _;
5540 #root::query::QuerySet::<Self>::default()
5541 .filter(where_col, where_val)
5542 .update()
5543 .set(set_col, set_val)
5544 .execute_pool(pool)
5545 .await
5546 }
5547
5548 /// Bulk-delete — remove every row matching
5549 /// `where_col = where_val`. Returns affected row count.
5550 /// Eloquent
5551 /// `Model::where($where_col, $where_val)->delete()` parity.
5552 ///
5553 /// For more complex filters drop into the queryset
5554 /// builder + `Self::query().filter(...).delete().execute_pool(&pool)`.
5555 ///
5556 /// # Errors
5557 /// As [`#root::sql::delete_pool`].
5558 pub async fn delete_where(
5559 where_col: &str,
5560 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5561 pool: &#root::sql::Pool,
5562 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5563 let _query = #root::core::DeleteQuery {
5564 model: <Self as #root::core::Model>::SCHEMA,
5565 where_clause: #root::core::WhereExpr::Predicate(
5566 #root::core::Filter {
5567 column: <Self as #root::core::Model>::SCHEMA
5568 .field(where_col)
5569 .ok_or_else(|| {
5570 #root::sql::ExecError::Query(
5571 #root::core::QueryError::UnknownField {
5572 model: <Self as #root::core::Model>::SCHEMA.name,
5573 field: ::std::string::ToString::to_string(where_col),
5574 },
5575 )
5576 })?
5577 .column,
5578 op: #root::core::Op::Eq,
5579 value: ::core::convert::Into::into(where_val),
5580 },
5581 ),
5582 };
5583 #root::sql::delete_pool(pool, &_query).await
5584 }
5585
5586 /// Bulk-update — set `set_col = set_val` on EVERY row of
5587 /// this model's table. **No WHERE clause** — use with
5588 /// care. Eloquent `Model::query()->update([$col => $val])`
5589 /// parity.
5590 ///
5591 /// Use for backfills, one-shot reset flows, etc. The
5592 /// returned count is rows affected.
5593 ///
5594 /// # Errors
5595 /// As [`UpdaterPool::execute_pool`].
5596 ///
5597 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5598 pub async fn update_all(
5599 set_col: &str,
5600 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5601 pool: &#root::sql::Pool,
5602 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5603 use #root::sql::UpdaterPool as _;
5604 #root::query::QuerySet::<Self>::default()
5605 .update()
5606 .set(set_col, set_val)
5607 .execute_pool(pool)
5608 .await
5609 }
5610
5611 /// Fetch every row where `<col> NOT LIKE <pattern>`.
5612 /// Eloquent `Model::whereNotLike` parity. Pattern is
5613 /// passed verbatim — caller controls `%` / `_`.
5614 ///
5615 /// # Errors
5616 /// As [`FetcherPool::fetch`].
5617 ///
5618 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5619 pub async fn where_not_like(
5620 col: &str,
5621 pattern: impl ::core::convert::Into<::std::string::String>,
5622 pool: &#root::sql::Pool,
5623 ) -> ::core::result::Result<
5624 ::std::vec::Vec<Self>,
5625 #root::sql::ExecError,
5626 > {
5627 use #root::sql::FetcherPool as _;
5628 let _key = ::std::format!("{}__not_like", col);
5629 #root::query::QuerySet::<Self>::default()
5630 .filter(
5631 &_key,
5632 #root::core::SqlValue::String(pattern.into()),
5633 )
5634 .fetch(pool)
5635 .await
5636 }
5637
5638 /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5639 /// case-insensitive `NOT LIKE` (PG native, MySQL /
5640 /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5641 ///
5642 /// # Errors
5643 /// As [`FetcherPool::fetch`].
5644 ///
5645 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5646 pub async fn where_not_ilike(
5647 col: &str,
5648 pattern: impl ::core::convert::Into<::std::string::String>,
5649 pool: &#root::sql::Pool,
5650 ) -> ::core::result::Result<
5651 ::std::vec::Vec<Self>,
5652 #root::sql::ExecError,
5653 > {
5654 use #root::sql::FetcherPool as _;
5655 let _key = ::std::format!("{}__not_ilike", col);
5656 #root::query::QuerySet::<Self>::default()
5657 .filter(
5658 &_key,
5659 #root::core::SqlValue::String(pattern.into()),
5660 )
5661 .fetch(pool)
5662 .await
5663 }
5664
5665 /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5666 /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5667 /// parity.
5668 ///
5669 /// # Errors
5670 /// As [`FetcherPool::fetch`].
5671 ///
5672 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5673 pub async fn where_not_between(
5674 col: &str,
5675 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5676 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5677 pool: &#root::sql::Pool,
5678 ) -> ::core::result::Result<
5679 ::std::vec::Vec<Self>,
5680 #root::sql::ExecError,
5681 > {
5682 use #root::sql::FetcherPool as _;
5683 let _key = ::std::format!("{}__not_between", col);
5684 let _vals = #root::core::SqlValue::List(::std::vec![
5685 ::core::convert::Into::into(lo),
5686 ::core::convert::Into::into(hi),
5687 ]);
5688 #root::query::QuerySet::<Self>::default()
5689 .filter(&_key, _vals)
5690 .fetch(pool)
5691 .await
5692 }
5693
5694 /// Returns the SQL table name for this model. Eloquent
5695 /// `$model->getTable()` parity.
5696 #[must_use]
5697 pub fn table_name() -> &'static str {
5698 <Self as #root::core::Model>::SCHEMA.table
5699 }
5700
5701 /// Returns the SQL column name of this model's primary
5702 /// key, or `None` when the model has no
5703 /// `#[rustango(primary_key)]`. Eloquent
5704 /// `$model->getKeyName()` parity.
5705 #[must_use]
5706 pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5707 <Self as #root::core::Model>::SCHEMA
5708 .primary_key()
5709 .map(|f| f.column)
5710 }
5711
5712 /// Fetch every row where `<col> BETWEEN lo AND hi`
5713 /// (inclusive on both ends — same as SQL). Eloquent
5714 /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5715 ///
5716 /// # Errors
5717 /// As [`FetcherPool::fetch`].
5718 ///
5719 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5720 pub async fn where_between(
5721 col: &str,
5722 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5723 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5724 pool: &#root::sql::Pool,
5725 ) -> ::core::result::Result<
5726 ::std::vec::Vec<Self>,
5727 #root::sql::ExecError,
5728 > {
5729 use #root::sql::FetcherPool as _;
5730 let _key = ::std::format!("{}__between", col);
5731 let _vals = #root::core::SqlValue::List(::std::vec![
5732 ::core::convert::Into::into(lo),
5733 ::core::convert::Into::into(hi),
5734 ]);
5735 #root::query::QuerySet::<Self>::default()
5736 .filter(&_key, _vals)
5737 .fetch(pool)
5738 .await
5739 }
5740
5741 /// Fetch the first row where `<col> = <val>`. Returns
5742 /// `Ok(None)` when no row matches. Eloquent
5743 /// `Model::firstWhere($col, $val)` / Django
5744 /// `Model.objects.filter(col=val).first()` parity.
5745 ///
5746 /// Thin wrapper over `QuerySet::<Self>::default()
5747 /// .filter(col, val).first(pool)`. Use this when you
5748 /// want one row identified by a non-PK column (e.g.
5749 /// `User::first_where_pool("email", "x@y.com", &pool)`).
5750 ///
5751 /// `val` accepts any value `Into<SqlValue>` so plain
5752 /// strings, ints, UUIDs, etc. all work.
5753 ///
5754 /// # Errors
5755 /// As `QuerySet::first`.
5756 pub async fn first_where(
5757 col: &str,
5758 val: impl ::core::convert::Into<#root::core::SqlValue>,
5759 pool: &#root::sql::Pool,
5760 ) -> ::core::result::Result<
5761 ::core::option::Option<Self>,
5762 #root::sql::ExecError,
5763 > {
5764 #root::query::QuerySet::<Self>::default()
5765 .filter(col, val)
5766 .first(pool)
5767 .await
5768 }
5769
5770 #value_method
5771
5772 /// Fetch the row with the largest `field` value —
5773 /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
5774 /// `Ok(None)` for an empty table. Eloquent
5775 /// `Model::latest($field)->first()` / Django
5776 /// `Model.objects.latest(field)` (non-throwing) parity.
5777 /// Thin wrapper over `QuerySet::<Self>::default()
5778 /// .latest(field, pool)`.
5779 ///
5780 /// **Field name** is the Rust field ident as a string
5781 /// (not the SQL column). Unknown fields surface as
5782 /// `ExecError::Query(QueryError::UnknownField)` at
5783 /// compile time.
5784 ///
5785 /// # Errors
5786 /// As `QuerySet::latest`.
5787 pub async fn latest(
5788 field: &str,
5789 pool: &#root::sql::Pool,
5790 ) -> ::core::result::Result<
5791 ::core::option::Option<Self>,
5792 #root::sql::ExecError,
5793 > {
5794 #root::query::QuerySet::<Self>::default()
5795 .latest(field, pool)
5796 .await
5797 }
5798
5799 /// Sibling of [`Self::latest_pool`] — fetches the row
5800 /// with the smallest `field` value (`ORDER BY <field>
5801 /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
5802 /// ->first()` / Django `Model.objects.earliest(field)`
5803 /// parity.
5804 ///
5805 /// # Errors
5806 /// As [`Self::latest_pool`].
5807 pub async fn earliest(
5808 field: &str,
5809 pool: &#root::sql::Pool,
5810 ) -> ::core::result::Result<
5811 ::core::option::Option<Self>,
5812 #root::sql::ExecError,
5813 > {
5814 #root::query::QuerySet::<Self>::default()
5815 .earliest(field, pool)
5816 .await
5817 }
5818
5819 #count_method
5820
5821 /// `true` when the table contains at least one row.
5822 /// Eloquent `Model::query()->exists()` / Django
5823 /// `Model.objects.exists()` parity. Thin wrapper over
5824 /// `QuerySet::<Self>::default().exists(pool)`.
5825 ///
5826 /// # Errors
5827 /// As [`ExistsPool::exists`].
5828 ///
5829 /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
5830 pub async fn exists(
5831 pool: &#root::sql::Pool,
5832 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5833 use #root::sql::ExistsPool as _;
5834 #root::query::QuerySet::<Self>::default()
5835 .exists(pool)
5836 .await
5837 }
5838
5839 /// Inverse of [`Self::exists`] — returns `true` when
5840 /// the table has zero rows. Eloquent
5841 /// `Model::doesntExist()` parity.
5842 ///
5843 /// # Errors
5844 /// As [`Self::exists`].
5845 pub async fn doesnt_exist(
5846 pool: &#root::sql::Pool,
5847 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5848 Self::exists(pool).await.map(|e| !e)
5849 }
5850
5851 /// Eloquent `Model::query()->whereKey($pk)->exists()` —
5852 /// `true` when a row with primary key `pk` exists in
5853 /// the table. Sugar over
5854 /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
5855 ///
5856 /// Differs from [`Self::exists`] (which checks "any row
5857 /// in the table") by checking a specific PK existence.
5858 /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
5859 /// because the row is never materialized — the SQL is
5860 /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
5861 ///
5862 /// # Errors
5863 /// As [`#root::sql::ExistsPool::contains_pk`].
5864 pub async fn contains_pk(
5865 pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
5866 pool: &#root::sql::Pool,
5867 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5868 use #root::sql::ExistsPool as _;
5869 #root::query::QuerySet::<Self>::default()
5870 .contains_pk(pool, pk)
5871 .await
5872 }
5873
5874 #sum_method
5875 #avg_method
5876 #min_method
5877 #max_method
5878
5879 /// Internal: forward to
5880 /// [`#root::sql::model_shortcuts::aggregate_one_pool`].
5881 /// Backs `sum` / `avg` / `min` / `max`.
5882 #[doc(hidden)]
5883 pub async fn __aggregate_one_pool<U>(
5884 col: &str,
5885 build: fn(&'static str) -> #root::core::AggregateExpr,
5886 pool: &#root::sql::Pool,
5887 ) -> ::core::result::Result<
5888 ::core::option::Option<U>,
5889 #root::sql::ExecError,
5890 >
5891 where
5892 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
5893 + #root::sql::MaybeMyFromRow
5894 + #root::sql::MaybeSqliteFromRow
5895 + ::core::marker::Send
5896 + ::core::marker::Unpin,
5897 {
5898 #root::sql::model_shortcuts::aggregate_one_pool::<Self, U>(col, build, pool)
5899 .await
5900 }
5901
5902 /// Fetch every row of this model from `pool`. Eloquent
5903 /// `Model::all()` parity — a thin wrapper over
5904 /// `QuerySet::<Self>::default().fetch(pool)`.
5905 ///
5906 /// **Use with care on large tables** — there's no
5907 /// pagination or limit; the entire table is materialized
5908 /// into memory. For anything beyond fixture / lookup
5909 /// tables, page through `QuerySet::<Self>::default()
5910 /// .order_by(...).limit(N).offset(M).fetch(pool)`
5911 /// or stream via `.iterator(chunk_size)`.
5912 ///
5913 /// # Errors
5914 /// As [`FetcherPool::fetch`].
5915 ///
5916 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5917 pub async fn all(
5918 pool: &#root::sql::Pool,
5919 ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
5920 {
5921 use #root::sql::FetcherPool as _;
5922 #root::query::QuerySet::<Self>::default()
5923 .fetch(pool)
5924 .await
5925 }
5926
5927 /// Look up every row whose primary key is in `pks`.
5928 /// Returns the matching rows in **inventory** order — NOT
5929 /// the order of `pks`. Empty `pks` returns an empty
5930 /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
5931 /// with a list) / Django `Model.objects.filter(pk__in=[...])`
5932 /// parity.
5933 ///
5934 /// Thin wrapper over `QuerySet::<Self>::default()
5935 /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
5936 /// Caller-supplied PKs that don't match a row are
5937 /// silently skipped (the returned vec is shorter than
5938 /// the input list). For an order-preserving / "fail
5939 /// when any missing" variant, build the queryset
5940 /// explicitly with `in_bulk` instead.
5941 ///
5942 /// Accepts any iterable whose elements are
5943 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
5944 /// `Vec<Uuid>`, etc.
5945 ///
5946 /// # Errors
5947 /// As [`FetcherPool::fetch`].
5948 ///
5949 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5950 pub async fn find_many<V>(
5951 pks: impl ::core::iter::IntoIterator<Item = V>,
5952 pool: &#root::sql::Pool,
5953 ) -> ::core::result::Result<
5954 ::std::vec::Vec<Self>,
5955 #root::sql::ExecError,
5956 >
5957 where
5958 V: ::core::convert::Into<#root::core::SqlValue>,
5959 {
5960 use #root::sql::FetcherPool as _;
5961 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5962 pks.into_iter().map(::core::convert::Into::into).collect();
5963 if _values.is_empty() {
5964 return ::core::result::Result::Ok(::std::vec::Vec::new());
5965 }
5966 let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
5967 #root::query::QuerySet::<Self>::default()
5968 .filter(&_key, #root::core::SqlValue::List(_values))
5969 .fetch(pool)
5970 .await
5971 }
5972
5973 /// Look up the row whose primary key equals `pk`. Returns
5974 /// `Ok(None)` when no row matches; this is the
5975 /// non-throwing counterpart of Django's `.get(pk=…)`
5976 /// (which raises `DoesNotExist`). Eloquent `Model::find`
5977 /// shape — accepts any value `Into<SqlValue>`.
5978 ///
5979 /// One-liner shortcut for the common
5980 /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
5981 /// .limit(1).fetch(pool).await?.into_iter().next()`
5982 /// dance.
5983 ///
5984 /// # Errors
5985 /// As [`FetcherPool::fetch`].
5986 ///
5987 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5988 pub async fn find(
5989 pk: impl ::core::convert::Into<#root::core::SqlValue>,
5990 pool: &#root::sql::Pool,
5991 ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
5992 {
5993 use #root::sql::FetcherPool as _;
5994 let _pk_val: #root::core::SqlValue = pk.into();
5995 let mut _rows: ::std::vec::Vec<Self> =
5996 #root::query::QuerySet::<Self>::default()
5997 .filter(::core::stringify!(#pk_ident), _pk_val)
5998 .limit(1)
5999 .fetch(pool)
6000 .await?;
6001 ::core::result::Result::Ok(_rows.into_iter().next())
6002 }
6003
6004 /// Look up the row whose primary key equals `pk`. Errors
6005 /// when no row matches — the throwing counterpart of
6006 /// [`Self::find_pool`]. Eloquent `Model::findOrFail` /
6007 /// Django `Model.objects.get(pk=…)` (which raises
6008 /// `DoesNotExist`) parity.
6009 ///
6010 /// Translates the miss into
6011 /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6012 /// so callers can `?`-bubble straight through the typical
6013 /// `ExecError` error chain.
6014 ///
6015 /// # Errors
6016 /// As [`Self::find_pool`]; additionally
6017 /// [`sqlx::Error::RowNotFound`] when no row matches.
6018 ///
6019 /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6020 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6021 pub async fn find_or_fail(
6022 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6023 pool: &#root::sql::Pool,
6024 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6025 match Self::find(pk, pool).await? {
6026 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6027 ::core::option::Option::None => ::core::result::Result::Err(
6028 #root::sql::ExecError::Driver(
6029 #root::sql::sqlx::Error::RowNotFound,
6030 ),
6031 ),
6032 }
6033 }
6034
6035 /// Look up multiple rows by primary key, **failing** if
6036 /// any requested PK is missing. Eloquent
6037 /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6038 /// inventory order (NOT request order — same as
6039 /// [`Self::find_many`]).
6040 ///
6041 /// Empty `pks` returns an empty `Vec` (no rows requested
6042 /// → nothing to fail on).
6043 ///
6044 /// Differs from [`Self::find_many`] in that this method
6045 /// requires every PK to resolve. If even one is missing,
6046 /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6047 /// in `ExecError::Driver`). Rows are deduped at the SQL
6048 /// layer, so passing the same PK twice in `pks` counts as
6049 /// one expected row.
6050 ///
6051 /// # Errors
6052 /// As [`Self::find_many`], plus `RowNotFound` when the
6053 /// returned row count is less than the count of distinct
6054 /// requested PKs.
6055 pub async fn find_many_or_fail<V>(
6056 pks: impl ::core::iter::IntoIterator<Item = V>,
6057 pool: &#root::sql::Pool,
6058 ) -> ::core::result::Result<
6059 ::std::vec::Vec<Self>,
6060 #root::sql::ExecError,
6061 >
6062 where
6063 V: ::core::convert::Into<#root::core::SqlValue>,
6064 {
6065 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6066 pks.into_iter().map(::core::convert::Into::into).collect();
6067 if _values.is_empty() {
6068 return ::core::result::Result::Ok(::std::vec::Vec::new());
6069 }
6070 // Dedup the requested PK list before counting so
6071 // duplicate-PK requests don't false-fail (the SQL
6072 // `IN (...)` clause naturally dedups too).
6073 let mut _seen: ::std::collections::HashSet<
6074 ::std::string::String,
6075 > = ::std::collections::HashSet::new();
6076 for v in &_values {
6077 _seen.insert(v.to_display_string());
6078 }
6079 let _expected = _seen.len();
6080 let _rows = Self::find_many(_values, pool).await?;
6081 if _rows.len() < _expected {
6082 return ::core::result::Result::Err(
6083 #root::sql::ExecError::Driver(
6084 #root::sql::sqlx::Error::RowNotFound,
6085 ),
6086 );
6087 }
6088 ::core::result::Result::Ok(_rows)
6089 }
6090
6091 /// Find by primary key or run `fallback` to produce a
6092 /// default row to return. Eloquent
6093 /// `Model::findOr($pk, fn() => …)` parity.
6094 ///
6095 /// Unlike [`Self::find_or_fail_pool`] (which raises on
6096 /// miss), this is the "give me something sensible"
6097 /// branch: typical use is "fetch the user's row, else
6098 /// fall back to an anonymous/guest stub".
6099 ///
6100 /// `fallback` runs only when no row matches — the DB
6101 /// round-trip happens unconditionally.
6102 ///
6103 /// # Errors
6104 /// As [`Self::find_pool`].
6105 pub async fn find_or<F>(
6106 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6107 pool: &#root::sql::Pool,
6108 fallback: F,
6109 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6110 where
6111 F: ::core::ops::FnOnce() -> Self,
6112 {
6113 ::core::result::Result::Ok(
6114 Self::find(pk, pool).await?.unwrap_or_else(fallback),
6115 )
6116 }
6117
6118 /// Same as [`Self::find_or`] but also returns a `bool`
6119 /// indicating whether the row was found in the DB
6120 /// (`true`) or freshly built from `fallback` (`false`).
6121 /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6122 /// in PHP the returned model also exposes `->exists`;
6123 /// here that's surfaced as the second tuple element.
6124 ///
6125 /// Useful for edit-or-create form handlers where the
6126 /// caller needs to know whether to PATCH or POST when
6127 /// the user submits the form.
6128 ///
6129 /// # Errors
6130 /// As [`Self::find`].
6131 pub async fn find_or_new<F>(
6132 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6133 pool: &#root::sql::Pool,
6134 fallback: F,
6135 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6136 where
6137 F: ::core::ops::FnOnce() -> Self,
6138 {
6139 match Self::find(pk, pool).await? {
6140 ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6141 ::core::option::Option::None => {
6142 ::core::result::Result::Ok((fallback(), false))
6143 }
6144 }
6145 }
6146
6147 /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6148 /// [`Self::find_or_new`] but **persists** the new row
6149 /// when the PK isn't found. Returns `(row, exists: bool)`
6150 /// — `exists=true` when the PK was found,
6151 /// `false` when a fresh row was inserted.
6152 ///
6153 /// ```ignore
6154 /// let (post, found) = Post::find_or_insert(
6155 /// pk,
6156 /// &pool,
6157 /// || Post { id: Auto::default(), title: "new".into() },
6158 /// ).await?;
6159 /// // `found` true → returned existing row;
6160 /// // `found` false → fallback was inserted, `post.id` populated.
6161 /// ```
6162 ///
6163 /// **Caveat**: two concurrent calls that both miss the
6164 /// find can both INSERT, violating uniqueness. Wrap in a
6165 /// transaction or rely on a UNIQUE constraint + handle
6166 /// the conflict error if you need race-free semantics.
6167 ///
6168 /// # Errors
6169 /// As [`Self::find`] and [`Self::save_pool`].
6170 pub async fn find_or_insert<F>(
6171 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6172 pool: &#root::sql::Pool,
6173 fallback: F,
6174 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6175 where
6176 F: ::core::ops::FnOnce() -> Self,
6177 {
6178 if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6179 return ::core::result::Result::Ok((_row, true));
6180 }
6181 let mut _new = fallback();
6182 _new.save_pool(pool).await?;
6183 ::core::result::Result::Ok((_new, false))
6184 }
6185
6186 /// Fetch the first row of the table, or run `fallback`
6187 /// when the table is empty. Eloquent
6188 /// `Model::firstOr(fn() => …)` parity.
6189 ///
6190 /// # Errors
6191 /// As [`Self::first_pool`].
6192 pub async fn first_or<F>(
6193 pool: &#root::sql::Pool,
6194 fallback: F,
6195 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6196 where
6197 F: ::core::ops::FnOnce() -> Self,
6198 {
6199 // Route through the queryset rather than `Self::first`,
6200 // which is suppressed on models with a field named
6201 // `first` (field/shortcut collision guard).
6202 ::core::result::Result::Ok(
6203 #root::query::QuerySet::<Self>::default()
6204 .first(pool)
6205 .await?
6206 .unwrap_or_else(fallback),
6207 )
6208 }
6209
6210 /// Fetch exactly one row matching `<col> = <val>`. Errors
6211 /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6212 /// or more than one matches
6213 /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6214 /// Eloquent `Model::sole($col, $val)` parity.
6215 ///
6216 /// # Errors
6217 /// As [`Self::where_pool`] plus the explicit
6218 /// `RowNotFound` / `MultipleRowsReturned` cases above.
6219 pub async fn sole(
6220 col: &str,
6221 val: impl ::core::convert::Into<#root::core::SqlValue>,
6222 pool: &#root::sql::Pool,
6223 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6224 let mut _rows = Self::where_(col, val, pool).await?;
6225 match _rows.len() {
6226 0 => ::core::result::Result::Err(
6227 #root::sql::ExecError::Driver(
6228 #root::sql::sqlx::Error::RowNotFound,
6229 ),
6230 ),
6231 1 => ::core::result::Result::Ok(_rows.remove(0)),
6232 n => ::core::result::Result::Err(
6233 #root::sql::ExecError::MultipleRowsReturned {
6234 op: "sole",
6235 table: <Self as #root::core::Model>::SCHEMA.name,
6236 count: n,
6237 },
6238 ),
6239 }
6240 }
6241 }
6242 } else {
6243 quote!()
6244 };
6245
6246 // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6247 // the non-audited `_pool` methods but execute against an open
6248 // `PoolTx` so the writes participate in the caller's transaction.
6249 // Auditing inside TX is deferred; these always use the plain
6250 // executor primitives regardless of whether the model is audited.
6251 let tx_insert_method = if fields.has_auto {
6252 let pushes = &fields.insert_pushes;
6253 let returning_cols = &fields.returning_cols;
6254 quote! {
6255 /// Insert this row inside an open transaction, populating
6256 /// any `Auto<T>` PK from the auto-assigned value. Works
6257 /// against any backend that `tx` wraps.
6258 ///
6259 /// # Errors
6260 /// As [`Self::insert_pool`].
6261 pub async fn insert_tx(
6262 &mut self,
6263 tx: &mut #root::sql::PoolTx<'_>,
6264 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6265 let mut _columns: ::std::vec::Vec<&'static str> =
6266 ::std::vec::Vec::new();
6267 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6268 ::std::vec::Vec::new();
6269 #( #pushes )*
6270 let _query = #root::core::InsertQuery {
6271 model: <Self as #root::core::Model>::SCHEMA,
6272 columns: _columns,
6273 values: _values,
6274 returning: ::std::vec![ #( #returning_cols ),* ],
6275 on_conflict: ::core::option::Option::None,
6276 };
6277 let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6278 #root::sql::apply_auto_pk(_result, self)
6279 }
6280 }
6281 } else {
6282 let insert_columns = &fields.insert_columns;
6283 let insert_values = &fields.insert_values;
6284 quote! {
6285 /// Insert this row inside an open transaction.
6286 ///
6287 /// # Errors
6288 /// As [`Self::insert_pool`].
6289 pub async fn insert_tx(
6290 &self,
6291 tx: &mut #root::sql::PoolTx<'_>,
6292 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6293 let _query = #root::core::InsertQuery {
6294 model: <Self as #root::core::Model>::SCHEMA,
6295 columns: ::std::vec![ #( #insert_columns ),* ],
6296 values: ::std::vec![ #( #insert_values ),* ],
6297 returning: ::std::vec::Vec::new(),
6298 on_conflict: ::core::option::Option::None,
6299 };
6300 #root::sql::insert_tx(tx, &_query).await
6301 }
6302 }
6303 };
6304
6305 let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6306 let pk_column_lit = pk_col.as_str();
6307 let assignments = &fields.update_assignments;
6308 let dispatch_unset = if fields.pk_is_auto {
6309 quote! {
6310 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6311 return self.insert_tx(tx).await.map(|()| 1u64);
6312 }
6313 }
6314 } else {
6315 quote!()
6316 };
6317 quote! {
6318 /// Save this row inside an open transaction. `INSERT` when
6319 /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6320 /// PK. Works against any backend that `tx` wraps.
6321 ///
6322 /// # Errors
6323 /// As [`Self::save_pool`].
6324 pub async fn save_tx(
6325 &mut self,
6326 tx: &mut #root::sql::PoolTx<'_>,
6327 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6328 #dispatch_unset
6329 let _query = #root::core::UpdateQuery {
6330 model: <Self as #root::core::Model>::SCHEMA,
6331 set: ::std::vec![ #( #assignments ),* ],
6332 where_clause: #root::core::WhereExpr::Predicate(
6333 #root::core::Filter {
6334 column: #pk_column_lit,
6335 op: #root::core::Op::Eq,
6336 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6337 ::core::clone::Clone::clone(&self.#pk_ident)
6338 ),
6339 }
6340 ),
6341 };
6342 let _affected = #root::sql::update_tx(tx, &_query).await?;
6343 ::core::result::Result::Ok(_affected)
6344 }
6345 }
6346 } else {
6347 quote!()
6348 };
6349
6350 let tx_delete_method = {
6351 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6352 let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6353 if let Some(pk_ident) = pk_ident_for_tx {
6354 quote! {
6355 /// Delete the row identified by this instance's PK
6356 /// inside an open transaction. Works against any backend
6357 /// that `tx` wraps.
6358 ///
6359 /// # Errors
6360 /// As [`Self::delete_pool`].
6361 pub async fn delete_tx(
6362 &self,
6363 tx: &mut #root::sql::PoolTx<'_>,
6364 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6365 let _query = #root::core::DeleteQuery {
6366 model: <Self as #root::core::Model>::SCHEMA,
6367 where_clause: #root::core::WhereExpr::Predicate(
6368 #root::core::Filter {
6369 column: #pk_column_lit,
6370 op: #root::core::Op::Eq,
6371 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6372 ::core::clone::Clone::clone(&self.#pk_ident)
6373 ),
6374 }
6375 ),
6376 };
6377 #root::sql::delete_tx(tx, &_query).await
6378 }
6379 }
6380 } else {
6381 quote!()
6382 }
6383 };
6384
6385 // Update emission captures both BEFORE and AFTER state — runs an
6386 // extra SELECT against `_executor` BEFORE the UPDATE, captures
6387 // each tracked field's prior value, then after the UPDATE diffs
6388 // against the in-memory `&self`. `diff_changes` drops unchanged
6389 // columns so the JSON only contains the actual delta.
6390 //
6391 // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6392 // and binds `_audit_before_pairs`; `audit_update_post` runs
6393 // after the UPDATE and emits the PendingEntry.
6394 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6395 audited_fields
6396 {
6397 if tracked.is_empty() {
6398 (quote!(), quote!())
6399 } else {
6400 let select_cols: String = tracked
6401 .iter()
6402 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
6403 .collect::<Vec<_>>()
6404 .join(", ");
6405 let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6406 let select_cols_lit = select_cols;
6407 let pk_column_lit_for_select = pk_column_for_select;
6408 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6409 if fields.pk_is_auto {
6410 quote!(self.#pk_ident.get().copied().unwrap_or_default())
6411 } else {
6412 quote!(::core::clone::Clone::clone(&self.#pk_ident))
6413 }
6414 } else {
6415 quote!(0_i64)
6416 };
6417 let before_pairs = tracked.iter().map(|c| {
6418 let column_lit = c.column.as_str();
6419 let value_ty = &c.value_ty;
6420 quote! {
6421 (
6422 #column_lit,
6423 match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6424 &_audit_before_row, #column_lit,
6425 ) {
6426 ::core::result::Result::Ok(v) => {
6427 #root::__serde_json::to_value(&v)
6428 .unwrap_or(#root::__serde_json::Value::Null)
6429 }
6430 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6431 },
6432 )
6433 }
6434 });
6435 let after_pairs = tracked.iter().map(|c| {
6436 let column_lit = c.column.as_str();
6437 let ident = &c.ident;
6438 quote! {
6439 (
6440 #column_lit,
6441 #root::__serde_json::to_value(&self.#ident)
6442 .unwrap_or(#root::__serde_json::Value::Null),
6443 )
6444 }
6445 });
6446 let pk_str = audit_pk_to_string.clone();
6447 let pre = quote! {
6448 let _audit_select_sql = ::std::format!(
6449 r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
6450 #select_cols_lit,
6451 <Self as #root::core::Model>::SCHEMA.table,
6452 #pk_column_lit_for_select,
6453 );
6454 let _audit_before_pairs:
6455 ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6456 match #root::sql::sqlx::query(&_audit_select_sql)
6457 .bind(#pk_value_for_bind)
6458 .fetch_optional(&mut *_executor)
6459 .await
6460 {
6461 ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
6462 ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
6463 }
6464 _ => ::core::option::Option::None,
6465 };
6466 };
6467 let post = quote! {
6468 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6469 let _audit_after:
6470 ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
6471 ::std::vec![ #( #after_pairs ),* ];
6472 let _audit_entry = #root::audit::PendingEntry {
6473 entity_table: <Self as #root::core::Model>::SCHEMA.table,
6474 entity_pk: #pk_str,
6475 operation: #root::audit::AuditOp::Update,
6476 source: #root::audit::current_source(),
6477 changes: #root::audit::diff_changes(
6478 &_audit_before,
6479 &_audit_after,
6480 ),
6481 };
6482 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6483 }
6484 };
6485 (pre, post)
6486 }
6487 } else {
6488 (quote!(), quote!())
6489 };
6490
6491 // Bulk-insert audit: capture every row's tracked fields after the
6492 // RETURNING populates each PK, then push one batched INSERT INTO
6493 // audit_log via `emit_many`. One round-trip regardless of N rows.
6494 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6495 let row_pk_str = if let Some((pk_ident, _)) = primary_key {
6496 if fields.pk_is_auto {
6497 quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
6498 } else {
6499 quote!(::std::format!("{}", &_row.#pk_ident))
6500 }
6501 } else {
6502 quote!(::std::string::String::new())
6503 };
6504 let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
6505 let column_lit = c.column.as_str();
6506 let ident = &c.ident;
6507 quote! {
6508 (
6509 #column_lit,
6510 #root::__serde_json::to_value(&_row.#ident)
6511 .unwrap_or(#root::__serde_json::Value::Null),
6512 )
6513 }
6514 });
6515 quote! {
6516 let _audit_source = #root::audit::current_source();
6517 let mut _audit_entries:
6518 ::std::vec::Vec<#root::audit::PendingEntry> =
6519 ::std::vec::Vec::with_capacity(rows.len());
6520 for _row in rows.iter() {
6521 _audit_entries.push(#root::audit::PendingEntry {
6522 entity_table: <Self as #root::core::Model>::SCHEMA.table,
6523 entity_pk: #row_pk_str,
6524 operation: #root::audit::AuditOp::Create,
6525 source: _audit_source.clone(),
6526 changes: #root::audit::snapshot_changes(&[
6527 #( #row_pairs ),*
6528 ]),
6529 });
6530 }
6531 #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6532 }
6533 } else {
6534 quote!()
6535 };
6536
6537 let save_method = if fields.pk_is_auto {
6538 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6539 let pk_column_lit = pk_column.as_str();
6540 let assignments = &fields.update_assignments;
6541 let upsert_cols = &fields.upsert_update_columns;
6542 let upsert_pushes = &fields.insert_pushes;
6543 let upsert_returning = &fields.returning_cols;
6544 let upsert_auto_assigns = &fields.auto_assigns;
6545 // Conflict target: prefer the first declared `unique_together`
6546 // when it exists. Plain `Auto<T>` PKs are server-assigned via
6547 // `BIGSERIAL` and never collide on insert, so a PK-only target
6548 // would silently turn `upsert()` into "always-insert" for
6549 // surrogate-PK models with composite UNIQUE constraints — see
6550 // `RolePermission` / `UserRole` / `UserPermission` in the
6551 // tenancy permission engine. When no `unique_together` is
6552 // declared we keep the PK target (the original behaviour).
6553 let upsert_target_columns: Vec<String> = indexes
6554 .iter()
6555 .find(|i| i.unique && !i.columns.is_empty())
6556 .map(|i| i.columns.clone())
6557 .unwrap_or_else(|| vec![pk_column.clone()]);
6558 let upsert_target_lits = upsert_target_columns
6559 .iter()
6560 .map(String::as_str)
6561 .collect::<Vec<_>>();
6562 let conflict_clause = if fields.upsert_update_columns.is_empty() {
6563 quote!(#root::core::ConflictClause::DoNothing)
6564 } else {
6565 quote!(#root::core::ConflictClause::DoUpdate {
6566 target: ::std::vec![ #( #upsert_target_lits ),* ],
6567 update_columns: ::std::vec![ #( #upsert_cols ),* ],
6568 })
6569 };
6570 Some(quote! {
6571 /// Insert this row if its `Auto<T>` primary key is
6572 /// `Unset`, otherwise update the existing row matching the
6573 /// PK. Mirrors Django's `save()` — caller doesn't need to
6574 /// pick `insert` vs the bulk-update path manually.
6575 ///
6576 /// On the insert branch, populates the PK from `RETURNING`
6577 /// (same behavior as `insert`). On the update branch,
6578 /// writes every non-PK column back; if no row matches the
6579 /// PK, returns `Ok(())` silently.
6580 ///
6581 /// Only generated when the primary key is declared as
6582 /// `Auto<T>`. Models with a manually-managed PK must use
6583 /// `insert` or the QuerySet update builder.
6584 ///
6585 /// # Errors
6586 /// Returns [`#root::sql::ExecError`] for SQL-writing
6587 /// or driver failures.
6588 #[cfg(feature = "postgres")]
6589 pub async fn save(
6590 &mut self,
6591 pool: &#root::sql::sqlx::PgPool,
6592 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6593 #pool_to_save_on
6594 }
6595
6596 /// Like [`Self::save`] but accepts any sqlx executor —
6597 /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6598 /// escape hatch for tenant-scoped writes: schema-mode
6599 /// tenants share the registry pool but rely on a per-
6600 /// checkout `SET search_path`, so passing `&PgPool` would
6601 /// silently hit the wrong schema. Acquire a connection
6602 /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6603 ///
6604 /// # Errors
6605 /// As [`Self::save`].
6606 #[cfg(feature = "postgres")]
6607 pub async fn save_on #executor_generics (
6608 &mut self,
6609 #executor_param,
6610 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6611 #executor_where
6612 {
6613 // #1029 — INSERT writes exactly one row → 1; UPDATE returns
6614 // the rows-affected count (0 when the PK no longer exists,
6615 // the Django 6.0 `Model.NotUpdated` signal).
6616 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6617 return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
6618 }
6619 #audit_update_pre
6620 let _query = #root::core::UpdateQuery {
6621 model: <Self as #root::core::Model>::SCHEMA,
6622 set: ::std::vec![ #( #assignments ),* ],
6623 where_clause: #root::core::WhereExpr::Predicate(
6624 #root::core::Filter {
6625 column: #pk_column_lit,
6626 op: #root::core::Op::Eq,
6627 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6628 ::core::clone::Clone::clone(&self.#pk_ident)
6629 ),
6630 }
6631 ),
6632 };
6633 let _affected = #root::sql::__macro_internals::update_on(
6634 #executor_passes_to_data_write,
6635 &_query,
6636 ).await?;
6637 #audit_update_post
6638 ::core::result::Result::Ok(_affected)
6639 }
6640
6641 /// Per-call override for the audit source. Runs
6642 /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6643 /// scope so the resulting audit entry records `source`
6644 /// instead of the task-local default. Useful for seed
6645 /// scripts and one-off CLI tools that don't sit inside an
6646 /// admin handler. The override applies only to this call;
6647 /// no global state changes.
6648 ///
6649 /// # Errors
6650 /// As [`Self::save_on`].
6651 #[cfg(feature = "postgres")]
6652 pub async fn save_on_with #executor_generics (
6653 &mut self,
6654 #executor_param,
6655 source: #root::audit::AuditSource,
6656 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6657 #executor_where
6658 {
6659 #root::audit::with_source(source, self.save_on(_executor)).await
6660 }
6661
6662 /// Insert this row or update it in-place if the primary key already
6663 /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6664 ///
6665 /// With `Auto::Unset` PK the server assigns a new key and no conflict
6666 /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6667 /// inserted if absent or all non-PK columns are overwritten if present.
6668 ///
6669 /// # Errors
6670 /// As [`Self::insert_on`].
6671 #[cfg(feature = "postgres")]
6672 pub async fn upsert(
6673 &mut self,
6674 pool: &#root::sql::sqlx::PgPool,
6675 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6676 #pool_to_upsert_on
6677 }
6678
6679 /// Like [`Self::upsert`] but accepts any sqlx executor.
6680 /// See [`Self::save_on`] for tenancy-scoped rationale.
6681 ///
6682 /// # Errors
6683 /// As [`Self::upsert`].
6684 #[cfg(feature = "postgres")]
6685 pub async fn upsert_on #executor_generics (
6686 &mut self,
6687 #executor_param,
6688 ) -> ::core::result::Result<(), #root::sql::ExecError>
6689 #executor_where
6690 {
6691 let mut _columns: ::std::vec::Vec<&'static str> =
6692 ::std::vec::Vec::new();
6693 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6694 ::std::vec::Vec::new();
6695 #( #upsert_pushes )*
6696 let query = #root::core::InsertQuery {
6697 model: <Self as #root::core::Model>::SCHEMA,
6698 columns: _columns,
6699 values: _values,
6700 returning: ::std::vec![ #( #upsert_returning ),* ],
6701 on_conflict: ::core::option::Option::Some(#conflict_clause),
6702 };
6703 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
6704 #executor_passes_to_data_write,
6705 &query,
6706 ).await?;
6707 let _returning_row = &_returning_row_v;
6708 #( #upsert_auto_assigns )*
6709 ::core::result::Result::Ok(())
6710 }
6711 })
6712 } else {
6713 None
6714 };
6715
6716 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6717 let pk_column_lit = pk_column.as_str();
6718 // Optional `soft_delete_on` / `restore_on` companions when the
6719 // model has a `#[rustango(soft_delete)]` column. They land
6720 // alongside the regular `delete_on` so callers have both
6721 // options — a hard delete (audit-tracked as a real DELETE) and
6722 // a logical delete (audit-tracked as an UPDATE setting the
6723 // deleted_at column to NOW()).
6724 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6725 let col_lit = col;
6726 let sd_field_ident = fields
6727 .soft_delete_field_ident
6728 .clone()
6729 .expect("soft_delete_column without ident");
6730 quote! {
6731 /// Soft-delete this row by setting its
6732 /// `#[rustango(soft_delete)]` column to `NOW()`.
6733 /// Mirrors Django's `SoftDeleteModel.delete()` shape:
6734 /// the row stays in the table; query helpers can
6735 /// filter it out by checking the column for `IS NOT
6736 /// NULL`.
6737 ///
6738 /// # Errors
6739 /// As [`Self::delete`].
6740 #[cfg(feature = "postgres")]
6741 pub async fn soft_delete_on #executor_generics (
6742 &self,
6743 #executor_param,
6744 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6745 #executor_where
6746 {
6747 let _query = #root::core::UpdateQuery {
6748 model: <Self as #root::core::Model>::SCHEMA,
6749 set: ::std::vec![
6750 #root::core::Assignment {
6751 column: #col_lit,
6752 value: ::core::convert::Into::<#root::core::Expr>::into(
6753 ::core::convert::Into::<#root::core::SqlValue>::into(
6754 #root::__chrono::Utc::now()
6755 )
6756 ),
6757 },
6758 ],
6759 where_clause: #root::core::WhereExpr::Predicate(
6760 #root::core::Filter {
6761 column: #pk_column_lit,
6762 op: #root::core::Op::Eq,
6763 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6764 ::core::clone::Clone::clone(&self.#pk_ident)
6765 ),
6766 }
6767 ),
6768 };
6769 let _affected = #root::sql::__macro_internals::update_on(
6770 #executor_passes_to_data_write,
6771 &_query,
6772 ).await?;
6773 #audit_softdelete_emit
6774 ::core::result::Result::Ok(_affected)
6775 }
6776
6777 /// Inverse of [`Self::soft_delete_on`] — clears the
6778 /// soft-delete column back to NULL so the row is
6779 /// considered live again.
6780 ///
6781 /// # Errors
6782 /// As [`Self::delete`].
6783 #[cfg(feature = "postgres")]
6784 pub async fn restore_on #executor_generics (
6785 &self,
6786 #executor_param,
6787 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6788 #executor_where
6789 {
6790 let _query = #root::core::UpdateQuery {
6791 model: <Self as #root::core::Model>::SCHEMA,
6792 set: ::std::vec![
6793 #root::core::Assignment {
6794 column: #col_lit,
6795 value: ::core::convert::Into::<#root::core::Expr>::into(
6796 #root::core::SqlValue::Null
6797 ),
6798 },
6799 ],
6800 where_clause: #root::core::WhereExpr::Predicate(
6801 #root::core::Filter {
6802 column: #pk_column_lit,
6803 op: #root::core::Op::Eq,
6804 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6805 ::core::clone::Clone::clone(&self.#pk_ident)
6806 ),
6807 }
6808 ),
6809 };
6810 let _affected = #root::sql::__macro_internals::update_on(
6811 #executor_passes_to_data_write,
6812 &_query,
6813 ).await?;
6814 #audit_restore_emit
6815 ::core::result::Result::Ok(_affected)
6816 }
6817
6818 /// Tri-dialect counterpart of [`Self::soft_delete_on`]
6819 /// — takes [`#root::sql::Pool`] and dispatches per
6820 /// backend. Eloquent `Model::delete()` semantics on
6821 /// soft-delete-enabled models (closes #821 partial).
6822 ///
6823 /// Sets the `#[rustango(soft_delete)]` column to
6824 /// `NOW()` on every backend. Query helpers
6825 /// (`QuerySet::active()` / `only_trashed()`,
6826 /// `soft_delete::active_filter` /
6827 /// `compose_with_active`) filter trashed rows out by
6828 /// reading `IS NULL` on the same column.
6829 ///
6830 /// # Errors
6831 /// As [`#root::sql::update_pool`].
6832 pub async fn soft_delete(
6833 &self,
6834 pool: &#root::sql::Pool,
6835 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6836 let _query = #root::core::UpdateQuery {
6837 model: <Self as #root::core::Model>::SCHEMA,
6838 set: ::std::vec![
6839 #root::core::Assignment {
6840 column: #col_lit,
6841 value: ::core::convert::Into::<#root::core::Expr>::into(
6842 ::core::convert::Into::<#root::core::SqlValue>::into(
6843 #root::__chrono::Utc::now()
6844 )
6845 ),
6846 },
6847 ],
6848 where_clause: #root::core::WhereExpr::Predicate(
6849 #root::core::Filter {
6850 column: #pk_column_lit,
6851 op: #root::core::Op::Eq,
6852 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6853 ::core::clone::Clone::clone(&self.#pk_ident)
6854 ),
6855 }
6856 ),
6857 };
6858 #root::sql::update_pool(pool, &_query).await
6859 }
6860
6861 /// Tri-dialect counterpart of [`Self::restore_on`].
6862 /// Clears the `#[rustango(soft_delete)]` column back
6863 /// to `NULL`, marking the row live again. Eloquent
6864 /// `Model::restore()` parity.
6865 ///
6866 /// # Errors
6867 /// As [`#root::sql::update_pool`].
6868 pub async fn restore(
6869 &self,
6870 pool: &#root::sql::Pool,
6871 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6872 let _query = #root::core::UpdateQuery {
6873 model: <Self as #root::core::Model>::SCHEMA,
6874 set: ::std::vec![
6875 #root::core::Assignment {
6876 column: #col_lit,
6877 value: ::core::convert::Into::<#root::core::Expr>::into(
6878 #root::core::SqlValue::Null
6879 ),
6880 },
6881 ],
6882 where_clause: #root::core::WhereExpr::Predicate(
6883 #root::core::Filter {
6884 column: #pk_column_lit,
6885 op: #root::core::Op::Eq,
6886 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6887 ::core::clone::Clone::clone(&self.#pk_ident)
6888 ),
6889 }
6890 ),
6891 };
6892 #root::sql::update_pool(pool, &_query).await
6893 }
6894
6895 /// Hard-delete this row, ignoring the soft-delete
6896 /// column. Eloquent `Model::forceDelete()` parity —
6897 /// the escape hatch when you need to actually purge
6898 /// data (GDPR, fixture cleanup, etc.).
6899 ///
6900 /// Equivalent to [`Self::delete_pool`] (the framework's
6901 /// non-soft delete) — exposed under the Eloquent name
6902 /// for muscle-memory + so soft-delete-enabled models
6903 /// have all three operations (soft / restore / force)
6904 /// in one place.
6905 ///
6906 /// # Errors
6907 /// As [`Self::delete_pool`].
6908 pub async fn force_delete(
6909 &self,
6910 pool: &#root::sql::Pool,
6911 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6912 Self::delete_pool(self, pool).await
6913 }
6914
6915 /// Returns `true` when this row is soft-deleted (its
6916 /// `#[rustango(soft_delete)]` column is currently
6917 /// set — Eloquent `$model->trashed()` parity).
6918 ///
6919 /// Pure in-memory predicate over `&self`; does not
6920 /// hit the database. Useful in admin/template code
6921 /// like `{% if post.trashed() %}…{% endif %}` and in
6922 /// guard clauses on restore/force-delete flows.
6923 pub fn trashed(&self) -> bool {
6924 ::core::option::Option::is_some(&self.#sd_field_ident)
6925 }
6926
6927 /// Fetch every row whose `#[rustango(soft_delete)]`
6928 /// column is `NULL` (a.k.a. the live, non-trashed
6929 /// rows). Eloquent's default `Model::all()` behavior on
6930 /// a soft-delete model (Eloquent auto-scopes trashed
6931 /// rows out; rustango doesn't have auto-scoping yet —
6932 /// see issue #820 — so this is the explicit shortcut).
6933 ///
6934 /// One-liner over `QuerySet::<Self>::default()
6935 /// .active().fetch(pool)`. Closes #821 partial.
6936 ///
6937 /// # Errors
6938 /// As [`#root::sql::FetcherPool::fetch`].
6939 pub async fn active(
6940 pool: &#root::sql::Pool,
6941 ) -> ::core::result::Result<
6942 ::std::vec::Vec<Self>,
6943 #root::sql::ExecError,
6944 > {
6945 use #root::sql::FetcherPool as _;
6946 #root::query::QuerySet::<Self>::default()
6947 .active()
6948 .fetch(pool)
6949 .await
6950 }
6951
6952 /// Fetch ONLY soft-deleted rows. Eloquent
6953 /// `Model::onlyTrashed()->get()` parity — drives the
6954 /// admin "Trash" page, restore flows, GDPR purge
6955 /// scans, etc.
6956 ///
6957 /// One-liner over `QuerySet::<Self>::default()
6958 /// .only_trashed().fetch(pool)`. Closes #821
6959 /// partial.
6960 ///
6961 /// # Errors
6962 /// As [`#root::sql::FetcherPool::fetch`].
6963 pub async fn only_trashed(
6964 pool: &#root::sql::Pool,
6965 ) -> ::core::result::Result<
6966 ::std::vec::Vec<Self>,
6967 #root::sql::ExecError,
6968 > {
6969 use #root::sql::FetcherPool as _;
6970 #root::query::QuerySet::<Self>::default()
6971 .only_trashed()
6972 .fetch(pool)
6973 .await
6974 }
6975
6976 /// Fetch every row, both live and soft-deleted.
6977 /// Eloquent `Model::withTrashed()->get()` parity.
6978 ///
6979 /// Today every queryset already includes trashed rows
6980 /// (rustango has no global-scope tracking yet — issue
6981 /// #820), so this is functionally equivalent to
6982 /// [`Self::all_pool`]. Exposed as a named shortcut so
6983 /// soft-delete-aware code reads `Model::with_trashed_pool`
6984 /// rather than `Model::all_pool` — keeps intent visible
6985 /// in callers and stays correct when auto-scoping lands.
6986 ///
6987 /// Closes #821 partial.
6988 ///
6989 /// # Errors
6990 /// As [`#root::sql::FetcherPool::fetch`].
6991 pub async fn with_trashed(
6992 pool: &#root::sql::Pool,
6993 ) -> ::core::result::Result<
6994 ::std::vec::Vec<Self>,
6995 #root::sql::ExecError,
6996 > {
6997 use #root::sql::FetcherPool as _;
6998 #root::query::QuerySet::<Self>::default()
6999 .with_trashed()
7000 .fetch(pool)
7001 .await
7002 }
7003
7004 }
7005 } else {
7006 quote!()
7007 };
7008 quote! {
7009 /// Delete the row identified by this instance's primary key.
7010 ///
7011 /// Returns the number of rows affected (0 or 1).
7012 ///
7013 /// # Errors
7014 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7015 /// driver failures.
7016 #[cfg(feature = "postgres")]
7017 pub async fn delete(
7018 &self,
7019 pool: &#root::sql::sqlx::PgPool,
7020 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7021 #pool_to_delete_on
7022 }
7023
7024 /// Like [`Self::delete`] but accepts any sqlx executor —
7025 /// for tenant-scoped deletes against an explicitly-acquired
7026 /// connection. See [`Self::save_on`] for the rationale.
7027 ///
7028 /// # Errors
7029 /// As [`Self::delete`].
7030 #[cfg(feature = "postgres")]
7031 pub async fn delete_on #executor_generics (
7032 &self,
7033 #executor_param,
7034 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7035 #executor_where
7036 {
7037 let query = #root::core::DeleteQuery {
7038 model: <Self as #root::core::Model>::SCHEMA,
7039 where_clause: #root::core::WhereExpr::Predicate(
7040 #root::core::Filter {
7041 column: #pk_column_lit,
7042 op: #root::core::Op::Eq,
7043 value: ::core::convert::Into::<#root::core::SqlValue>::into(
7044 ::core::clone::Clone::clone(&self.#pk_ident)
7045 ),
7046 }
7047 ),
7048 };
7049 let _affected = #root::sql::__macro_internals::delete_on(
7050 #executor_passes_to_data_write,
7051 &query,
7052 ).await?;
7053 #audit_delete_emit
7054 ::core::result::Result::Ok(_affected)
7055 }
7056
7057 /// Per-call audit-source override for [`Self::delete_on`].
7058 /// See [`Self::save_on_with`] for shape rationale.
7059 ///
7060 /// # Errors
7061 /// As [`Self::delete_on`].
7062 #[cfg(feature = "postgres")]
7063 pub async fn delete_on_with #executor_generics (
7064 &self,
7065 #executor_param,
7066 source: #root::audit::AuditSource,
7067 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7068 #executor_where
7069 {
7070 #root::audit::with_source(source, self.delete_on(_executor)).await
7071 }
7072 #pool_delete_method
7073 #pool_insert_method
7074 #pool_save_method
7075 #refresh_replicate_methods
7076 #tx_delete_method
7077 #tx_insert_method
7078 #tx_save_method
7079 #soft_delete_methods
7080
7081 /// Returns `true` when `other` represents the same DB
7082 /// row as `self` — i.e. their primary keys compare
7083 /// equal. Eloquent `$model->is($other)` parity.
7084 ///
7085 /// Because both arguments are typed `&Self`, the
7086 /// model/table check is automatic — `Post::is` cannot
7087 /// be invoked against a `Comment` at compile time. Only
7088 /// the PK has to be compared at runtime.
7089 pub fn is(&self, other: &Self) -> bool {
7090 self.#pk_ident == other.#pk_ident
7091 }
7092
7093 /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7094 /// parity.
7095 pub fn is_not(&self, other: &Self) -> bool {
7096 self.#pk_ident != other.#pk_ident
7097 }
7098
7099 /// Returns this row's primary-key value as an
7100 /// [`#root::core::SqlValue`]. Eloquent
7101 /// `$model->getKey()` parity.
7102 ///
7103 /// Useful when you need to thread the PK through a
7104 /// generic `Into<SqlValue>`-bound API without knowing the
7105 /// concrete PK type (`i64` vs `Uuid` vs `String`).
7106 #[must_use]
7107 pub fn get_key(&self) -> #root::core::SqlValue {
7108 ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7109 }
7110
7111 }
7112 });
7113
7114 let insert_method = if fields.has_auto {
7115 let pushes = &fields.insert_pushes;
7116 let returning_cols = &fields.returning_cols;
7117 let auto_assigns = &fields.auto_assigns;
7118 quote! {
7119 /// Insert this row into its table. Skips columns whose
7120 /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7121 /// sequence fills them in, then reads each `Auto` column
7122 /// back via `RETURNING` and stores it on `self`.
7123 ///
7124 /// # Errors
7125 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7126 /// driver failures.
7127 #[cfg(feature = "postgres")]
7128 pub async fn insert(
7129 &mut self,
7130 pool: &#root::sql::sqlx::PgPool,
7131 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7132 #pool_to_insert_on
7133 }
7134
7135 /// Like [`Self::insert`] but accepts any sqlx executor.
7136 /// See [`Self::save_on`] for tenancy-scoped rationale.
7137 ///
7138 /// # Errors
7139 /// As [`Self::insert`].
7140 #[cfg(feature = "postgres")]
7141 pub async fn insert_on #executor_generics (
7142 &mut self,
7143 #executor_param,
7144 ) -> ::core::result::Result<(), #root::sql::ExecError>
7145 #executor_where
7146 {
7147 let mut _columns: ::std::vec::Vec<&'static str> =
7148 ::std::vec::Vec::new();
7149 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7150 ::std::vec::Vec::new();
7151 #( #pushes )*
7152 let query = #root::core::InsertQuery {
7153 model: <Self as #root::core::Model>::SCHEMA,
7154 columns: _columns,
7155 values: _values,
7156 returning: ::std::vec![ #( #returning_cols ),* ],
7157 on_conflict: ::core::option::Option::None,
7158 };
7159 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7160 #executor_passes_to_data_write,
7161 &query,
7162 ).await?;
7163 let _returning_row = &_returning_row_v;
7164 #( #auto_assigns )*
7165 #audit_insert_emit
7166 ::core::result::Result::Ok(())
7167 }
7168
7169 /// Per-call audit-source override for [`Self::insert_on`].
7170 /// See [`Self::save_on_with`] for shape rationale.
7171 ///
7172 /// # Errors
7173 /// As [`Self::insert_on`].
7174 #[cfg(feature = "postgres")]
7175 pub async fn insert_on_with #executor_generics (
7176 &mut self,
7177 #executor_param,
7178 source: #root::audit::AuditSource,
7179 ) -> ::core::result::Result<(), #root::sql::ExecError>
7180 #executor_where
7181 {
7182 #root::audit::with_source(source, self.insert_on(_executor)).await
7183 }
7184 }
7185 } else {
7186 let insert_columns = &fields.insert_columns;
7187 let insert_values = &fields.insert_values;
7188 quote! {
7189 /// Insert this row into its table.
7190 ///
7191 /// # Errors
7192 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7193 /// driver failures.
7194 #[cfg(feature = "postgres")]
7195 pub async fn insert(
7196 &self,
7197 pool: &#root::sql::sqlx::PgPool,
7198 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7199 self.insert_on(pool).await
7200 }
7201
7202 /// Like [`Self::insert`] but accepts any sqlx executor.
7203 /// See [`Self::save_on`] for tenancy-scoped rationale.
7204 ///
7205 /// # Errors
7206 /// As [`Self::insert`].
7207 #[cfg(feature = "postgres")]
7208 pub async fn insert_on<'_c, _E>(
7209 &self,
7210 _executor: _E,
7211 ) -> ::core::result::Result<(), #root::sql::ExecError>
7212 where
7213 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7214 {
7215 let query = #root::core::InsertQuery {
7216 model: <Self as #root::core::Model>::SCHEMA,
7217 columns: ::std::vec![ #( #insert_columns ),* ],
7218 values: ::std::vec![ #( #insert_values ),* ],
7219 returning: ::std::vec::Vec::new(),
7220 on_conflict: ::core::option::Option::None,
7221 };
7222 #root::sql::__macro_internals::insert_on(_executor, &query).await
7223 }
7224 }
7225 };
7226
7227 let bulk_insert_method = if fields.has_auto {
7228 let cols_no_auto = &fields.bulk_columns_no_auto;
7229 let cols_all = &fields.bulk_columns_all;
7230 let pushes_no_auto = &fields.bulk_pushes_no_auto;
7231 let pushes_all = &fields.bulk_pushes_all;
7232 let returning_cols = &fields.returning_cols;
7233 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7234 let uniformity = &fields.bulk_auto_uniformity;
7235 let first_auto_ident = fields
7236 .first_auto_ident
7237 .as_ref()
7238 .expect("has_auto implies first_auto_ident is Some");
7239 quote! {
7240 /// Bulk-insert `rows` in a single round-trip. Every row's
7241 /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7242 /// (sequence fills them in) or uniformly `Auto::Set(_)`
7243 /// (caller-supplied values). Mixed Set/Unset is rejected
7244 /// — call `insert` per row for that case.
7245 ///
7246 /// Empty slice is a no-op. Each row's `Auto` fields are
7247 /// populated from the `RETURNING` clause in input order
7248 /// before this returns.
7249 ///
7250 /// # Errors
7251 /// Returns [`#root::sql::ExecError`] for validation,
7252 /// SQL-writing, mixed-Auto rejection, or driver failures.
7253 #[cfg(feature = "postgres")]
7254 pub async fn bulk_insert(
7255 rows: &mut [Self],
7256 pool: &#root::sql::sqlx::PgPool,
7257 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7258 #pool_to_bulk_insert_on
7259 }
7260
7261 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7262 /// See [`Self::save_on`] for tenancy-scoped rationale.
7263 ///
7264 /// # Errors
7265 /// As [`Self::bulk_insert`].
7266 #[cfg(feature = "postgres")]
7267 pub async fn bulk_insert_on #executor_generics (
7268 rows: &mut [Self],
7269 #executor_param,
7270 ) -> ::core::result::Result<(), #root::sql::ExecError>
7271 #executor_where
7272 {
7273 if rows.is_empty() {
7274 return ::core::result::Result::Ok(());
7275 }
7276 let _first_unset = matches!(
7277 rows[0].#first_auto_ident,
7278 #root::sql::Auto::Unset
7279 );
7280 #( #uniformity )*
7281
7282 let mut _all_rows: ::std::vec::Vec<
7283 ::std::vec::Vec<#root::core::SqlValue>,
7284 > = ::std::vec::Vec::with_capacity(rows.len());
7285 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7286 for _row in rows.iter() {
7287 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7288 ::std::vec::Vec::new();
7289 #( #pushes_no_auto )*
7290 _all_rows.push(_row_vals);
7291 }
7292 ::std::vec![ #( #cols_no_auto ),* ]
7293 } else {
7294 for _row in rows.iter() {
7295 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7296 ::std::vec::Vec::new();
7297 #( #pushes_all )*
7298 _all_rows.push(_row_vals);
7299 }
7300 ::std::vec![ #( #cols_all ),* ]
7301 };
7302
7303 let _query = #root::core::BulkInsertQuery {
7304 model: <Self as #root::core::Model>::SCHEMA,
7305 columns: _columns,
7306 rows: _all_rows,
7307 returning: ::std::vec![ #( #returning_cols ),* ],
7308 on_conflict: ::core::option::Option::None,
7309 };
7310 let _returned = #root::sql::__macro_internals::bulk_insert_on(
7311 #executor_passes_to_data_write,
7312 &_query,
7313 ).await?;
7314 if _returned.len() != rows.len() {
7315 return ::core::result::Result::Err(
7316 #root::sql::ExecError::Sql(
7317 #root::sql::SqlError::BulkInsertReturningMismatch {
7318 expected: rows.len(),
7319 actual: _returned.len(),
7320 }
7321 )
7322 );
7323 }
7324 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7325 #auto_assigns_for_row
7326 }
7327 #audit_bulk_insert_emit
7328 ::core::result::Result::Ok(())
7329 }
7330 }
7331 } else {
7332 let cols_all = &fields.bulk_columns_all;
7333 let pushes_all = &fields.bulk_pushes_all;
7334 quote! {
7335 /// Bulk-insert `rows` in a single round-trip. Every row's
7336 /// fields are written verbatim — there are no `Auto<T>`
7337 /// fields on this model.
7338 ///
7339 /// Empty slice is a no-op.
7340 ///
7341 /// # Errors
7342 /// Returns [`#root::sql::ExecError`] for validation,
7343 /// SQL-writing, or driver failures.
7344 #[cfg(feature = "postgres")]
7345 pub async fn bulk_insert(
7346 rows: &[Self],
7347 pool: &#root::sql::sqlx::PgPool,
7348 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7349 Self::bulk_insert_on(rows, pool).await
7350 }
7351
7352 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7353 /// See [`Self::save_on`] for tenancy-scoped rationale.
7354 ///
7355 /// # Errors
7356 /// As [`Self::bulk_insert`].
7357 #[cfg(feature = "postgres")]
7358 pub async fn bulk_insert_on<'_c, _E>(
7359 rows: &[Self],
7360 _executor: _E,
7361 ) -> ::core::result::Result<(), #root::sql::ExecError>
7362 where
7363 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7364 {
7365 if rows.is_empty() {
7366 return ::core::result::Result::Ok(());
7367 }
7368 let mut _all_rows: ::std::vec::Vec<
7369 ::std::vec::Vec<#root::core::SqlValue>,
7370 > = ::std::vec::Vec::with_capacity(rows.len());
7371 for _row in rows.iter() {
7372 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7373 ::std::vec::Vec::new();
7374 #( #pushes_all )*
7375 _all_rows.push(_row_vals);
7376 }
7377 let _query = #root::core::BulkInsertQuery {
7378 model: <Self as #root::core::Model>::SCHEMA,
7379 columns: ::std::vec![ #( #cols_all ),* ],
7380 rows: _all_rows,
7381 returning: ::std::vec::Vec::new(),
7382 on_conflict: ::core::option::Option::None,
7383 };
7384 let _ = #root::sql::__macro_internals::bulk_insert_on(_executor, &_query).await?;
7385 ::core::result::Result::Ok(())
7386 }
7387 }
7388 };
7389
7390 // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7391 // (no postgres-feature gate); routes through the existing
7392 // `bulk_insert_pool` + per-dialect conflict writer.
7393 //
7394 // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7395 // sequence picks the PK for fresh inserts; the UPDATE branch never
7396 // touches the Auto column.
7397 let bulk_upsert_pool_method = {
7398 // Pick the "no Auto" columns when the model has Auto fields,
7399 // else every column.
7400 let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7401 (
7402 fields.bulk_columns_no_auto.clone(),
7403 fields.bulk_pushes_no_auto.clone(),
7404 )
7405 } else {
7406 (
7407 fields.bulk_columns_all.clone(),
7408 fields.bulk_pushes_all.clone(),
7409 )
7410 };
7411 quote! {
7412 /// Tri-dialect `bulk_create(update_conflicts=True)` — Django's
7413 /// canonical "import a batch idempotently" shape. Issue #267
7414 /// / T1.5.
7415 ///
7416 /// Per-row values are extracted and lowered into a
7417 /// [`#root::core::BulkInsertQuery`] with
7418 /// `on_conflict = DoUpdate { target, update_columns }`. The
7419 /// writer dispatches per-dialect:
7420 /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7421 /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)` (target ignored — MySQL matches every UNIQUE index)
7422 ///
7423 /// `target` names the column(s) whose unique constraint
7424 /// defines the conflict (typically a `unique` or
7425 /// `unique_together` natural-key column, NOT the `Auto<T>`
7426 /// PK). `update_cols` names the columns to overwrite on
7427 /// conflict — every other column is left untouched on the
7428 /// existing row.
7429 ///
7430 /// Auto-PK rows must all have `Auto::Unset` (the sequence
7431 /// picks the PK on insert; the update path never touches
7432 /// the Auto column). Auto-set rows trigger a hard error.
7433 /// Empty slice is a no-op.
7434 ///
7435 /// # Errors
7436 /// Returns [`#root::sql::ExecError`] for validation,
7437 /// SQL-writing, or driver failures.
7438 pub async fn bulk_upsert_pool(
7439 rows: &[Self],
7440 target: &[&'static str],
7441 update_cols: &[&'static str],
7442 pool: &#root::sql::Pool,
7443 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7444 if rows.is_empty() {
7445 return ::core::result::Result::Ok(());
7446 }
7447 let mut _all_rows: ::std::vec::Vec<
7448 ::std::vec::Vec<#root::core::SqlValue>,
7449 > = ::std::vec::Vec::with_capacity(rows.len());
7450 for _row in rows.iter() {
7451 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7452 ::std::vec::Vec::new();
7453 #( #upsert_pushes )*
7454 _all_rows.push(_row_vals);
7455 }
7456 let _query = #root::core::BulkInsertQuery {
7457 model: <Self as #root::core::Model>::SCHEMA,
7458 columns: ::std::vec![ #( #upsert_cols ),* ],
7459 rows: _all_rows,
7460 returning: ::std::vec::Vec::new(),
7461 on_conflict: ::core::option::Option::Some(
7462 #root::core::ConflictClause::DoUpdate {
7463 target: target.to_vec(),
7464 update_columns: update_cols.to_vec(),
7465 }
7466 ),
7467 };
7468 #root::sql::bulk_insert_pool(pool, &_query).await
7469 }
7470
7471 /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7472 /// skip rows that would violate a unique constraint. Issue
7473 /// #267 / T1.5. Same per-dialect dispatch as
7474 /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7475 /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7476 /// <pivot> = <pivot>` (MySQL no-op write).
7477 ///
7478 /// # Errors
7479 /// As [`Self::bulk_upsert_pool`].
7480 pub async fn bulk_insert_or_ignore_pool(
7481 rows: &[Self],
7482 pool: &#root::sql::Pool,
7483 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7484 if rows.is_empty() {
7485 return ::core::result::Result::Ok(());
7486 }
7487 let mut _all_rows: ::std::vec::Vec<
7488 ::std::vec::Vec<#root::core::SqlValue>,
7489 > = ::std::vec::Vec::with_capacity(rows.len());
7490 for _row in rows.iter() {
7491 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7492 ::std::vec::Vec::new();
7493 #( #upsert_pushes )*
7494 _all_rows.push(_row_vals);
7495 }
7496 let _query = #root::core::BulkInsertQuery {
7497 model: <Self as #root::core::Model>::SCHEMA,
7498 columns: ::std::vec![ #( #upsert_cols ),* ],
7499 rows: _all_rows,
7500 returning: ::std::vec::Vec::new(),
7501 on_conflict: ::core::option::Option::Some(
7502 #root::core::ConflictClause::DoNothing
7503 ),
7504 };
7505 #root::sql::bulk_insert_pool(pool, &_query).await
7506 }
7507 }
7508 };
7509
7510 // Ergonomic `Model::bulk_update(objs, fields)` — Django's
7511 // `QuerySet.bulk_update`. The SQL/IR/executor stack
7512 // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7513 // `write_bulk_update_*` writers) already existed; what was missing
7514 // was the per-model constructor that maps `&[Self]` + a runtime
7515 // column list into rows of `[pk, col_vals…]` so callers don't
7516 // hand-build the IR. Emitted only when the model has a primary key
7517 // (the PK is the join key and can't itself be updated).
7518 let bulk_update_method = match &fields.primary_key {
7519 None => quote! {},
7520 Some((pk_ident, pk_col)) => {
7521 // One pair of match arms per non-PK column: a validation arm
7522 // resolving the runtime name to its `&'static str` column,
7523 // and a value arm pushing that field off the row.
7524 let mut col_arms: Vec<TokenStream2> = Vec::new();
7525 let mut val_arms: Vec<TokenStream2> = Vec::new();
7526 for entry in &fields.column_entries {
7527 if &entry.column == pk_col {
7528 continue;
7529 }
7530 let col = &entry.column;
7531 let ident = &entry.ident;
7532 col_arms.push(quote! { #col => #col, });
7533 val_arms.push(quote! {
7534 #col => _row_vals.push(
7535 ::core::convert::Into::<#root::core::SqlValue>::into(
7536 ::core::clone::Clone::clone(&_o.#ident)
7537 )
7538 ),
7539 });
7540 }
7541 quote! {
7542 /// Django's `QuerySet.bulk_update(objs, fields)` — write
7543 /// per-row-different values for the named `fields` across
7544 /// every object in `objs` in a single statement, matched
7545 /// by primary key.
7546 ///
7547 /// `fields` names the **columns** to update. The primary
7548 /// key identifies each row and cannot itself be updated
7549 /// (pass it and you get
7550 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7551 /// Empty `objs` or `fields` is a no-op returning `0`.
7552 /// Objects whose PK matches no row are simply not updated.
7553 /// Returns the number of rows affected.
7554 ///
7555 /// Tri-dialect: lowers to one
7556 /// [`#root::core::BulkUpdateQuery`] and dispatches
7557 /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7558 /// a CTE + correlated subquery on SQLite, an inner
7559 /// `JOIN (VALUES …)` on MySQL.
7560 ///
7561 /// # Errors
7562 /// [`#root::core::QueryError::UnknownField`] for a name
7563 /// that isn't a column on this model,
7564 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7565 /// `fields` names the PK, or [`#root::sql::ExecError`] for
7566 /// SQL-writing / driver failures.
7567 pub async fn bulk_update(
7568 objs: &[Self],
7569 fields: &[&str],
7570 pool: &#root::sql::Pool,
7571 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7572 if objs.is_empty() || fields.is_empty() {
7573 return ::core::result::Result::Ok(0);
7574 }
7575 let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7576 let mut _update_columns: ::std::vec::Vec<&'static str> =
7577 ::std::vec::Vec::with_capacity(fields.len());
7578 for &_f in fields {
7579 let _col: &'static str = match _f {
7580 #pk_col => {
7581 return ::core::result::Result::Err(
7582 ::core::convert::Into::into(
7583 #root::core::QueryError::BulkUpdatePrimaryKey {
7584 model: _model_name,
7585 field: ::std::string::ToString::to_string(_f),
7586 }
7587 )
7588 );
7589 }
7590 #( #col_arms )*
7591 _ => {
7592 return ::core::result::Result::Err(
7593 ::core::convert::Into::into(
7594 #root::core::QueryError::UnknownField {
7595 model: _model_name,
7596 field: ::std::string::ToString::to_string(_f),
7597 }
7598 )
7599 );
7600 }
7601 };
7602 // A model whose only field is the primary key emits no
7603 // non-PK arms, so every arm above diverges and this push
7604 // is genuinely unreachable — correct, since such a model
7605 // has nothing to bulk-update. Scoped to the statement
7606 // rather than the fn so a real unreachability elsewhere
7607 // in this body still warns. Without it every PK-only
7608 // model warns in the *user's* crate, which they cannot
7609 // act on (#1290).
7610 #[allow(unreachable_code)]
7611 _update_columns.push(_col);
7612 }
7613 let mut _rows: ::std::vec::Vec<
7614 ::std::vec::Vec<#root::core::SqlValue>,
7615 > = ::std::vec::Vec::with_capacity(objs.len());
7616 for _o in objs.iter() {
7617 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7618 ::std::vec::Vec::with_capacity(fields.len() + 1);
7619 // PK first — the writers expect `[pk, …update cols]`.
7620 _row_vals.push(
7621 ::core::convert::Into::<#root::core::SqlValue>::into(
7622 ::core::clone::Clone::clone(&_o.#pk_ident)
7623 )
7624 );
7625 for &_f in fields {
7626 match _f {
7627 #( #val_arms )*
7628 // Unreachable: every name was validated
7629 // against the same arm set above.
7630 _ => {}
7631 }
7632 }
7633 _rows.push(_row_vals);
7634 }
7635 let _query = #root::core::BulkUpdateQuery {
7636 model: <Self as #root::core::Model>::SCHEMA,
7637 update_columns: _update_columns,
7638 rows: _rows,
7639 };
7640 #root::sql::bulk_update_pool(pool, &_query).await
7641 }
7642 }
7643 }
7644 };
7645
7646 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7647 quote! {
7648 /// Hidden runtime accessor for the primary-key value as a
7649 /// [`SqlValue`]. Used by reverse-relation helpers
7650 /// (`<parent>::<child>_set`) emitted from sibling models'
7651 /// FK fields. Not part of the public API.
7652 #[doc(hidden)]
7653 pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7654 ::core::convert::Into::<#root::core::SqlValue>::into(
7655 ::core::clone::Clone::clone(&self.#pk_ident)
7656 )
7657 }
7658 }
7659 });
7660
7661 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7662 quote! {
7663 impl #root::sql::HasPkValue for #struct_name {
7664 fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7665 ::core::convert::Into::<#root::core::SqlValue>::into(
7666 ::core::clone::Clone::clone(&self.#pk_ident)
7667 )
7668 }
7669 }
7670 }
7671 });
7672
7673 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7674
7675 // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7676 // dispatch to the right per-backend body without the macro emitting
7677 // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7678 // so audited models with non-Auto PKs (which still go through
7679 // `insert_one_with_audit` → `apply_auto_pk`) link.
7680 let assign_auto_pk_pool_impl = {
7681 let auto_assigns = &fields.auto_assigns;
7682 // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7683 // so the body is structurally identical to `auto_assigns` —
7684 // only the helper name swaps from `try_get_returning` to
7685 // `try_get_returning_sqlite` so the closure typechecks against
7686 // a `SqliteRow` instead of a `PgRow`.
7687 let auto_assigns_sqlite: Vec<TokenStream2> = fields
7688 .auto_field_idents
7689 .iter()
7690 .map(|(ident, column)| {
7691 quote! {
7692 self.#ident = #root::sql::try_get_returning_sqlite(
7693 _returning_row, #column
7694 )?;
7695 }
7696 })
7697 .collect();
7698 // #1028 — decode each `generated_as` column from the same
7699 // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7700 // own type drives the decode (no `Auto<T>` wrapper).
7701 let generated_assigns: Vec<TokenStream2> = fields
7702 .generated_field_idents
7703 .iter()
7704 .map(|(ident, column)| {
7705 quote! {
7706 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7707 }
7708 })
7709 .collect();
7710 let generated_assigns_sqlite: Vec<TokenStream2> = fields
7711 .generated_field_idents
7712 .iter()
7713 .map(|(ident, column)| {
7714 quote! {
7715 self.#ident = #root::sql::try_get_returning_sqlite(
7716 _returning_row, #column
7717 )?;
7718 }
7719 })
7720 .collect();
7721 let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
7722 // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7723 // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7724 // Auto<Uuid>/etc. fail to link against MySQL (intended —
7725 // those models can't use AUTO_INCREMENT). The trait is only
7726 // touched on the MySQL arm at runtime, so PG-only consumers
7727 // never see the bound failure.
7728 //
7729 // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7730 // Auto<i64> PK + auto_now_add timestamp) errored hard at
7731 // runtime with "multi-column RETURNING". MySQL has no
7732 // multi-column RETURNING semantic and a follow-up SELECT
7733 // would need cross-trait plumbing. Pragmatic shape: succeed
7734 // with the FIRST Auto field populated from LAST_INSERT_ID();
7735 // any other Auto fields stay `Auto::Unset`. Callers that
7736 // need the DB-defaulted timestamp / UUID can re-fetch the
7737 // row by PK after `save_pool`. Fixes the cookbook chapter
7738 // 12 dialect divergence.
7739 let value_ty = fields
7740 .first_auto_value_ty
7741 .as_ref()
7742 .expect("first_auto_value_ty set whenever first_auto_ident is");
7743 quote! {
7744 let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7745 ::rustango_from_mysql_auto_id(_id)?;
7746 self.#first = #root::sql::Auto::Set(_converted);
7747 ::core::result::Result::Ok(())
7748 }
7749 } else {
7750 quote! {
7751 let _ = _id;
7752 ::core::result::Result::Ok(())
7753 }
7754 };
7755 quote! {
7756 impl #root::sql::AssignAutoPkPool for #struct_name {
7757 fn __rustango_assign_from_pg_row(
7758 &mut self,
7759 _returning_row: &#root::sql::PgReturningRow,
7760 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7761 #( #auto_assigns )*
7762 #( #generated_assigns )*
7763 ::core::result::Result::Ok(())
7764 }
7765 fn __rustango_assign_from_mysql_id(
7766 &mut self,
7767 _id: i64,
7768 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7769 #mysql_body
7770 }
7771 fn __rustango_assign_from_sqlite_row(
7772 &mut self,
7773 _returning_row: &#root::sql::SqliteReturningRow,
7774 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7775 #( #auto_assigns_sqlite )*
7776 #( #generated_assigns_sqlite )*
7777 ::core::result::Result::Ok(())
7778 }
7779 }
7780 }
7781 };
7782
7783 let from_aliased_row_inits = &fields.from_aliased_row_inits;
7784 let aliased_row_helper = quote! {
7785 /// Decode a row's aliased target columns (produced by
7786 /// `select_related`'s LEFT JOIN) into a fresh instance of
7787 /// this model. Reads each column via
7788 /// `format!("{prefix}__{col}")`, matching the alias the
7789 /// SELECT writer emitted. Slice 9.0d.
7790 #[doc(hidden)]
7791 #[cfg(feature = "postgres")]
7792 pub fn __rustango_from_aliased_row(
7793 row: &#root::sql::sqlx::postgres::PgRow,
7794 prefix: &str,
7795 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7796 ::core::result::Result::Ok(Self {
7797 #( #from_aliased_row_inits ),*
7798 })
7799 }
7800 };
7801 // v0.23.0-batch8 — MySQL counterpart, gated through the
7802 // cfg-aware macro_rules so PG-only builds expand to nothing.
7803 let aliased_row_helper_my = quote! {
7804 #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7805 #( #from_aliased_row_inits ),*
7806 });
7807 };
7808
7809 // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7810 // pattern + cfg gate on the `sqlite` feature.
7811 let aliased_row_helper_sqlite = quote! {
7812 #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7813 #( #from_aliased_row_inits ),*
7814 });
7815 };
7816
7817 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7818 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7819 let load_related_impl_sqlite =
7820 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7821
7822 // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7823 // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7824 // default `Self::objects()`. Each accessor returns a fresh
7825 // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7826 // methods the user defined.
7827 let extra_manager_fns: Vec<TokenStream2> = manager_fns
7828 .iter()
7829 .map(|fn_ident| {
7830 let model_name_str = struct_name.to_string();
7831 let fn_name_str = fn_ident.to_string();
7832 let doc = format!(
7833 "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7834 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7835 equivalent to `Self::objects()`. Chains with any \
7836 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7837 extension methods."
7838 );
7839 quote! {
7840 #[doc = #doc]
7841 #[must_use]
7842 pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7843 #root::query::QuerySet::new()
7844 }
7845 }
7846 })
7847 .collect();
7848
7849 quote! {
7850 impl #struct_name {
7851 /// Start a new `QuerySet` over this model. Django shape.
7852 #[must_use]
7853 pub fn objects() -> #root::query::QuerySet<#struct_name> {
7854 #root::query::QuerySet::new()
7855 }
7856
7857 /// Eloquent-shape alias of [`Self::objects`]. Returns
7858 /// a fresh `QuerySet<Self>` ready for `.filter()` /
7859 /// `.where_()` / etc. Matches Laravel muscle-memory:
7860 ///
7861 /// ```ignore
7862 /// // Eloquent: Post::query()->where('published', true)
7863 /// // Django: Post.objects.filter(published=True)
7864 /// // rustango: Post::query().filter("published", true)
7865 /// // or: Post::objects().filter("published", true)
7866 /// ```
7867 ///
7868 /// Both names point at the same underlying constructor;
7869 /// neither is preferred.
7870 #[must_use]
7871 pub fn query() -> #root::query::QuerySet<#struct_name> {
7872 #root::query::QuerySet::new()
7873 }
7874
7875 #( #extra_manager_fns )*
7876
7877 #insert_method
7878
7879 #bulk_insert_method
7880
7881 #bulk_upsert_pool_method
7882
7883 #bulk_update_method
7884
7885 #save_method
7886
7887 #pk_methods
7888
7889 #pk_value_helper
7890
7891 #aliased_row_helper
7892
7893 #column_consts
7894 }
7895
7896 #aliased_row_helper_my
7897
7898 #aliased_row_helper_sqlite
7899
7900 #load_related_impl
7901
7902 #load_related_impl_my
7903
7904 #load_related_impl_sqlite
7905
7906 #has_pk_value_impl
7907
7908 #fk_pk_access_impl
7909
7910 #assign_auto_pk_pool_impl
7911 }
7912}
7913
7914/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
7915/// `auto_assigns` but reading from `_returning_row` and writing to
7916/// `_row_mut` instead of `self`.
7917fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
7918 let root = rustango_root();
7919 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
7920 let col_lit = column.as_str();
7921 quote! {
7922 _row_mut.#ident = #root::sql::sqlx::Row::try_get(
7923 _returning_row,
7924 #col_lit,
7925 )?;
7926 }
7927 });
7928 quote! { #( #lines )* }
7929}
7930
7931/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
7932fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
7933 let lines = entries.iter().map(|e| {
7934 let ident = &e.ident;
7935 let col_ty = column_type_ident(ident);
7936 quote! {
7937 #[allow(non_upper_case_globals)]
7938 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
7939 }
7940 });
7941 quote! { #(#lines)* }
7942}
7943
7944/// Emit a hidden per-model module carrying one zero-sized type per field,
7945/// each with a `Column` impl pointing back at the model.
7946fn column_module_tokens(
7947 module_ident: &syn::Ident,
7948 struct_name: &syn::Ident,
7949 entries: &[ColumnEntry],
7950) -> TokenStream2 {
7951 let root = rustango_root();
7952 let items = entries.iter().map(|e| {
7953 let col_ty = column_type_ident(&e.ident);
7954 let value_ty = &e.value_ty;
7955 let name = &e.name;
7956 let column = &e.column;
7957 let field_type_tokens = &e.field_type_tokens;
7958 quote! {
7959 #[derive(::core::clone::Clone, ::core::marker::Copy)]
7960 pub struct #col_ty;
7961
7962 impl #root::core::Column for #col_ty {
7963 type Model = super::#struct_name;
7964 type Value = #value_ty;
7965 const NAME: &'static str = #name;
7966 const COLUMN: &'static str = #column;
7967 const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
7968 }
7969 }
7970 });
7971 quote! {
7972 #[doc(hidden)]
7973 #[allow(non_camel_case_types, non_snake_case)]
7974 pub mod #module_ident {
7975 // Re-import the parent scope so field types referencing
7976 // sibling models (e.g. `ForeignKey<Author>`) resolve
7977 // inside this submodule. Without this we'd hit
7978 // `proc_macro_derive_resolution_fallback` warnings.
7979 #[allow(unused_imports)]
7980 use super::*;
7981 #(#items)*
7982 }
7983 }
7984}
7985
7986fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
7987 syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
7988}
7989
7990fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
7991 syn::Ident::new(
7992 &format!("__rustango_cols_{struct_name}"),
7993 struct_name.span(),
7994 )
7995}
7996
7997fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
7998 let root = rustango_root();
7999 // The Postgres impl is always emitted — every rustango build pulls in
8000 // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8001 // routed through `#root::__impl_my_from_row!`, a cfg-gated
8002 // macro_rules whose body collapses to nothing when rustango is built
8003 // without the `mysql` feature. No user-facing feature shim required.
8004 //
8005 // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8006 // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8007 // back into a comma-separated list inside square brackets. Since each
8008 // entry is already in `field: expr` shape, the existing tokens slot in.
8009 quote! {
8010 #[cfg(feature = "postgres")]
8011 impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8012 for #struct_name
8013 {
8014 fn from_row(
8015 row: &'r #root::sql::sqlx::postgres::PgRow,
8016 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8017 ::core::result::Result::Ok(Self {
8018 #( #from_row_inits ),*
8019 })
8020 }
8021 }
8022
8023 #root::__impl_my_from_row!(#struct_name, |row| {
8024 #( #from_row_inits ),*
8025 });
8026
8027 #root::__impl_sqlite_from_row!(#struct_name, |row| {
8028 #( #from_row_inits ),*
8029 });
8030 }
8031}
8032
8033struct ContainerAttrs {
8034 table: Option<String>,
8035 display: Option<(String, proc_macro2::Span)>,
8036 /// Explicit Django-style app label from `#[rustango(app = "blog")]`.
8037 /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8038 /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8039 /// at runtime — this attribute is the override for cases where
8040 /// the inference is wrong (e.g. a model that conceptually belongs
8041 /// to one app but is physically in another module).
8042 app: Option<String>,
8043 /// Django ModelAdmin-shape per-model knobs from
8044 /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8045 /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8046 /// admin code falls back to `AdminConfig::DEFAULT`.
8047 admin: Option<AdminAttrs>,
8048 /// Per-model audit configuration from `#[rustango(audit(...))]`.
8049 /// `None` when the model isn't audited — write paths emit no
8050 /// audit entries. When present, single-row writes capture
8051 /// before/after for the listed fields and bulk writes batch
8052 /// snapshots into one INSERT into `rustango_audit_log`.
8053 audit: Option<AuditAttrs>,
8054 /// `true` when `#[rustango(permissions)]` is present. Signals that
8055 /// `auto_create_permissions` should seed the four CRUD codenames for
8056 /// this model.
8057 permissions: bool,
8058 /// Many-to-many relations declared via
8059 /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8060 /// src = "post_id", dst = "tag_id"))]`.
8061 m2m: Vec<M2MAttr>,
8062 /// Polymorphic M2M relations declared via
8063 /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8064 /// pk_column = "taggable_id", ct_column = "taggable_type",
8065 /// related_column = "tag_id"))]` (issue #818).
8066 generic_m2m: Vec<GenericM2MAttr>,
8067 /// Composite indexes declared via
8068 /// `#[rustango(index("col1, col2"))]` or
8069 /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8070 /// Single-column indexes from `#[rustango(index)]` on fields are
8071 /// accumulated here during field collection.
8072 indexes: Vec<IndexAttr>,
8073 /// Table-level CHECK constraints declared via
8074 /// `#[rustango(check(name = "…", expr = "…"))]`.
8075 checks: Vec<CheckAttr>,
8076 /// Table-level PG `EXCLUDE` constraints declared via
8077 /// `#[rustango(exclude(name = "…", using = "gist", elements =
8078 /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8079 /// migration writer renders them on Postgres and skips with a
8080 /// warning on MySQL/SQLite. Issue #319.
8081 excludes: Vec<ExcludeAttr>,
8082 /// Composite (multi-column) FKs declared via
8083 /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8084 /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8085 composite_fks: Vec<CompositeFkAttr>,
8086 /// Generic ("any model") FKs declared via
8087 /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8088 /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8089 generic_fks: Vec<GenericFkAttr>,
8090 /// Where this model lives in a tenancy deployment, declared via
8091 /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8092 /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8093 /// to partition diff output between registry-scoped and
8094 /// tenant-scoped migration files.
8095 scope: Option<String>,
8096 /// Custom-Manager extension-trait name from
8097 /// `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8098 /// When set, the macro emits an empty `pub trait <name>: Sized {}`
8099 /// adjacent to the model so users can write
8100 /// `impl FooManagerExt for QuerySet<Foo> { fn published(self) -> Self ... }`
8101 /// and discover the convention from the model definition.
8102 manager_ext: Option<syn::Ident>,
8103 /// Extra QuerySet accessor names from
8104 /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8105 /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8106 /// the default `Self::objects()`. Multiple attributes allowed.
8107 manager_fns: Vec<syn::Ident>,
8108 /// Default ordering declared via `#[rustango(default_order =
8109 /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8110 /// `(column_name, desc_flag, span_for_error_reporting)` — the
8111 /// `-` prefix means descending; the `+` prefix or no prefix means
8112 /// ascending.
8113 default_order: Vec<(String, bool, proc_macro2::Span)>,
8114 /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8115 /// Routes the emitted schema's `is_view = true` so the migration
8116 /// snapshot skips this model (its underlying SQL view is operator-
8117 /// managed, not rustango-managed).
8118 is_view: bool,
8119 /// Django-shape `Meta.managed` from `#[rustango(managed = false)]`.
8120 /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8121 /// the migration snapshot skips this model so `makemigrations` /
8122 /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8123 /// TABLE` against it (operator-managed schema).
8124 managed: bool,
8125 /// Django-shape `Meta.base_manager_name` from
8126 /// `#[rustango(base_manager_name = "...")]`. Threaded into
8127 /// `ModelSchema::base_manager_name`. Declarative-only today.
8128 base_manager_name: Option<String>,
8129 /// Django-shape `Meta.order_with_respect_to = "parent_fk"` from
8130 /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8131 /// field this model's instances are ordered relative to.
8132 /// Declarative-only today; threaded onto
8133 /// `ModelSchema::order_with_respect_to`.
8134 order_with_respect_to: Option<String>,
8135 /// Django-shape `Meta.proxy = True` from `#[rustango(proxy)]` /
8136 /// `#[rustango(proxy = true)]`. Marks the model as a proxy that
8137 /// shares its DB table with another struct. Threaded into
8138 /// `ModelSchema::proxy` so future codegen can skip table-owning
8139 /// behavior for proxies.
8140 proxy: bool,
8141 /// Django-shape `Meta.required_db_features` from
8142 /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8143 /// Each comma-separated capability token surfaces on
8144 /// `ModelSchema::required_db_features` so `manage check --deploy`
8145 /// can warn when the active dialect lacks one.
8146 required_db_features: Vec<String>,
8147 /// Django-shape `Meta.required_db_vendor` from
8148 /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8149 /// Normalized to the dialect name `manage check --deploy`
8150 /// compares against `Settings.database.backend`. Aliases
8151 /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8152 /// stored under the canonical name.
8153 required_db_vendor: Option<String>,
8154 /// Django-shape `Meta.default_related_name` from
8155 /// `#[rustango(default_related_name = "...")]`. Threaded into
8156 /// `ModelSchema::default_related_name`. Reverse-relation accessor
8157 /// name to use when an FK / M2M field doesn't override it.
8158 /// Today rustango doesn't auto-emit reverse managers; the
8159 /// metadata is the foundation for that work.
8160 default_related_name: Option<String>,
8161 /// Django-shape `Meta.db_table_comment` (4.2+) from
8162 /// `#[rustango(db_table_comment = "...")]`. Threaded into
8163 /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8164 /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8165 /// inline `COMMENT='...'`, SQLite: no-op).
8166 db_table_comment: Option<String>,
8167 /// Django-shape `Meta.get_latest_by` from
8168 /// `#[rustango(get_latest_by = "created_at")]` /
8169 /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8170 /// `(column, descending)` where `descending = true` when the
8171 /// attribute value starts with `-`. Threaded into
8172 /// `ModelSchema::get_latest_by`.
8173 get_latest_by: Option<(String, bool)>,
8174 /// Django-shape `Meta.permissions = [(codename, name), ...]`
8175 /// from `#[rustango(extra_permissions = "approve:Can approve,
8176 /// archive:Can archive")]`. Comma-separated `codename:label`
8177 /// pairs. Threaded into `ModelSchema::extra_permissions`.
8178 extra_permissions: Vec<(String, String)>,
8179 /// Django-shape `Meta.default_permissions` — which CRUD codenames
8180 /// (`"add"` / `"change"` / `"delete"` / `"view"`) the framework
8181 /// auto-creates. Empty `Vec` (default) means **all four** — matches
8182 /// Django's behavior when the operator omits the option. Set via
8183 /// `#[rustango(default_permissions = "view,change")]` to opt out.
8184 /// Validated at parse time; unknown actions fail with a span-pointing
8185 /// error.
8186 default_permissions: Vec<String>,
8187 /// `#[rustango(verbose_name = "blog post")]` — Django-shape
8188 /// human-readable singular label for the model. Threaded into
8189 /// `ModelSchema::verbose_name` so admin section headers /
8190 /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8191 /// over the Rust struct identifier.
8192 verbose_name: Option<String>,
8193 /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8194 /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8195 /// When unset, `display_label_plural()` auto-suffixes `s`.
8196 verbose_name_plural: Option<String>,
8197 /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8198 /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8199 /// a name (used by `QuerySet::without_global_scope`) with a
8200 /// `fn() -> WhereExpr` path that the macro emits into
8201 /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8202 global_scopes: Vec<GlobalScopeAttr>,
8203 /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8204 /// `#[rustango(through(name, far, far_fk_column, intermediate,
8205 /// intermediate_fk_column, intermediate_pk_column))]` — issue
8206 /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8207 /// entry emits an inherent `<name>_through(&self)` accessor that
8208 /// returns a `QuerySet<Far>` filtered via a correlated subquery
8209 /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8210 /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8211 through_relations: Vec<ThroughAttr>,
8212 /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8213 /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8214 /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8215 /// Each entry emits two associated functions on the parent —
8216 /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8217 /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8218 /// over a correlated subquery against the child table. Users
8219 /// drop the result into `QuerySet::where_raw(...)`.
8220 reverse_has_relations: Vec<ReverseHasAttr>,
8221 /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8222 /// issue #830. Each emits a `Model::generic_reverse_relations()`
8223 /// entry so the relation-existence family resolves polymorphic
8224 /// children by name.
8225 generic_has_relations: Vec<GenericHasAttr>,
8226}
8227
8228/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8229/// declaration. Each entry becomes one `core::GlobalScope` in the
8230/// emitted schema literal; `apply` resolves at macro-expand time
8231/// against the consumer's scope so the function must be in scope at
8232/// the use site. Issue #820.
8233struct GlobalScopeAttr {
8234 name: String,
8235 apply: syn::Path,
8236}
8237
8238/// Parsed `#[rustango(through(...))]` declaration. Issue
8239/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8240/// `hasManyThrough` / `hasOneThrough` parity.
8241///
8242/// `Country hasManyThrough Post via User` declares as:
8243///
8244/// ```ignore
8245/// #[rustango(through(
8246/// name = "posts",
8247/// far = "Post",
8248/// far_fk_column = "author_id",
8249/// intermediate = "User",
8250/// intermediate_fk_column = "country_id",
8251/// ))]
8252/// struct Country { ... }
8253/// ```
8254///
8255/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8256/// which returns a queryset filtered via a correlated subquery —
8257/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8258/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8259/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8260/// because the subquery lives inside a `WhereExpr::InSubquery` node
8261/// and the outer queryset's pending list stays empty.
8262///
8263/// All four required arguments use **SQL column / table names**
8264/// (not Rust field names) to sidestep the multi-hop-filter substrate
8265/// gap. Once that substrate lands, a higher-level Rust-field-name
8266/// shorthand can be added without breaking this surface.
8267struct ThroughAttr {
8268 /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8269 name: String,
8270 /// Far model type identifier. `far = "Post"` → returns
8271 /// `QuerySet<Post>`. Resolved verbatim against the scope where
8272 /// the derive expands.
8273 far: syn::Ident,
8274 /// SQL column on the far model's table that references the
8275 /// intermediate model's primary key. For `Post`'s
8276 /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8277 /// default `<field>_id` convention) or whatever the user
8278 /// declared via `#[rustango(db_column = "...")]`.
8279 far_fk_column: String,
8280 /// Intermediate model type identifier. Needed to look up its
8281 /// `SCHEMA` so the subquery's `FROM` clause points at the
8282 /// intermediate table. `intermediate = "User"`.
8283 intermediate: syn::Ident,
8284 /// SQL column on the intermediate's table that references the
8285 /// source (this) model's primary key. For `User`'s
8286 /// `country: ForeignKey<Country>` the column is `"country_id"`.
8287 intermediate_fk_column: String,
8288 /// SQL primary-key column on the intermediate's table — the
8289 /// column the subquery projects. Optional; defaults to `"id"`
8290 /// (rustango's default PK column name). Override when the
8291 /// intermediate declares a custom PK column.
8292 intermediate_pk_column: String,
8293}
8294
8295/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8296/// child_fk_column = "..."))]` declaration. Issue
8297/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8298/// `whereHas` / `whereDoesntHave` parity.
8299///
8300/// `Post hasMany Comment` declares as:
8301///
8302/// ```ignore
8303/// #[rustango(reverse_has(
8304/// name = "comments",
8305/// child = "Comment",
8306/// child_fk_column = "post_id",
8307/// ))]
8308/// struct Post { ... }
8309/// ```
8310///
8311/// The macro emits two associated functions on `Post`:
8312///
8313/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8314/// `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8315/// <outer>.<self_pk_column>)`.
8316/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8317/// but `NOT EXISTS`, the `whereDoesntHave` analog.
8318///
8319/// User code:
8320///
8321/// ```ignore
8322/// // Posts with at least one comment:
8323/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8324/// // Posts with no comments:
8325/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8326/// ```
8327///
8328/// As with #817, all column / table identifiers are **SQL names**
8329/// (not Rust field names) so the substrate is independent of the
8330/// outstanding multi-hop filter resolver gap. The emitted
8331/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8332/// SQL-emit time via the writer's scope stack.
8333struct ReverseHasAttr {
8334 /// Accessor name. `name = "comments"` → emits
8335 /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8336 name: String,
8337 /// Child model type identifier. `child = "Comment"` — needed to
8338 /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8339 /// points at the child table.
8340 child: syn::Ident,
8341 /// SQL column on the child's table that references this model's
8342 /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8343 /// column is `"post_id"`.
8344 child_fk_column: String,
8345 /// SQL primary-key column on **this** model's table — the column
8346 /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8347 /// (rustango's default PK column name).
8348 self_pk_column: String,
8349}
8350
8351/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8352/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8353/// relation-existence family (issue #830). The child is a polymorphic,
8354/// content-type-discriminated model; emits a `Model::
8355/// generic_reverse_relations()` entry the queryset resolves by name.
8356struct GenericHasAttr {
8357 /// Accessor name, e.g. `name = "tags"`.
8358 name: String,
8359 /// Child model type identifier (`child = "Tag"`) — looked up for its
8360 /// `SCHEMA` so the subquery's `FROM` points at the child table.
8361 child: syn::Ident,
8362 /// Column on the child table holding the parent's content-type id.
8363 /// Optional; defaults to `"content_type_id"`.
8364 ct_column: String,
8365 /// Column on the child table holding the parent's PK value.
8366 /// Optional; defaults to `"object_pk"`.
8367 pk_column: String,
8368 /// SQL primary-key column on **this** (parent) model's table.
8369 /// Optional; defaults to `"id"`.
8370 self_pk_column: String,
8371}
8372
8373/// Parsed form of one index declaration (field-level or container-level).
8374struct IndexAttr {
8375 /// Index name; auto-derived when `None` at parse time.
8376 name: Option<String>,
8377 /// Column names in the index.
8378 columns: Vec<String>,
8379 /// `true` for `CREATE UNIQUE INDEX`.
8380 unique: bool,
8381 /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8382 /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8383 /// `"btree"` when the attribute is absent — the DDL writer omits
8384 /// the `USING` clause and the backend uses its own default
8385 /// (btree on every supported dialect).
8386 method: String,
8387 /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8388 /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8389 /// condition = "...", name = "..."))]`. `None` for plain indexes.
8390 where_clause: Option<String>,
8391 /// Django `Index(fields=..., include=[...])` covering-index
8392 /// columns (PG 11+ `INCLUDE (...)` clause). Empty `Vec` (the
8393 /// default) means "no covering columns".
8394 include: Vec<String>,
8395}
8396
8397/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8398struct CheckAttr {
8399 name: String,
8400 expr: String,
8401}
8402
8403/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8404/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8405/// PG-only — surfaced on every backend in the macro emit; the migration
8406/// writer skips the op on MySQL/SQLite. Issue #319.
8407struct ExcludeAttr {
8408 /// Constraint name (free-form Rust identifier).
8409 name: String,
8410 /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8411 using: String,
8412 /// Comma-separated `(column, operator)` pairs, in declaration
8413 /// order. Parsed from `"col WITH op, col WITH op"`.
8414 elements: Vec<(String, String)>,
8415 /// Optional `WHERE` predicate (raw SQL).
8416 where_clause: Option<String>,
8417}
8418
8419/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8420/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8421/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8422/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8423/// the model, not the field.
8424struct CompositeFkAttr {
8425 /// Logical relation name (free-form Rust identifier).
8426 name: String,
8427 /// SQL table name of the target.
8428 to: String,
8429 /// Source-side column names, in declaration order.
8430 from: Vec<String>,
8431 /// Target-side column names, same length / order as `from`.
8432 on: Vec<String>,
8433}
8434
8435/// Parsed form of one `#[rustango(generic_fk(name = "target",
8436/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8437/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8438/// generic ("any model") FKs live on the model, not the field.
8439struct GenericFkAttr {
8440 /// Logical relation name (free-form Rust identifier).
8441 name: String,
8442 /// Source-side column carrying the `content_type_id` value.
8443 ct_column: String,
8444 /// Source-side column carrying the target row's primary key.
8445 pk_column: String,
8446}
8447
8448/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8449struct M2MAttr {
8450 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8451 name: String,
8452 /// Target table (e.g. `"app_tags"`).
8453 to: String,
8454 /// Junction table (e.g. `"post_tags"`).
8455 through: String,
8456 /// Source FK column in the junction table (e.g. `"post_id"`).
8457 src: String,
8458 /// Destination FK column in the junction table (e.g. `"tag_id"`).
8459 dst: String,
8460 /// Whether the migration writer should auto-create the junction
8461 /// table. Default `true`. Set `auto_create = false` (#324) when
8462 /// the operator declares the through table as its own
8463 /// `#[derive(Model)]` struct with extra columns.
8464 auto_create: bool,
8465}
8466
8467/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8468/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8469/// junction carries a ContentType discriminator so unrelated models
8470/// share one pivot + related set.
8471struct GenericM2MAttr {
8472 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8473 name: String,
8474 /// Polymorphic junction table (e.g. `"taggables"`).
8475 through: String,
8476 /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8477 pk_column: String,
8478 /// Junction column holding the owning model's `content_type_id`
8479 /// discriminator (e.g. `"taggable_type"`).
8480 ct_column: String,
8481 /// Junction column holding the related model PK (e.g. `"tag_id"`).
8482 related_column: String,
8483}
8484
8485/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8486/// "user"))]`. `track` is a comma-separated list of field names whose
8487/// before/after values land in the JSONB `changes` column. `source`
8488/// is informational only — it pins a default source when the model
8489/// is written outside any `audit::with_source(...)` scope (rare).
8490#[derive(Default)]
8491struct AuditAttrs {
8492 /// Field names to capture in the `changes` JSONB. Validated
8493 /// against declared scalar fields at compile time. Empty means
8494 /// "track every scalar field" — Django's audit-everything default.
8495 track: Option<(Vec<String>, proc_macro2::Span)>,
8496}
8497
8498/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8499/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8500/// comma-separated strings; we validate each ident against the model's
8501/// declared fields at compile time.
8502#[derive(Default)]
8503struct AdminAttrs {
8504 list_display: Option<(Vec<String>, proc_macro2::Span)>,
8505 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8506 list_per_page: Option<usize>,
8507 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8508 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8509 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8510 /// Bulk action names. No field-validation against model fields —
8511 /// these are action handlers, not column references.
8512 actions: Option<(Vec<String>, proc_macro2::Span)>,
8513 /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8514 /// sections, comma-separated fields per section, optional
8515 /// `Title:` prefix. Empty title omits the `<legend>`.
8516 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8517 /// `admin(list_display_links = "title")` — Django-shape. Names
8518 /// from `list_display` whose cells should link to detail/edit.
8519 /// Issue #350.
8520 list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8521 /// `admin(search_help_text = "...")` — Django-shape. Short
8522 /// caption rendered beside the admin list view's search box.
8523 /// Issue #353.
8524 search_help_text: Option<String>,
8525 /// `admin(actions_on_top = false)` — Django-shape. Hides the
8526 /// action-bar above the table. Default `true`. Issue #354.
8527 actions_on_top: Option<bool>,
8528 /// `admin(actions_on_bottom = true)` — Django-shape. Renders an
8529 /// additional action-bar below the table. Default `false`.
8530 /// Issue #354.
8531 actions_on_bottom: Option<bool>,
8532 /// `admin(date_hierarchy = "created_at")` — Django-shape. Name of
8533 /// a date / datetime field whose values render as a clickable
8534 /// year / month / day drill-down strip above the list table.
8535 /// Empty / unset disables the strip. Issue #355.
8536 date_hierarchy: Option<String>,
8537 /// `admin(prepopulated_fields = "slug:title")` — Django-shape.
8538 /// Each entry is `target:source[+source2]`; multiple entries are
8539 /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8540 /// The admin change-form emits JS that slugifies the source values
8541 /// into the target field on every keystroke. Issue #356.
8542 prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8543 /// `admin(raw_id_fields = "parent, owner")` — Django-shape. Names
8544 /// of FK fields whose change-form widget renders a lookup link
8545 /// next to the input. Issue #357.
8546 raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8547 /// `admin(autocomplete_fields = "author_id")` — Django-shape.
8548 /// Names of FK fields whose change-form widget renders an
8549 /// Ajax-driven typeahead populated from a `__autocomplete`
8550 /// endpoint on the target model. Issue #358.
8551 autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8552 /// `admin(list_select_related = "all" | "none" | "author, …")`
8553 /// — Django-shape. Tunes the admin list view's FK auto-JOIN
8554 /// policy. Default `"all"` matches rustango's join-everything
8555 /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8556 list_select_related: Option<String>,
8557 /// `admin(formfield_overrides = "field:widget, field2:widget2")` —
8558 /// Django-shape. Each entry is `field_name:widget_name`; multiple
8559 /// entries comma-separated. Empty / unset → no overrides. The
8560 /// list of widget names supported is documented on
8561 /// `AdminConfig::formfield_overrides`. Issue #359.
8562 formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8563}
8564
8565fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8566 let mut out = ContainerAttrs {
8567 table: None,
8568 display: None,
8569 app: None,
8570 admin: None,
8571 audit: None,
8572 // Default `permissions = true` so every `#[derive(Model)]`
8573 // gets the four CRUD codenames seeded by `auto_create_permissions`
8574 // and is visible to non-superusers in the tenant admin without
8575 // manual per-model annotation. Models that intentionally don't
8576 // want permission rows (registry-internal types, framework
8577 // tables operators shouldn't manage directly) opt out via
8578 // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8579 // out-of-the-box admin invisibility regression (#62).
8580 permissions: true,
8581 m2m: Vec::new(),
8582 generic_m2m: Vec::new(),
8583 indexes: Vec::new(),
8584 checks: Vec::new(),
8585 excludes: Vec::new(),
8586 composite_fks: Vec::new(),
8587 generic_fks: Vec::new(),
8588 scope: None,
8589 manager_ext: None,
8590 manager_fns: Vec::new(),
8591 default_order: Vec::new(),
8592 is_view: false,
8593 managed: true,
8594 verbose_name: None,
8595 verbose_name_plural: None,
8596 base_manager_name: None,
8597 order_with_respect_to: None,
8598 proxy: false,
8599 required_db_features: Vec::new(),
8600 required_db_vendor: None,
8601 default_related_name: None,
8602 db_table_comment: None,
8603 get_latest_by: None,
8604 extra_permissions: Vec::new(),
8605 default_permissions: Vec::new(),
8606 global_scopes: Vec::new(),
8607 through_relations: Vec::new(),
8608 reverse_has_relations: Vec::new(),
8609 generic_has_relations: Vec::new(),
8610 };
8611 for attr in &input.attrs {
8612 if !attr.path().is_ident("rustango") {
8613 continue;
8614 }
8615 attr.parse_nested_meta(|meta| {
8616 if meta.path.is_ident("table") {
8617 let s: LitStr = meta.value()?.parse()?;
8618 let name = s.value();
8619 // v0.27.3 (#65) — macro-time guard against table names
8620 // that compile but break SQL downstream. Hyphens are
8621 // the common footgun: PostgreSQL accepts a quoted
8622 // `"intermediate-region"` in CREATE TABLE, but the
8623 // FK / index name derivation in `migrate::ddl`
8624 // emits `intermediate-region_field_fkey` unquoted,
8625 // which then fails the SQL parser. Same shape rule
8626 // as Postgres regular identifiers so the safe path
8627 // is the only path.
8628 validate_table_name(&name, s.span())?;
8629 out.table = Some(name);
8630 return Ok(());
8631 }
8632 if meta.path.is_ident("display") {
8633 let s: LitStr = meta.value()?.parse()?;
8634 out.display = Some((s.value(), s.span()));
8635 return Ok(());
8636 }
8637 if meta.path.is_ident("app") {
8638 let s: LitStr = meta.value()?.parse()?;
8639 out.app = Some(s.value());
8640 return Ok(());
8641 }
8642 if meta.path.is_ident("scope") {
8643 let s: LitStr = meta.value()?.parse()?;
8644 let val = s.value();
8645 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8646 return Err(meta.error(format!(
8647 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8648 )));
8649 }
8650 out.scope = Some(val);
8651 return Ok(());
8652 }
8653 if meta.path.is_ident("admin") {
8654 let mut admin = AdminAttrs::default();
8655 meta.parse_nested_meta(|inner| {
8656 if inner.path.is_ident("list_display") {
8657 let s: LitStr = inner.value()?.parse()?;
8658 admin.list_display =
8659 Some((split_field_list(&s.value()), s.span()));
8660 return Ok(());
8661 }
8662 if inner.path.is_ident("search_fields") {
8663 let s: LitStr = inner.value()?.parse()?;
8664 admin.search_fields =
8665 Some((split_field_list(&s.value()), s.span()));
8666 return Ok(());
8667 }
8668 if inner.path.is_ident("readonly_fields") {
8669 let s: LitStr = inner.value()?.parse()?;
8670 admin.readonly_fields =
8671 Some((split_field_list(&s.value()), s.span()));
8672 return Ok(());
8673 }
8674 if inner.path.is_ident("list_per_page") {
8675 let lit: syn::LitInt = inner.value()?.parse()?;
8676 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8677 return Ok(());
8678 }
8679 if inner.path.is_ident("ordering") {
8680 let s: LitStr = inner.value()?.parse()?;
8681 admin.ordering = Some((
8682 parse_ordering_list(&s.value()),
8683 s.span(),
8684 ));
8685 return Ok(());
8686 }
8687 if inner.path.is_ident("list_filter") {
8688 let s: LitStr = inner.value()?.parse()?;
8689 admin.list_filter =
8690 Some((split_field_list(&s.value()), s.span()));
8691 return Ok(());
8692 }
8693 if inner.path.is_ident("actions") {
8694 let s: LitStr = inner.value()?.parse()?;
8695 admin.actions =
8696 Some((split_field_list(&s.value()), s.span()));
8697 return Ok(());
8698 }
8699 if inner.path.is_ident("fieldsets") {
8700 let s: LitStr = inner.value()?.parse()?;
8701 admin.fieldsets =
8702 Some((parse_fieldset_list(&s.value()), s.span()));
8703 return Ok(());
8704 }
8705 if inner.path.is_ident("list_display_links") {
8706 let s: LitStr = inner.value()?.parse()?;
8707 admin.list_display_links =
8708 Some((split_field_list(&s.value()), s.span()));
8709 return Ok(());
8710 }
8711 if inner.path.is_ident("search_help_text") {
8712 let s: LitStr = inner.value()?.parse()?;
8713 admin.search_help_text = Some(s.value());
8714 return Ok(());
8715 }
8716 if inner.path.is_ident("actions_on_top") {
8717 let lit: syn::LitBool = inner.value()?.parse()?;
8718 admin.actions_on_top = Some(lit.value);
8719 return Ok(());
8720 }
8721 if inner.path.is_ident("actions_on_bottom") {
8722 let lit: syn::LitBool = inner.value()?.parse()?;
8723 admin.actions_on_bottom = Some(lit.value);
8724 return Ok(());
8725 }
8726 if inner.path.is_ident("date_hierarchy") {
8727 let s: LitStr = inner.value()?.parse()?;
8728 admin.date_hierarchy = Some(s.value());
8729 return Ok(());
8730 }
8731 if inner.path.is_ident("prepopulated_fields") {
8732 let s: LitStr = inner.value()?.parse()?;
8733 admin.prepopulated_fields =
8734 Some((parse_prepopulated_list(&s.value()), s.span()));
8735 return Ok(());
8736 }
8737 if inner.path.is_ident("raw_id_fields") {
8738 let s: LitStr = inner.value()?.parse()?;
8739 admin.raw_id_fields =
8740 Some((split_field_list(&s.value()), s.span()));
8741 return Ok(());
8742 }
8743 if inner.path.is_ident("autocomplete_fields") {
8744 let s: LitStr = inner.value()?.parse()?;
8745 admin.autocomplete_fields =
8746 Some((split_field_list(&s.value()), s.span()));
8747 return Ok(());
8748 }
8749 if inner.path.is_ident("list_select_related") {
8750 let s: LitStr = inner.value()?.parse()?;
8751 admin.list_select_related = Some(s.value());
8752 return Ok(());
8753 }
8754 if inner.path.is_ident("formfield_overrides") {
8755 let s: LitStr = inner.value()?.parse()?;
8756 admin.formfield_overrides =
8757 Some((parse_formfield_overrides(&s.value()), s.span()));
8758 return Ok(());
8759 }
8760 Err(inner.error(
8761 "unknown admin attribute (supported: \
8762 `list_display`, `list_display_links`, \
8763 `search_fields`, `search_help_text`, \
8764 `readonly_fields`, \
8765 `list_filter`, `list_per_page`, `ordering`, `actions`, \
8766 `actions_on_top`, `actions_on_bottom`, \
8767 `date_hierarchy`, \
8768 `prepopulated_fields`, \
8769 `raw_id_fields`, \
8770 `autocomplete_fields`, \
8771 `list_select_related`, \
8772 `formfield_overrides`, \
8773 `fieldsets`)",
8774 ))
8775 })?;
8776 out.admin = Some(admin);
8777 return Ok(());
8778 }
8779 if meta.path.is_ident("manager") {
8780 // `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8781 // Stretch `from_queryset = "..."` (Django Manager.from_queryset
8782 // shape) is left as a follow-up — the issue's primary
8783 // acceptance is the `ext = ...` trait emission.
8784 meta.parse_nested_meta(|inner| {
8785 if inner.path.is_ident("ext") {
8786 let s: LitStr = inner.value()?.parse()?;
8787 let name = s.value();
8788 if name.is_empty() {
8789 return Err(inner.error("manager(ext = \"...\") cannot be empty"));
8790 }
8791 out.manager_ext =
8792 Some(syn::Ident::new(&name, s.span()));
8793 return Ok(());
8794 }
8795 Err(inner.error(
8796 "unknown manager attribute (supported: `ext = \"TraitName\"`)",
8797 ))
8798 })?;
8799 return Ok(());
8800 }
8801 if meta.path.is_ident("manager_fn") {
8802 // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8803 // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8804 // next to the default `Self::objects()`. Multiple
8805 // attributes accumulate.
8806 let s: LitStr = meta.value()?.parse()?;
8807 let name = s.value();
8808 if name.is_empty() {
8809 return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8810 }
8811 if name == "objects" {
8812 return Err(meta.error(
8813 "`manager_fn = \"objects\"` collides with the default \
8814 accessor — pick a different name",
8815 ));
8816 }
8817 let ident = syn::Ident::new(&name, s.span());
8818 if out.manager_fns.iter().any(|prev| *prev == ident) {
8819 return Err(meta.error(format!(
8820 "duplicate `manager_fn = \"{name}\"`"
8821 )));
8822 }
8823 out.manager_fns.push(ident);
8824 return Ok(());
8825 }
8826 if meta.path.is_ident("default_order") {
8827 // `#[rustango(default_order = "-created_at, status")]`
8828 // — issue #291 / T2.5. Comma-separated list; `-prefix`
8829 // means descending, `+prefix` or bare name means ascending.
8830 // Per-query opt-in via `QuerySet::with_default_order()`.
8831 let s: LitStr = meta.value()?.parse()?;
8832 let raw = s.value();
8833 let span = s.span();
8834 let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
8835 Vec::new();
8836 for entry in raw.split(',') {
8837 let trimmed = entry.trim();
8838 if trimmed.is_empty() {
8839 return Err(syn::Error::new(
8840 span,
8841 "`default_order = \"...\"` has an empty entry — \
8842 check for a stray comma",
8843 ));
8844 }
8845 let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
8846 (true, rest.trim().to_owned())
8847 } else if let Some(rest) = trimmed.strip_prefix('+') {
8848 (false, rest.trim().to_owned())
8849 } else {
8850 (false, trimmed.to_owned())
8851 };
8852 if name.is_empty() {
8853 return Err(syn::Error::new(
8854 span,
8855 "`default_order` entry has no column name after the prefix",
8856 ));
8857 }
8858 if parsed.iter().any(|(n, _, _)| *n == name) {
8859 return Err(syn::Error::new(
8860 span,
8861 format!("duplicate column `{name}` in `default_order`"),
8862 ));
8863 }
8864 parsed.push((name, desc, span));
8865 }
8866 if parsed.is_empty() {
8867 return Err(syn::Error::new(
8868 span,
8869 "`default_order = \"...\"` cannot be empty",
8870 ));
8871 }
8872 out.default_order = parsed;
8873 return Ok(());
8874 }
8875 if meta.path.is_ident("global_scope") {
8876 // `#[rustango(global_scope(name = "active", apply =
8877 // path::to::fn))]` — issue #820. The apply function
8878 // path resolves at macro-expand time in the consumer's
8879 // scope; the macro re-emits it verbatim into the
8880 // `ModelSchema::global_scopes` slice literal.
8881 let span = meta.path.span();
8882 let mut scope_name: Option<String> = None;
8883 let mut apply_path: Option<syn::Path> = None;
8884 meta.parse_nested_meta(|inner| {
8885 if inner.path.is_ident("name") {
8886 let s: LitStr = inner.value()?.parse()?;
8887 let raw = s.value();
8888 if raw.trim().is_empty() {
8889 return Err(syn::Error::new(
8890 s.span(),
8891 "`global_scope(name = \"...\")` must not be empty",
8892 ));
8893 }
8894 scope_name = Some(raw);
8895 return Ok(());
8896 }
8897 if inner.path.is_ident("apply") {
8898 let p: syn::Path = inner.value()?.parse()?;
8899 apply_path = Some(p);
8900 return Ok(());
8901 }
8902 Err(inner.error(
8903 "unknown `global_scope` attribute (supported: \
8904 `name`, `apply`)",
8905 ))
8906 })?;
8907 let Some(name) = scope_name else {
8908 return Err(syn::Error::new(
8909 span,
8910 "`global_scope` requires `name = \"...\"`",
8911 ));
8912 };
8913 let Some(apply) = apply_path else {
8914 return Err(syn::Error::new(
8915 span,
8916 "`global_scope` requires `apply = fn_path`",
8917 ));
8918 };
8919 if out.global_scopes.iter().any(|s| s.name == name) {
8920 return Err(syn::Error::new(
8921 span,
8922 format!(
8923 "duplicate global scope name `{name}` — \
8924 pick a unique identifier so \
8925 `QuerySet::without_global_scope(\"{name}\")` \
8926 is unambiguous"
8927 ),
8928 ));
8929 }
8930 out.global_scopes.push(GlobalScopeAttr { name, apply });
8931 return Ok(());
8932 }
8933 if meta.path.is_ident("through") {
8934 // `#[rustango(through(name = "posts", far = "Post",
8935 // far_fk_column = "author_id", intermediate = "User",
8936 // intermediate_fk_column = "country_id"
8937 // [, intermediate_pk_column = "id"]))]` — issue #817.
8938 let span = meta.path.span();
8939 let mut accessor_name: Option<String> = None;
8940 let mut far_ident: Option<syn::Ident> = None;
8941 let mut far_fk_column: Option<String> = None;
8942 let mut intermediate_ident: Option<syn::Ident> = None;
8943 let mut intermediate_fk_column: Option<String> = None;
8944 let mut intermediate_pk_column: Option<String> = None;
8945 fn parse_nonempty_string(
8946 inner: &syn::meta::ParseNestedMeta<'_>,
8947 field: &str,
8948 ) -> syn::Result<String> {
8949 let s: LitStr = inner.value()?.parse()?;
8950 let raw = s.value();
8951 let trimmed = raw.trim();
8952 if trimmed.is_empty() {
8953 return Err(syn::Error::new(
8954 s.span(),
8955 format!("`through({field} = \"...\")` must not be empty"),
8956 ));
8957 }
8958 Ok(trimmed.to_owned())
8959 }
8960 meta.parse_nested_meta(|inner| {
8961 if inner.path.is_ident("name") {
8962 accessor_name = Some(parse_nonempty_string(&inner, "name")?);
8963 return Ok(());
8964 }
8965 if inner.path.is_ident("far") {
8966 // Far model name accepted as a string literal
8967 // so the attribute fits inside the existing
8968 // `parse_nested_meta` shape; parsed back into a
8969 // `syn::Ident` so the emitted accessor can
8970 // reference the type directly.
8971 let s: LitStr = inner.value()?.parse()?;
8972 let raw = s.value();
8973 let trimmed = raw.trim();
8974 if trimmed.is_empty() {
8975 return Err(syn::Error::new(
8976 s.span(),
8977 "`through(far = \"...\")` must not be empty",
8978 ));
8979 }
8980 far_ident = Some(syn::Ident::new(trimmed, s.span()));
8981 return Ok(());
8982 }
8983 if inner.path.is_ident("far_fk_column") {
8984 far_fk_column =
8985 Some(parse_nonempty_string(&inner, "far_fk_column")?);
8986 return Ok(());
8987 }
8988 if inner.path.is_ident("intermediate") {
8989 let s: LitStr = inner.value()?.parse()?;
8990 let raw = s.value();
8991 let trimmed = raw.trim();
8992 if trimmed.is_empty() {
8993 return Err(syn::Error::new(
8994 s.span(),
8995 "`through(intermediate = \"...\")` must not be empty",
8996 ));
8997 }
8998 intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
8999 return Ok(());
9000 }
9001 if inner.path.is_ident("intermediate_fk_column") {
9002 intermediate_fk_column =
9003 Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9004 return Ok(());
9005 }
9006 if inner.path.is_ident("intermediate_pk_column") {
9007 intermediate_pk_column =
9008 Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9009 return Ok(());
9010 }
9011 Err(inner.error(
9012 "unknown `through` attribute (supported: \
9013 `name`, `far`, `far_fk_column`, \
9014 `intermediate`, `intermediate_fk_column`, \
9015 `intermediate_pk_column`)",
9016 ))
9017 })?;
9018 let Some(name) = accessor_name else {
9019 return Err(syn::Error::new(
9020 span,
9021 "`through` requires `name = \"...\"`",
9022 ));
9023 };
9024 let Some(far) = far_ident else {
9025 return Err(syn::Error::new(
9026 span,
9027 "`through` requires `far = \"FarModelType\"`",
9028 ));
9029 };
9030 let Some(far_fk_column) = far_fk_column else {
9031 return Err(syn::Error::new(
9032 span,
9033 "`through` requires `far_fk_column = \"<column>\"`",
9034 ));
9035 };
9036 let Some(intermediate) = intermediate_ident else {
9037 return Err(syn::Error::new(
9038 span,
9039 "`through` requires `intermediate = \"IntermediateModelType\"`",
9040 ));
9041 };
9042 let Some(intermediate_fk_column) = intermediate_fk_column else {
9043 return Err(syn::Error::new(
9044 span,
9045 "`through` requires `intermediate_fk_column = \"<column>\"`",
9046 ));
9047 };
9048 let intermediate_pk_column =
9049 intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9050 if out.through_relations.iter().any(|t| t.name == name) {
9051 return Err(syn::Error::new(
9052 span,
9053 format!(
9054 "duplicate `through(name = \"{name}\")` — \
9055 pick a unique accessor name"
9056 ),
9057 ));
9058 }
9059 out.through_relations.push(ThroughAttr {
9060 name,
9061 far,
9062 far_fk_column,
9063 intermediate,
9064 intermediate_fk_column,
9065 intermediate_pk_column,
9066 });
9067 return Ok(());
9068 }
9069 if meta.path.is_ident("reverse_has") {
9070 // `#[rustango(reverse_has(name = "comments",
9071 // child = "Comment", child_fk_column = "post_id"
9072 // [, self_pk_column = "id"]))]` — issue #830.
9073 let span = meta.path.span();
9074 let mut accessor_name: Option<String> = None;
9075 let mut child_ident: Option<syn::Ident> = None;
9076 let mut child_fk_column: Option<String> = None;
9077 let mut self_pk_column: Option<String> = None;
9078 meta.parse_nested_meta(|inner| {
9079 if inner.path.is_ident("name") {
9080 let s: LitStr = inner.value()?.parse()?;
9081 let raw = s.value();
9082 if raw.trim().is_empty() {
9083 return Err(syn::Error::new(
9084 s.span(),
9085 "`reverse_has(name = \"...\")` must not be empty",
9086 ));
9087 }
9088 accessor_name = Some(raw);
9089 return Ok(());
9090 }
9091 if inner.path.is_ident("child") {
9092 let s: LitStr = inner.value()?.parse()?;
9093 let raw = s.value();
9094 let trimmed = raw.trim();
9095 if trimmed.is_empty() {
9096 return Err(syn::Error::new(
9097 s.span(),
9098 "`reverse_has(child = \"...\")` must not be empty",
9099 ));
9100 }
9101 child_ident = Some(syn::Ident::new(trimmed, s.span()));
9102 return Ok(());
9103 }
9104 if inner.path.is_ident("child_fk_column") {
9105 let s: LitStr = inner.value()?.parse()?;
9106 let raw = s.value();
9107 let trimmed = raw.trim();
9108 if trimmed.is_empty() {
9109 return Err(syn::Error::new(
9110 s.span(),
9111 "`reverse_has(child_fk_column = \"...\")` must not be empty",
9112 ));
9113 }
9114 child_fk_column = Some(trimmed.to_owned());
9115 return Ok(());
9116 }
9117 if inner.path.is_ident("self_pk_column") {
9118 let s: LitStr = inner.value()?.parse()?;
9119 let raw = s.value();
9120 let trimmed = raw.trim();
9121 if trimmed.is_empty() {
9122 return Err(syn::Error::new(
9123 s.span(),
9124 "`reverse_has(self_pk_column = \"...\")` must not be empty",
9125 ));
9126 }
9127 self_pk_column = Some(trimmed.to_owned());
9128 return Ok(());
9129 }
9130 Err(inner.error(
9131 "unknown `reverse_has` attribute (supported: \
9132 `name`, `child`, `child_fk_column`, \
9133 `self_pk_column`)",
9134 ))
9135 })?;
9136 let Some(name) = accessor_name else {
9137 return Err(syn::Error::new(
9138 span,
9139 "`reverse_has` requires `name = \"...\"`",
9140 ));
9141 };
9142 let Some(child) = child_ident else {
9143 return Err(syn::Error::new(
9144 span,
9145 "`reverse_has` requires `child = \"ChildModelType\"`",
9146 ));
9147 };
9148 let Some(child_fk_column) = child_fk_column else {
9149 return Err(syn::Error::new(
9150 span,
9151 "`reverse_has` requires `child_fk_column = \"<column>\"`",
9152 ));
9153 };
9154 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9155 if out.reverse_has_relations.iter().any(|r| r.name == name) {
9156 return Err(syn::Error::new(
9157 span,
9158 format!(
9159 "duplicate `reverse_has(name = \"{name}\")` — \
9160 pick a unique accessor name"
9161 ),
9162 ));
9163 }
9164 out.reverse_has_relations.push(ReverseHasAttr {
9165 name,
9166 child,
9167 child_fk_column,
9168 self_pk_column,
9169 });
9170 return Ok(());
9171 }
9172 if meta.path.is_ident("generic_has") {
9173 // `#[rustango(generic_has(name = "tags",
9174 // child = "Tag", ct_column = "content_type_id",
9175 // pk_column = "object_pk" [, self_pk_column = "id"]))]`
9176 // — issue #830, the reverse generic-FK (GFK) arm of the
9177 // relation-existence family.
9178 let span = meta.path.span();
9179 let mut accessor_name: Option<String> = None;
9180 let mut child_ident: Option<syn::Ident> = None;
9181 let mut ct_column: Option<String> = None;
9182 let mut pk_column: Option<String> = None;
9183 let mut self_pk_column: Option<String> = None;
9184 meta.parse_nested_meta(|inner| {
9185 if inner.path.is_ident("name") {
9186 let s: LitStr = inner.value()?.parse()?;
9187 let raw = s.value();
9188 if raw.trim().is_empty() {
9189 return Err(syn::Error::new(
9190 s.span(),
9191 "`generic_has(name = \"...\")` must not be empty",
9192 ));
9193 }
9194 accessor_name = Some(raw);
9195 return Ok(());
9196 }
9197 if inner.path.is_ident("child") {
9198 let s: LitStr = inner.value()?.parse()?;
9199 let trimmed = s.value().trim().to_owned();
9200 if trimmed.is_empty() {
9201 return Err(syn::Error::new(
9202 s.span(),
9203 "`generic_has(child = \"...\")` must not be empty",
9204 ));
9205 }
9206 child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9207 return Ok(());
9208 }
9209 if inner.path.is_ident("ct_column") {
9210 let s: LitStr = inner.value()?.parse()?;
9211 let trimmed = s.value().trim().to_owned();
9212 if trimmed.is_empty() {
9213 return Err(syn::Error::new(
9214 s.span(),
9215 "`generic_has(ct_column = \"...\")` must not be empty",
9216 ));
9217 }
9218 ct_column = Some(trimmed);
9219 return Ok(());
9220 }
9221 if inner.path.is_ident("pk_column") {
9222 let s: LitStr = inner.value()?.parse()?;
9223 let trimmed = s.value().trim().to_owned();
9224 if trimmed.is_empty() {
9225 return Err(syn::Error::new(
9226 s.span(),
9227 "`generic_has(pk_column = \"...\")` must not be empty",
9228 ));
9229 }
9230 pk_column = Some(trimmed);
9231 return Ok(());
9232 }
9233 if inner.path.is_ident("self_pk_column") {
9234 let s: LitStr = inner.value()?.parse()?;
9235 let trimmed = s.value().trim().to_owned();
9236 if trimmed.is_empty() {
9237 return Err(syn::Error::new(
9238 s.span(),
9239 "`generic_has(self_pk_column = \"...\")` must not be empty",
9240 ));
9241 }
9242 self_pk_column = Some(trimmed);
9243 return Ok(());
9244 }
9245 Err(inner.error(
9246 "unknown `generic_has` attribute (supported: \
9247 `name`, `child`, `ct_column`, `pk_column`, \
9248 `self_pk_column`)",
9249 ))
9250 })?;
9251 let Some(name) = accessor_name else {
9252 return Err(syn::Error::new(
9253 span,
9254 "`generic_has` requires `name = \"...\"`",
9255 ));
9256 };
9257 let Some(child) = child_ident else {
9258 return Err(syn::Error::new(
9259 span,
9260 "`generic_has` requires `child = \"ChildModelType\"`",
9261 ));
9262 };
9263 let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9264 let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9265 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9266 if out.generic_has_relations.iter().any(|r| r.name == name) {
9267 return Err(syn::Error::new(
9268 span,
9269 format!(
9270 "duplicate `generic_has(name = \"{name}\")` — \
9271 pick a unique accessor name"
9272 ),
9273 ));
9274 }
9275 out.generic_has_relations.push(GenericHasAttr {
9276 name,
9277 child,
9278 ct_column,
9279 pk_column,
9280 self_pk_column,
9281 });
9282 return Ok(());
9283 }
9284 if meta.path.is_ident("audit") {
9285 let mut audit = AuditAttrs::default();
9286 meta.parse_nested_meta(|inner| {
9287 if inner.path.is_ident("track") {
9288 let s: LitStr = inner.value()?.parse()?;
9289 audit.track =
9290 Some((split_field_list(&s.value()), s.span()));
9291 return Ok(());
9292 }
9293 Err(inner.error(
9294 "unknown audit attribute (supported: `track`)",
9295 ))
9296 })?;
9297 out.audit = Some(audit);
9298 return Ok(());
9299 }
9300 if meta.path.is_ident("permissions") {
9301 // Two forms accepted:
9302 // #[rustango(permissions)] — flag form, true
9303 // #[rustango(permissions = false)] — explicit opt-out
9304 // #[rustango(permissions = true)] — explicit opt-in
9305 if let Ok(v) = meta.value() {
9306 let lit: syn::LitBool = v.parse()?;
9307 out.permissions = lit.value;
9308 } else {
9309 out.permissions = true;
9310 }
9311 return Ok(());
9312 }
9313 if meta.path.is_ident("view") {
9314 // Issue #293 / T2.10. Two forms accepted, matching
9315 // the `permissions` flag pattern:
9316 // #[rustango(view)] — flag form, true
9317 // #[rustango(view = false)] — explicit opt-out
9318 // #[rustango(view = true)] — explicit opt-in
9319 if let Ok(v) = meta.value() {
9320 let lit: syn::LitBool = v.parse()?;
9321 out.is_view = lit.value;
9322 } else {
9323 out.is_view = true;
9324 }
9325 return Ok(());
9326 }
9327 if meta.path.is_ident("managed") {
9328 // Django-shape Meta.managed. Issue #321.
9329 // #[rustango(managed = false)] — operator-managed table
9330 // #[rustango(managed = true)] — rustango-managed (the default)
9331 // Bare-flag form is intentionally not accepted: writing
9332 // `#[rustango(managed)]` reads as "yes please manage it"
9333 // which is already the default. The opt-out is the only
9334 // useful state, so it must be explicit.
9335 let v = meta.value()?;
9336 let lit: syn::LitBool = v.parse()?;
9337 out.managed = lit.value;
9338 return Ok(());
9339 }
9340 if meta.path.is_ident("verbose_name") {
9341 let s: LitStr = meta.value()?.parse()?;
9342 out.verbose_name = Some(s.value());
9343 return Ok(());
9344 }
9345 if meta.path.is_ident("verbose_name_plural") {
9346 let s: LitStr = meta.value()?.parse()?;
9347 out.verbose_name_plural = Some(s.value());
9348 return Ok(());
9349 }
9350 if meta.path.is_ident("db_table_comment") {
9351 // Django-shape `Meta.db_table_comment` (4.2+) — free-form
9352 // table-level comment attached to the DB catalog.
9353 let s: LitStr = meta.value()?.parse()?;
9354 out.db_table_comment = Some(s.value());
9355 return Ok(());
9356 }
9357 if meta.path.is_ident("proxy") {
9358 // Django-shape `Meta.proxy = True` — declarative flag
9359 // marking the struct as a proxy of another model that
9360 // shares its DB table. Stored on `ModelSchema::proxy`
9361 // so future codegen can skip `CreateTable` emission
9362 // for proxies (parent owns the table) and route
9363 // per-instance method resolution to the proxy class.
9364 //
9365 // Accepts `proxy` (bare → true) and `proxy = true/false`.
9366 let value = if meta.input.peek(syn::Token![=]) {
9367 meta.value()?.parse::<syn::LitBool>()?.value
9368 } else {
9369 true
9370 };
9371 out.proxy = value;
9372 return Ok(());
9373 }
9374 if meta.path.is_ident("order_with_respect_to") {
9375 // Django-shape `Meta.order_with_respect_to = "parent_fk"` —
9376 // the model's instances are intrinsically ordered
9377 // relative to their parent FK. Django auto-generates
9378 // a `_order` integer column + admin reordering UI.
9379 //
9380 // rustango stores the FK field name on
9381 // `ModelSchema::order_with_respect_to`. Declarative-only
9382 // today: the migration writer + admin surfaces still
9383 // treat every model identically. Future codegen will
9384 // key off the metadata to auto-emit the `_order`
9385 // column and reorder helpers.
9386 //
9387 // Validated as a Rust-shape identifier so the macro
9388 // can reject typos at derive time.
9389 let s: LitStr = meta.value()?.parse()?;
9390 let raw = s.value();
9391 if raw.is_empty() {
9392 return Err(syn::Error::new(
9393 s.span(),
9394 "`order_with_respect_to` must be a non-empty FK field name",
9395 ));
9396 }
9397 let valid = raw
9398 .chars()
9399 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9400 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9401 if !valid {
9402 return Err(syn::Error::new(
9403 s.span(),
9404 format!(
9405 "`order_with_respect_to` must be a valid Rust \
9406 identifier (letters / digits / underscores, \
9407 not starting with a digit); got `{raw}`"
9408 ),
9409 ));
9410 }
9411 out.order_with_respect_to = Some(raw);
9412 return Ok(());
9413 }
9414 if meta.path.is_ident("required_db_features") {
9415 // Django-shape `Meta.required_db_features` — capability
9416 // tokens the model needs (e.g. `"json_extract"`,
9417 // `"window_functions"`, `"row_security"`). Comma-separated.
9418 // `manage check --deploy` walks every model and warns
9419 // when the active backend doesn't advertise the
9420 // capability.
9421 //
9422 // rustango ships a small registry of capability tokens
9423 // each dialect supports — see `Dialect::supports`.
9424 // Unknown tokens still parse (they end up on the
9425 // schema and show up in the warning) so projects can
9426 // declare aspirational capabilities and the check
9427 // verb will keep nagging until the dialect implements
9428 // them.
9429 let s: LitStr = meta.value()?.parse()?;
9430 let raw = s.value();
9431 let features: Vec<String> = raw
9432 .split(',')
9433 .map(str::trim)
9434 .filter(|s| !s.is_empty())
9435 .map(str::to_owned)
9436 .collect();
9437 if features.is_empty() {
9438 return Err(syn::Error::new(
9439 s.span(),
9440 "`required_db_features` must list at least one \
9441 comma-separated capability token",
9442 ));
9443 }
9444 out.required_db_features = features;
9445 return Ok(());
9446 }
9447 if meta.path.is_ident("required_db_vendor") {
9448 // Django-shape `Meta.required_db_vendor` — the model
9449 // is only meant to run against the named DB backend.
9450 // `manage check --deploy` flags a mismatch so
9451 // ops catches "I forgot to switch DATABASE_URL" at
9452 // deploy time rather than runtime.
9453 //
9454 // Django spells it as a free-form string; rustango
9455 // restricts to the three backends it ships dialects
9456 // for so the check verb can compare reliably.
9457 let s: LitStr = meta.value()?.parse()?;
9458 let raw = s.value().to_ascii_lowercase();
9459 match raw.as_str() {
9460 "postgresql" | "postgres" | "pg" => {
9461 out.required_db_vendor = Some("postgres".to_owned());
9462 }
9463 "mysql" | "mariadb" => {
9464 out.required_db_vendor = Some("mysql".to_owned());
9465 }
9466 "sqlite" | "sqlite3" => {
9467 out.required_db_vendor = Some("sqlite".to_owned());
9468 }
9469 _ => {
9470 return Err(syn::Error::new(
9471 s.span(),
9472 format!(
9473 "unknown required_db_vendor `{raw}` — \
9474 expected `postgres` (aliases: `postgresql`, `pg`), \
9475 `mysql` (alias: `mariadb`), or `sqlite` \
9476 (alias: `sqlite3`)"
9477 ),
9478 ));
9479 }
9480 }
9481 return Ok(());
9482 }
9483 if meta.path.is_ident("base_manager_name") {
9484 // Django-shape `Meta.base_manager_name` — name of the
9485 // Manager subclass that `<instance>.<relation>_set`
9486 // uses when resolving reverse-relation managers.
9487 // Distinct from `default_manager_name` (what
9488 // `Model.objects` returns at the class level).
9489 // Stored on `ModelSchema::base_manager_name`.
9490 //
9491 // Validated as a Rust identifier so it stays safe to
9492 // re-emit as code in future reverse-manager codegen.
9493 let s: LitStr = meta.value()?.parse()?;
9494 let raw = s.value();
9495 if raw.is_empty() {
9496 return Err(syn::Error::new(
9497 s.span(),
9498 "`base_manager_name` must be a non-empty string",
9499 ));
9500 }
9501 let valid = raw
9502 .chars()
9503 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9504 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9505 if !valid {
9506 return Err(syn::Error::new(
9507 s.span(),
9508 format!(
9509 "`base_manager_name` must be a valid Rust \
9510 identifier (letters / digits / underscores, \
9511 not starting with a digit); got `{raw}`"
9512 ),
9513 ));
9514 }
9515 out.base_manager_name = Some(raw);
9516 return Ok(());
9517 }
9518 if meta.path.is_ident("default_related_name") {
9519 // Django-shape `Meta.default_related_name` — the name
9520 // reverse-relation accessors use when callers don't
9521 // override `related_name=...` on the FK / M2M field.
9522 // Stored on `ModelSchema::default_related_name` so
9523 // future reverse-manager codegen / DRF schema emit /
9524 // admin templates can pick the right accessor name
9525 // (today rustango doesn't auto-emit reverse managers;
9526 // the metadata is the foundation for that work).
9527 //
9528 // Django requires snake_case + no `+` suffix; we
9529 // enforce non-empty + ASCII identifier-shape so the
9530 // string is safe to use as a Rust ident later.
9531 let s: LitStr = meta.value()?.parse()?;
9532 let raw = s.value();
9533 if raw.is_empty() {
9534 return Err(syn::Error::new(
9535 s.span(),
9536 "`default_related_name` must be a non-empty string",
9537 ));
9538 }
9539 let valid = raw
9540 .chars()
9541 .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9542 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9543 if !valid {
9544 return Err(syn::Error::new(
9545 s.span(),
9546 format!(
9547 "`default_related_name` must be snake_case ASCII \
9548 (lowercase letters / digits / underscores, not \
9549 starting with a digit); got `{raw}`"
9550 ),
9551 ));
9552 }
9553 out.default_related_name = Some(raw);
9554 return Ok(());
9555 }
9556 if meta.path.is_ident("extra_permissions") {
9557 // Django-shape `Meta.permissions = [(codename, name), ...]`.
9558 // Comma-separated `codename:label` pairs.
9559 let s: LitStr = meta.value()?.parse()?;
9560 let raw = s.value();
9561 let mut pairs = Vec::new();
9562 for entry in raw.split(',') {
9563 let entry = entry.trim();
9564 if entry.is_empty() {
9565 continue;
9566 }
9567 let (codename, label) = match entry.split_once(':') {
9568 Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9569 None => (entry.to_owned(), entry.to_owned()),
9570 };
9571 if codename.is_empty() {
9572 return Err(meta.error(
9573 "`extra_permissions` entries must be `codename:label` pairs",
9574 ));
9575 }
9576 pairs.push((codename, label));
9577 }
9578 if pairs.is_empty() {
9579 return Err(meta
9580 .error("`extra_permissions = \"…\"` must list at least one pair"));
9581 }
9582 out.extra_permissions = pairs;
9583 return Ok(());
9584 }
9585 if meta.path.is_ident("default_permissions") {
9586 // Django-shape `Meta.default_permissions = ('view',
9587 // 'change')`. Comma-separated subset of the CRUD
9588 // action set. Empty means all four (the framework
9589 // default — matches Django when the option is
9590 // omitted).
9591 let s: LitStr = meta.value()?.parse()?;
9592 let raw = s.value();
9593 let mut actions: Vec<String> = Vec::new();
9594 for entry in raw.split(',') {
9595 let action = entry.trim().to_ascii_lowercase();
9596 if action.is_empty() {
9597 continue;
9598 }
9599 match action.as_str() {
9600 "add" | "change" | "delete" | "view" => {}
9601 other => {
9602 return Err(syn::Error::new(
9603 s.span(),
9604 format!(
9605 "unknown default_permissions action `{other}` — \
9606 expected one of `add`, `change`, `delete`, `view`"
9607 ),
9608 ));
9609 }
9610 }
9611 if !actions.contains(&action) {
9612 actions.push(action);
9613 }
9614 }
9615 if actions.is_empty() {
9616 return Err(syn::Error::new(
9617 s.span(),
9618 "`default_permissions = \"…\"` must list at least one action; \
9619 use `permissions = false` on the container if you want NO \
9620 permissions seeded for this model.",
9621 ));
9622 }
9623 out.default_permissions = actions;
9624 return Ok(());
9625 }
9626 if meta.path.is_ident("get_latest_by") {
9627 // Django-shape `Meta.get_latest_by`. The `-` prefix
9628 // selects descending order (Django muscle memory).
9629 let s: LitStr = meta.value()?.parse()?;
9630 let raw = s.value();
9631 let trimmed = raw.trim();
9632 if trimmed.is_empty() {
9633 return Err(meta.error("`get_latest_by` must name a column"));
9634 }
9635 let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9636 (stripped.to_owned(), true)
9637 } else if let Some(stripped) = trimmed.strip_prefix('+') {
9638 (stripped.to_owned(), false)
9639 } else {
9640 (trimmed.to_owned(), false)
9641 };
9642 if col.is_empty() {
9643 return Err(meta.error("`get_latest_by` must name a column"));
9644 }
9645 out.get_latest_by = Some((col, desc));
9646 return Ok(());
9647 }
9648 if meta.path.is_ident("unique_together") {
9649 // Django-shape composite UNIQUE index. Two syntaxes:
9650 //
9651 // #[rustango(unique_together = "org_id, user_id")] — auto-derived name
9652 // #[rustango(unique_together(columns = "org_id, user_id", name = "x"))] — explicit name
9653 //
9654 // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9655 // (col1, col2)`, where <name> defaults to
9656 // `<table>_<col1>_<col2>_uq` when not supplied.
9657 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9658 out.indexes.push(IndexAttr {
9659 name,
9660 columns,
9661 unique: true,
9662 method: "btree".to_owned(),
9663 where_clause: None,
9664 include: Vec::new(),
9665 });
9666 return Ok(());
9667 }
9668 if meta.path.is_ident("index_together") {
9669 // Django-shape composite (non-unique) index. Two syntaxes
9670 // mirroring `unique_together`.
9671 //
9672 // #[rustango(index_together = "created_at, status")]
9673 // #[rustango(index_together(columns = "created_at, status", name = "x"))]
9674 let (columns, name) = parse_together_attr(&meta, "index_together")?;
9675 out.indexes.push(IndexAttr {
9676 name,
9677 columns,
9678 unique: false,
9679 method: "btree".to_owned(),
9680 where_clause: None,
9681 include: Vec::new(),
9682 });
9683 return Ok(());
9684 }
9685 if meta.path.is_ident("unique_when") {
9686 // Django 4.0+ `UniqueConstraint(condition=Q(...))` —
9687 // partial unique index. Issue #265 / T1.3.
9688 //
9689 // #[rustango(unique_when(
9690 // columns = "email",
9691 // condition = "deleted_at IS NULL",
9692 // name = "unique_active_email"
9693 // ))]
9694 //
9695 // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9696 // on PG / SQLite (both ship partial indexes natively).
9697 // MySQL falls back to a plain UNIQUE index — the
9698 // condition is lost; document the limitation in the
9699 // generated migration.
9700 let mut columns: Option<Vec<String>> = None;
9701 let mut condition: Option<String> = None;
9702 let mut name: Option<String> = None;
9703 let mut include: Vec<String> = Vec::new();
9704 meta.parse_nested_meta(|inner| {
9705 if inner.path.is_ident("columns") {
9706 let s: LitStr = inner.value()?.parse()?;
9707 columns = Some(split_field_list(&s.value()));
9708 return Ok(());
9709 }
9710 if inner.path.is_ident("condition") {
9711 let s: LitStr = inner.value()?.parse()?;
9712 condition = Some(s.value());
9713 return Ok(());
9714 }
9715 if inner.path.is_ident("name") {
9716 let s: LitStr = inner.value()?.parse()?;
9717 name = Some(s.value());
9718 return Ok(());
9719 }
9720 if inner.path.is_ident("include") {
9721 // Django `UniqueConstraint(include=[...])` — PG
9722 // 11+ covering-index columns. Non-key columns
9723 // travel with the index leaf for index-only
9724 // scans. Dropped on MySQL/SQLite by the writer.
9725 let s: LitStr = inner.value()?.parse()?;
9726 include = split_field_list(&s.value());
9727 return Ok(());
9728 }
9729 Err(inner.error(
9730 "unknown unique_when attribute (supported: \
9731 `columns = \"...\"`, `condition = \"...\"`, \
9732 `name = \"...\"`, `include = \"...\"`)",
9733 ))
9734 })?;
9735 let columns = columns.ok_or_else(|| {
9736 meta.error("`unique_when(...)` requires `columns = \"...\"`")
9737 })?;
9738 let condition = condition.ok_or_else(|| {
9739 meta.error("`unique_when(...)` requires `condition = \"...\"`")
9740 })?;
9741 if columns.is_empty() {
9742 return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9743 }
9744 out.indexes.push(IndexAttr {
9745 name,
9746 columns,
9747 unique: true,
9748 method: "btree".to_owned(),
9749 where_clause: Some(condition),
9750 include,
9751 });
9752 return Ok(());
9753 }
9754 if meta.path.is_ident("index_when") {
9755 // Django `Index(fields=..., condition=Q(...))` parity —
9756 // non-unique partial index. Sibling of `unique_when`
9757 // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9758 //
9759 // #[rustango(index_when(
9760 // columns = "status, created_at",
9761 // condition = "deleted_at IS NULL",
9762 // name = "active_status_created_idx"
9763 // ))]
9764 //
9765 // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9766 // on PG / SQLite (both ship partial indexes natively).
9767 // MySQL has no native partial-index support — the writer
9768 // emits a plain CREATE INDEX and the condition is lost;
9769 // operators wanting that selectivity on MySQL should
9770 // declare a covering index plus an application-level
9771 // filter.
9772 let mut columns: Option<Vec<String>> = None;
9773 let mut condition: Option<String> = None;
9774 let mut name: Option<String> = None;
9775 let mut method: String = "btree".to_owned();
9776 let mut include: Vec<String> = Vec::new();
9777 meta.parse_nested_meta(|inner| {
9778 if inner.path.is_ident("columns") {
9779 let s: LitStr = inner.value()?.parse()?;
9780 columns = Some(split_field_list(&s.value()));
9781 return Ok(());
9782 }
9783 if inner.path.is_ident("condition") {
9784 let s: LitStr = inner.value()?.parse()?;
9785 condition = Some(s.value());
9786 return Ok(());
9787 }
9788 if inner.path.is_ident("name") {
9789 let s: LitStr = inner.value()?.parse()?;
9790 name = Some(s.value());
9791 return Ok(());
9792 }
9793 if inner.path.is_ident("method") {
9794 let s: LitStr = inner.value()?.parse()?;
9795 method = s.value();
9796 return Ok(());
9797 }
9798 if inner.path.is_ident("include") {
9799 // Django `Index(include=[...])` — PG 11+
9800 // covering-index columns; non-key columns
9801 // travel with the index leaf. Dropped on
9802 // MySQL/SQLite.
9803 let s: LitStr = inner.value()?.parse()?;
9804 include = split_field_list(&s.value());
9805 return Ok(());
9806 }
9807 Err(inner.error(
9808 "unknown index_when attribute (supported: \
9809 `columns = \"...\"`, `condition = \"...\"`, \
9810 `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9811 `include = \"...\"`)",
9812 ))
9813 })?;
9814 let columns = columns
9815 .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9816 let condition = condition.ok_or_else(|| {
9817 meta.error("`index_when(...)` requires `condition = \"...\"`")
9818 })?;
9819 if columns.is_empty() {
9820 return Err(meta.error("`index_when(columns = \"\")` is empty"));
9821 }
9822 out.indexes.push(IndexAttr {
9823 name,
9824 columns,
9825 unique: false,
9826 method,
9827 where_clause: Some(condition),
9828 include,
9829 });
9830 return Ok(());
9831 }
9832 if meta.path.is_ident("index") {
9833 // Container-level composite index. Two syntaxes:
9834 // #[rustango(index = "col1, col2")] — bare, non-unique btree
9835 // #[rustango(index("col1, col2"))] — call form (same result)
9836 // #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
9837 // The call form takes the column list as a leading string
9838 // literal, then optional `unique` / `name = "..."` /
9839 // `method = "..."` flags (a leading literal can't compose
9840 // under `parse_nested_meta`, so the paren body is parsed by
9841 // hand). `unique_together` / `index_together` remain the
9842 // Django-shape aliases for the same feature.
9843 let cols_lit: LitStr;
9844 let mut unique = false;
9845 let mut name: Option<String> = None;
9846 let mut method = "btree".to_owned();
9847 if meta.input.peek(syn::token::Paren) {
9848 let content;
9849 syn::parenthesized!(content in meta.input);
9850 cols_lit = content.parse()?;
9851 while content.peek(syn::Token![,]) {
9852 content.parse::<syn::Token![,]>()?;
9853 if content.is_empty() {
9854 break;
9855 }
9856 let flag: syn::Ident = content.parse()?;
9857 if flag == "unique" {
9858 unique = true;
9859 } else if flag == "name" {
9860 content.parse::<syn::Token![=]>()?;
9861 let s: LitStr = content.parse()?;
9862 name = Some(s.value());
9863 } else if flag == "method" {
9864 content.parse::<syn::Token![=]>()?;
9865 let s: LitStr = content.parse()?;
9866 let v = s.value();
9867 match v.as_str() {
9868 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
9869 | "bloom" => method = v,
9870 other => {
9871 return Err(syn::Error::new(
9872 s.span(),
9873 format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
9874 ));
9875 }
9876 }
9877 } else {
9878 return Err(syn::Error::new(
9879 flag.span(),
9880 "unknown index flag (supported: `unique`, `name`, `method`)",
9881 ));
9882 }
9883 }
9884 } else {
9885 cols_lit = meta.value()?.parse()?;
9886 }
9887 let columns = split_field_list(&cols_lit.value());
9888 out.indexes.push(IndexAttr {
9889 name,
9890 columns,
9891 unique,
9892 method,
9893 where_clause: None,
9894 include: Vec::new(),
9895 });
9896 return Ok(());
9897 }
9898 if meta.path.is_ident("check") {
9899 // #[rustango(check(name = "…", expr = "…"))]
9900 let mut name: Option<String> = None;
9901 let mut expr: Option<String> = None;
9902 meta.parse_nested_meta(|inner| {
9903 if inner.path.is_ident("name") {
9904 let s: LitStr = inner.value()?.parse()?;
9905 name = Some(s.value());
9906 return Ok(());
9907 }
9908 if inner.path.is_ident("expr") {
9909 let s: LitStr = inner.value()?.parse()?;
9910 expr = Some(s.value());
9911 return Ok(());
9912 }
9913 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
9914 })?;
9915 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
9916 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
9917 out.checks.push(CheckAttr { name, expr });
9918 return Ok(());
9919 }
9920 if meta.path.is_ident("exclude") {
9921 // #[rustango(exclude(name = "…", using = "gist",
9922 // elements = "col WITH op, col WITH op",
9923 // where = "…"))]
9924 let mut name: Option<String> = None;
9925 let mut using: Option<String> = None;
9926 let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
9927 let mut where_clause: Option<String> = None;
9928 meta.parse_nested_meta(|inner| {
9929 if inner.path.is_ident("name") {
9930 let s: LitStr = inner.value()?.parse()?;
9931 name = Some(s.value());
9932 return Ok(());
9933 }
9934 if inner.path.is_ident("using") {
9935 let s: LitStr = inner.value()?.parse()?;
9936 using = Some(s.value());
9937 return Ok(());
9938 }
9939 if inner.path.is_ident("elements") {
9940 let s: LitStr = inner.value()?.parse()?;
9941 elements_raw = Some((s.value(), s.span()));
9942 return Ok(());
9943 }
9944 if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
9945 let s: LitStr = inner.value()?.parse()?;
9946 where_clause = Some(s.value());
9947 return Ok(());
9948 }
9949 Err(inner.error(
9950 "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
9951 ))
9952 })?;
9953 let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
9954 let using = using.unwrap_or_else(|| "gist".to_owned());
9955 let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
9956 meta.error(
9957 "exclude requires `elements = \"col WITH op, col WITH op\"`",
9958 )
9959 })?;
9960 // Parse `col WITH op` pairs separated by commas.
9961 let mut elements: Vec<(String, String)> = Vec::new();
9962 for pair in elements_str.split(',') {
9963 let pair = pair.trim();
9964 if pair.is_empty() {
9965 continue;
9966 }
9967 let mut split = pair.splitn(2, |c: char| c.is_whitespace());
9968 let col = split.next().unwrap_or("").trim();
9969 let rest = split.next().unwrap_or("").trim();
9970 // `WITH op` — case-insensitive on `WITH`, then op.
9971 let rest_lc = rest.to_ascii_lowercase();
9972 let op = rest_lc
9973 .strip_prefix("with")
9974 .map(|r| r.trim_start())
9975 .filter(|r| !r.is_empty())
9976 .map(|_| {
9977 // Pull the original-case op from `rest` after the
9978 // `WITH ` token (5 chars).
9979 rest[4..].trim_start().to_owned()
9980 });
9981 let Some(op) = op else {
9982 return Err(syn::Error::new(
9983 elements_span,
9984 format!(
9985 "exclude elements: `{pair}` must be `<col> WITH <op>` \
9986 (e.g. `room_id WITH =` or `during WITH &&`)"
9987 ),
9988 ));
9989 };
9990 if col.is_empty() || op.is_empty() {
9991 return Err(syn::Error::new(
9992 elements_span,
9993 format!(
9994 "exclude elements: `{pair}` must be `<col> WITH <op>` \
9995 (both sides non-empty)"
9996 ),
9997 ));
9998 }
9999 elements.push((col.to_owned(), op));
10000 }
10001 if elements.is_empty() {
10002 return Err(syn::Error::new(
10003 elements_span,
10004 "exclude requires at least one `col WITH op` element",
10005 ));
10006 }
10007 out.excludes.push(ExcludeAttr {
10008 name,
10009 using,
10010 elements,
10011 where_clause,
10012 });
10013 return Ok(());
10014 }
10015 if meta.path.is_ident("generic_fk") {
10016 let mut gfk = GenericFkAttr {
10017 name: String::new(),
10018 ct_column: String::new(),
10019 pk_column: String::new(),
10020 };
10021 meta.parse_nested_meta(|inner| {
10022 if inner.path.is_ident("name") {
10023 let s: LitStr = inner.value()?.parse()?;
10024 gfk.name = s.value();
10025 return Ok(());
10026 }
10027 if inner.path.is_ident("ct_column") {
10028 let s: LitStr = inner.value()?.parse()?;
10029 gfk.ct_column = s.value();
10030 return Ok(());
10031 }
10032 if inner.path.is_ident("pk_column") {
10033 let s: LitStr = inner.value()?.parse()?;
10034 gfk.pk_column = s.value();
10035 return Ok(());
10036 }
10037 Err(inner.error(
10038 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10039 ))
10040 })?;
10041 if gfk.name.is_empty() {
10042 return Err(meta.error("generic_fk requires `name = \"...\"`"));
10043 }
10044 if gfk.ct_column.is_empty() {
10045 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10046 }
10047 if gfk.pk_column.is_empty() {
10048 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10049 }
10050 out.generic_fks.push(gfk);
10051 return Ok(());
10052 }
10053 if meta.path.is_ident("fk_composite") {
10054 let mut fk = CompositeFkAttr {
10055 name: String::new(),
10056 to: String::new(),
10057 from: Vec::new(),
10058 on: Vec::new(),
10059 };
10060 meta.parse_nested_meta(|inner| {
10061 if inner.path.is_ident("name") {
10062 let s: LitStr = inner.value()?.parse()?;
10063 fk.name = s.value();
10064 return Ok(());
10065 }
10066 if inner.path.is_ident("to") {
10067 let s: LitStr = inner.value()?.parse()?;
10068 fk.to = s.value();
10069 return Ok(());
10070 }
10071 // `on = ("col1", "col2", ...)` — parse a parenthesised
10072 // comma-list of string literals.
10073 if inner.path.is_ident("on") || inner.path.is_ident("from") {
10074 let value = inner.value()?;
10075 let content;
10076 syn::parenthesized!(content in value);
10077 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10078 content.parse_terminated(
10079 |p| p.parse::<syn::LitStr>(),
10080 syn::Token![,],
10081 )?;
10082 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10083 if inner.path.is_ident("on") {
10084 fk.on = cols;
10085 } else {
10086 fk.from = cols;
10087 }
10088 return Ok(());
10089 }
10090 Err(inner.error(
10091 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10092 ))
10093 })?;
10094 if fk.name.is_empty() {
10095 return Err(meta.error("fk_composite requires `name = \"...\"`"));
10096 }
10097 if fk.to.is_empty() {
10098 return Err(meta.error("fk_composite requires `to = \"...\"`"));
10099 }
10100 if fk.from.is_empty() || fk.on.is_empty() {
10101 return Err(meta.error(
10102 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10103 ));
10104 }
10105 if fk.from.len() != fk.on.len() {
10106 return Err(meta.error(format!(
10107 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10108 fk.from.len(),
10109 fk.on.len(),
10110 )));
10111 }
10112 out.composite_fks.push(fk);
10113 return Ok(());
10114 }
10115 if meta.path.is_ident("m2m") {
10116 let mut m2m = M2MAttr {
10117 name: String::new(),
10118 to: String::new(),
10119 through: String::new(),
10120 src: String::new(),
10121 dst: String::new(),
10122 auto_create: true,
10123 };
10124 meta.parse_nested_meta(|inner| {
10125 if inner.path.is_ident("name") {
10126 let s: LitStr = inner.value()?.parse()?;
10127 m2m.name = s.value();
10128 return Ok(());
10129 }
10130 if inner.path.is_ident("to") {
10131 let s: LitStr = inner.value()?.parse()?;
10132 m2m.to = s.value();
10133 return Ok(());
10134 }
10135 if inner.path.is_ident("through") {
10136 let s: LitStr = inner.value()?.parse()?;
10137 m2m.through = s.value();
10138 return Ok(());
10139 }
10140 if inner.path.is_ident("src") {
10141 let s: LitStr = inner.value()?.parse()?;
10142 m2m.src = s.value();
10143 return Ok(());
10144 }
10145 if inner.path.is_ident("dst") {
10146 let s: LitStr = inner.value()?.parse()?;
10147 m2m.dst = s.value();
10148 return Ok(());
10149 }
10150 if inner.path.is_ident("auto_create") {
10151 let lit: syn::LitBool = inner.value()?.parse()?;
10152 m2m.auto_create = lit.value;
10153 return Ok(());
10154 }
10155 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10156 })?;
10157 if m2m.name.is_empty() {
10158 return Err(meta.error("m2m requires `name = \"...\"`"));
10159 }
10160 if m2m.to.is_empty() {
10161 return Err(meta.error("m2m requires `to = \"...\"`"));
10162 }
10163 if m2m.through.is_empty() {
10164 return Err(meta.error("m2m requires `through = \"...\"`"));
10165 }
10166 if m2m.src.is_empty() {
10167 return Err(meta.error("m2m requires `src = \"...\"`"));
10168 }
10169 if m2m.dst.is_empty() {
10170 return Err(meta.error("m2m requires `dst = \"...\"`"));
10171 }
10172 out.m2m.push(m2m);
10173 return Ok(());
10174 }
10175 if meta.path.is_ident("generic_m2m") {
10176 let mut gm = GenericM2MAttr {
10177 name: String::new(),
10178 through: String::new(),
10179 pk_column: String::new(),
10180 ct_column: String::new(),
10181 related_column: String::new(),
10182 };
10183 meta.parse_nested_meta(|inner| {
10184 let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10185 let s: LitStr = inner.value()?.parse()?;
10186 Ok(s.value())
10187 };
10188 if inner.path.is_ident("name") {
10189 gm.name = field(&inner)?;
10190 return Ok(());
10191 }
10192 if inner.path.is_ident("through") {
10193 gm.through = field(&inner)?;
10194 return Ok(());
10195 }
10196 if inner.path.is_ident("pk_column") {
10197 gm.pk_column = field(&inner)?;
10198 return Ok(());
10199 }
10200 if inner.path.is_ident("ct_column") {
10201 gm.ct_column = field(&inner)?;
10202 return Ok(());
10203 }
10204 if inner.path.is_ident("related_column") {
10205 gm.related_column = field(&inner)?;
10206 return Ok(());
10207 }
10208 Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10209 })?;
10210 for (val, label) in [
10211 (&gm.name, "name"),
10212 (&gm.through, "through"),
10213 (&gm.pk_column, "pk_column"),
10214 (&gm.ct_column, "ct_column"),
10215 (&gm.related_column, "related_column"),
10216 ] {
10217 if val.is_empty() {
10218 return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10219 }
10220 }
10221 out.generic_m2m.push(gm);
10222 return Ok(());
10223 }
10224 Err(meta.error("unknown rustango container attribute"))
10225 })?;
10226 }
10227 Ok(out)
10228}
10229
10230/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10231/// owned field names, trimming whitespace and skipping empty entries.
10232/// Field-name validation against the model is done by the caller.
10233fn split_field_list(raw: &str) -> Vec<String> {
10234 raw.split(',')
10235 .map(str::trim)
10236 .filter(|s| !s.is_empty())
10237 .map(str::to_owned)
10238 .collect()
10239}
10240
10241/// Shared parser for `unique_together` and `index_together` container
10242/// attrs. Accepts both shapes:
10243///
10244/// * `attr = "col1, col2"` — auto-derived index name.
10245/// * `attr(columns = "col1, col2", name = "...")` — explicit name.
10246///
10247/// Returns `(columns, name)`.
10248fn parse_together_attr(
10249 meta: &syn::meta::ParseNestedMeta<'_>,
10250 attr: &str,
10251) -> syn::Result<(Vec<String>, Option<String>)> {
10252 // Disambiguate by whether the next token is `=` (key-value) or
10253 // `(` (parenthesized).
10254 if meta.input.peek(syn::Token![=]) {
10255 let cols_lit: LitStr = meta.value()?.parse()?;
10256 let columns = split_field_list(&cols_lit.value());
10257 check_together_columns(meta, attr, &columns)?;
10258 return Ok((columns, None));
10259 }
10260 let mut columns: Option<Vec<String>> = None;
10261 let mut name: Option<String> = None;
10262 meta.parse_nested_meta(|inner| {
10263 if inner.path.is_ident("columns") {
10264 let s: LitStr = inner.value()?.parse()?;
10265 columns = Some(split_field_list(&s.value()));
10266 return Ok(());
10267 }
10268 if inner.path.is_ident("name") {
10269 let s: LitStr = inner.value()?.parse()?;
10270 name = Some(s.value());
10271 return Ok(());
10272 }
10273 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10274 })?;
10275 let columns = columns.ok_or_else(|| {
10276 meta.error(format!(
10277 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10278 ))
10279 })?;
10280 check_together_columns(meta, attr, &columns)?;
10281 Ok((columns, name))
10282}
10283
10284fn check_together_columns(
10285 meta: &syn::meta::ParseNestedMeta<'_>,
10286 attr: &str,
10287 columns: &[String],
10288) -> syn::Result<()> {
10289 if columns.len() < 2 {
10290 let single = if attr == "unique_together" {
10291 "#[rustango(unique)] on the field"
10292 } else {
10293 "#[rustango(index)] on the field"
10294 };
10295 return Err(meta.error(format!(
10296 "{attr} expects two or more columns; for a single-column equivalent use {single}",
10297 )));
10298 }
10299 Ok(())
10300}
10301
10302/// Parse the fieldsets DSL: pipe-separated sections, optional
10303/// `"Title:"` prefix on each, comma-separated field names after.
10304/// Examples:
10305/// * `"name, office"` → one untitled section with two fields
10306/// * `"Identity: name, office | Metadata: created_at"` → two titled
10307/// sections
10308///
10309/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10310fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10311 raw.split('|')
10312 .map(str::trim)
10313 .filter(|s| !s.is_empty())
10314 .map(|section| {
10315 // Split off an optional `Title:` prefix (first colon).
10316 let (title, rest) = match section.split_once(':') {
10317 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10318 _ => (String::new(), section),
10319 };
10320 let fields = split_field_list(rest);
10321 (title, fields)
10322 })
10323 .collect()
10324}
10325
10326/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10327/// comma-separated entry maps a target field to one or more source
10328/// fields joined with `+`. Whitespace around tokens is trimmed.
10329/// Entries missing `:` or with empty target/source lists are dropped.
10330fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10331 raw.split(',')
10332 .map(str::trim)
10333 .filter(|s| !s.is_empty())
10334 .filter_map(|entry| {
10335 let (target, sources_raw) = entry.split_once(':')?;
10336 let target = target.trim().to_owned();
10337 if target.is_empty() {
10338 return None;
10339 }
10340 let sources: Vec<String> = sources_raw
10341 .split('+')
10342 .map(|s| s.trim().to_owned())
10343 .filter(|s| !s.is_empty())
10344 .collect();
10345 if sources.is_empty() {
10346 return None;
10347 }
10348 Some((target, sources))
10349 })
10350 .collect()
10351}
10352
10353/// Parse Django-shape `formfield_overrides` — `"field:widget,field2:widget2"`
10354/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10355/// and empty halves drop silently — the macro layer only enforces shape,
10356/// not field-name vs. widget-name validity (those checks happen at
10357/// `AdminConfig` consumption time). Issue #359.
10358fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10359 raw.split(',')
10360 .map(str::trim)
10361 .filter(|s| !s.is_empty())
10362 .filter_map(|entry| {
10363 let (field, widget) = entry.split_once(':')?;
10364 let field = field.trim().to_owned();
10365 let widget = widget.trim().to_owned();
10366 if field.is_empty() || widget.is_empty() {
10367 return None;
10368 }
10369 Some((field, widget))
10370 })
10371 .collect()
10372}
10373
10374/// Parse Django-shape ordering — `"name"` is ASC, `"-name"` is DESC.
10375/// Returns `(field_name, desc)` pairs in the same order as the input.
10376fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10377 raw.split(',')
10378 .map(str::trim)
10379 .filter(|s| !s.is_empty())
10380 .map(|spec| {
10381 spec.strip_prefix('-')
10382 .map_or((spec.to_owned(), false), |rest| {
10383 (rest.trim().to_owned(), true)
10384 })
10385 })
10386 .collect()
10387}
10388
10389struct FieldAttrs {
10390 column: Option<String>,
10391 primary_key: bool,
10392 fk: Option<String>,
10393 o2o: Option<String>,
10394 on: Option<String>,
10395 /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10396 /// "set_default" | "no_action")]` — Django-shape
10397 /// `ForeignKey(on_delete=…)`. Only meaningful when `fk` / `o2o` is
10398 /// also set; the macro errors at compile time if applied to a
10399 /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10400 /// DDL writer renders `ON DELETE <action>` after the constraint
10401 /// clause when this is `Some`; `None` falls back to the database
10402 /// default (NO ACTION on every backend rustango supports).
10403 on_delete: Option<String>,
10404 /// `#[rustango(related_name = "...")]` — Django-shape per-FK
10405 /// reverse-accessor override. When set, the derive emits
10406 /// `Parent::<related_name>[_pool]` instead of the container-level
10407 /// `default_related_name` or the `<child_snake>_set[_pool]`
10408 /// fallback. Only meaningful when `fk` / `o2o` is also set;
10409 /// silently ignored on non-FK fields. Follow-up to #816.
10410 related_name: Option<String>,
10411 max_length: Option<u32>,
10412 /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10413 /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10414 /// unconstrained `vector` column.
10415 vector_dims: Option<u32>,
10416 /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10417 /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10418 /// unconstrained `geometry(Point)` column (SRID 0).
10419 geometry_srid: Option<u32>,
10420 min: Option<i64>,
10421 max: Option<i64>,
10422 default: Option<String>,
10423 /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10424 /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10425 /// "gen_random_uuid()"`. The Rust field type must be
10426 /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10427 /// INSERTs so the DB DEFAULT fires.
10428 auto_uuid: bool,
10429 /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10430 /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10431 /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10432 /// the field is `Auto::Unset`, then bound as a normal parameter
10433 /// rather than relying on a per-dialect DB function. Issue #823
10434 /// (Eloquent `HasUuids`).
10435 ///
10436 /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10437 /// Composes with every backend (PG / MySQL / SQLite) — no
10438 /// `gen_random_uuid()` requirement on the database.
10439 default_uuid_v7: bool,
10440 /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10441 /// Server-set on insert, immutable from app code afterwards.
10442 /// Implies `auto + default = "now()"`. Field type must be
10443 /// `DateTime<Utc>`.
10444 auto_now_add: bool,
10445 /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10446 /// every insert AND every update. Implies `auto + default =
10447 /// "now()"`; the macro additionally rewrites `update_on` /
10448 /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10449 /// field value.
10450 auto_now: bool,
10451 /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10452 /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10453 /// `soft_delete_on(executor)` and `restore_on(executor)`
10454 /// methods on the model.
10455 soft_delete: bool,
10456 /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10457 /// the column in the generated DDL.
10458 unique: bool,
10459 /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10460 /// generates a `CREATE INDEX` for this column. `unique` here means
10461 /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10462 index: bool,
10463 index_unique: bool,
10464 index_name: Option<String>,
10465 /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10466 /// `"btree"`. Issue #34.
10467 index_method: String,
10468 /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10469 /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10470 /// the macro skips this column from every INSERT and UPDATE
10471 /// path, so the database always recomputes the value from
10472 /// `EXPR`. Backlog item #35.
10473 generated_as: Option<String>,
10474 /// `#[rustango(help_text = "…")]` — Django-shape help text
10475 /// rendered below the admin form's input. Threaded into
10476 /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10477 /// layers can read it.
10478 help_text: Option<String>,
10479 /// `#[rustango(choices = "value:Label, value:Label")]` — Django-shape
10480 /// enumerated allowed values. Threaded into `FieldSchema::choices`
10481 /// as a `&'static [(&'static str, &'static str)]` slice. When
10482 /// present, the admin form renders a `<select>` instead of `<input>`
10483 /// and the validator rejects values not in the list. Only meaningful
10484 /// for `FieldType::String`; the macro errors at compile time if
10485 /// applied to a non-string field.
10486 choices: Option<Vec<(String, String)>>,
10487 /// `#[rustango(db_comment = "…")]` — Django-shape DB-side column
10488 /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10489 /// the comment in CREATE TABLE; Postgres emits a separate
10490 /// `COMMENT ON COLUMN` statement after the table is created;
10491 /// SQLite silently drops the value (no native column comments).
10492 db_comment: Option<String>,
10493 /// `#[rustango(verbose_name = "…")]` — Django-shape human-readable
10494 /// label for the field. Threaded into `FieldSchema::verbose_name`
10495 /// so admin column headers, form labels, and other display
10496 /// surfaces can prefer the friendly caption over the Rust
10497 /// identifier. `None` means renderers fall back to the field name.
10498 verbose_name: Option<String>,
10499 /// `#[rustango(editable = false)]` — Django-shape opt-out from
10500 /// auto-generated form rendering. Defaults to `true` so existing
10501 /// fields keep their current admin / form behavior; setting
10502 /// `false` removes the field from the admin change-form entirely
10503 /// (the value is still visible on detail / list views, just not
10504 /// editable).
10505 editable: bool,
10506 /// `#[rustango(blank)]` / `#[rustango(blank = true)]` — Django-shape
10507 /// "form may submit empty even when DB is NOT NULL". Threaded into
10508 /// `FieldSchema::blank`. Defaults to `false`.
10509 blank: bool,
10510 /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10511 /// Django-shape `CITextField`. Threaded into
10512 /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10513 /// fields; the macro errors at derive time if applied elsewhere.
10514 case_insensitive: bool,
10515 /// `#[rustango(validators = "email,url")]` — Django-shape
10516 /// model-level validator chain. Comma-separated names that
10517 /// dispatch to the `validators::*` family in `validate_value`.
10518 /// Empty by default; fires on every typed INSERT/UPDATE.
10519 validators: Vec<String>,
10520}
10521
10522fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10523 let mut out = FieldAttrs {
10524 column: None,
10525 primary_key: false,
10526 fk: None,
10527 o2o: None,
10528 on: None,
10529 on_delete: None,
10530 related_name: None,
10531 max_length: None,
10532 vector_dims: None,
10533 geometry_srid: None,
10534 min: None,
10535 max: None,
10536 default: None,
10537 auto_uuid: false,
10538 default_uuid_v7: false,
10539 auto_now_add: false,
10540 auto_now: false,
10541 soft_delete: false,
10542 unique: false,
10543 index: false,
10544 index_unique: false,
10545 index_name: None,
10546 index_method: "btree".to_owned(),
10547 generated_as: None,
10548 help_text: None,
10549 choices: None,
10550 db_comment: None,
10551 verbose_name: None,
10552 editable: true,
10553 blank: false,
10554 case_insensitive: false,
10555 validators: Vec::new(),
10556 };
10557 for attr in &field.attrs {
10558 if !attr.path().is_ident("rustango") {
10559 continue;
10560 }
10561 attr.parse_nested_meta(|meta| {
10562 if meta.path.is_ident("column") {
10563 let s: LitStr = meta.value()?.parse()?;
10564 let name = s.value();
10565 validate_sql_identifier(&name, "column", s.span())?;
10566 out.column = Some(name);
10567 return Ok(());
10568 }
10569 if meta.path.is_ident("primary_key") {
10570 out.primary_key = true;
10571 return Ok(());
10572 }
10573 if meta.path.is_ident("fk") {
10574 let s: LitStr = meta.value()?.parse()?;
10575 out.fk = Some(s.value());
10576 return Ok(());
10577 }
10578 if meta.path.is_ident("o2o") {
10579 let s: LitStr = meta.value()?.parse()?;
10580 out.o2o = Some(s.value());
10581 return Ok(());
10582 }
10583 if meta.path.is_ident("on") {
10584 let s: LitStr = meta.value()?.parse()?;
10585 out.on = Some(s.value());
10586 return Ok(());
10587 }
10588 if meta.path.is_ident("on_delete") {
10589 let s: LitStr = meta.value()?.parse()?;
10590 let raw = s.value();
10591 let normalized = raw.trim().to_ascii_lowercase();
10592 // Validate at parse time so the user gets a clear span
10593 // rather than a downstream compile error in the emit.
10594 match normalized.as_str() {
10595 "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10596 _ => {
10597 return Err(syn::Error::new(
10598 s.span(),
10599 format!(
10600 "unknown on_delete action `{raw}`; expected one of \
10601 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10602 ),
10603 ));
10604 }
10605 }
10606 out.on_delete = Some(normalized);
10607 return Ok(());
10608 }
10609 if meta.path.is_ident("related_name") {
10610 let s: LitStr = meta.value()?.parse()?;
10611 let raw = s.value();
10612 if raw.trim().is_empty() {
10613 return Err(syn::Error::new(
10614 s.span(),
10615 "`related_name` must be a non-empty identifier",
10616 ));
10617 }
10618 // Validate as a Rust-ident shape — the value becomes
10619 // a method name on the parent type. Same rule as
10620 // `default_related_name` so the two surfaces match.
10621 if !raw
10622 .chars()
10623 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10624 || raw.starts_with(char::is_numeric)
10625 {
10626 return Err(syn::Error::new(
10627 s.span(),
10628 "`related_name` must be snake_case ASCII (lowercase letters, \
10629 digits, underscores; no leading digit)",
10630 ));
10631 }
10632 out.related_name = Some(raw);
10633 return Ok(());
10634 }
10635 if meta.path.is_ident("max_length") {
10636 let lit: syn::LitInt = meta.value()?.parse()?;
10637 out.max_length = Some(lit.base10_parse::<u32>()?);
10638 return Ok(());
10639 }
10640 // `#[rustango(vector(dims = N))]` — pgvector column
10641 // dimension (#824). Nested-meta form so it reads like the
10642 // other typed-column attrs.
10643 if meta.path.is_ident("vector") {
10644 meta.parse_nested_meta(|inner| {
10645 if inner.path.is_ident("dims") {
10646 let lit: syn::LitInt = inner.value()?.parse()?;
10647 out.vector_dims = Some(lit.base10_parse::<u32>()?);
10648 return Ok(());
10649 }
10650 Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10651 })?;
10652 return Ok(());
10653 }
10654 // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10655 // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10656 if meta.path.is_ident("geometry") {
10657 meta.parse_nested_meta(|inner| {
10658 if inner.path.is_ident("srid") {
10659 let lit: syn::LitInt = inner.value()?.parse()?;
10660 out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10661 return Ok(());
10662 }
10663 Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10664 })?;
10665 return Ok(());
10666 }
10667 if meta.path.is_ident("min") {
10668 out.min = Some(parse_signed_i64(&meta)?);
10669 return Ok(());
10670 }
10671 if meta.path.is_ident("max") {
10672 out.max = Some(parse_signed_i64(&meta)?);
10673 return Ok(());
10674 }
10675 if meta.path.is_ident("default") {
10676 let s: LitStr = meta.value()?.parse()?;
10677 out.default = Some(s.value());
10678 return Ok(());
10679 }
10680 if meta.path.is_ident("generated_as") {
10681 let s: LitStr = meta.value()?.parse()?;
10682 out.generated_as = Some(s.value());
10683 return Ok(());
10684 }
10685 if meta.path.is_ident("help_text") {
10686 let s: LitStr = meta.value()?.parse()?;
10687 out.help_text = Some(s.value());
10688 return Ok(());
10689 }
10690 if meta.path.is_ident("choices") {
10691 let s: LitStr = meta.value()?.parse()?;
10692 let raw = s.value();
10693 let mut pairs: Vec<(String, String)> = Vec::new();
10694 for chunk in raw.split(',') {
10695 let chunk = chunk.trim();
10696 if chunk.is_empty() {
10697 continue;
10698 }
10699 let (value, label) = match chunk.split_once(':') {
10700 Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10701 None => (chunk.to_owned(), chunk.to_owned()),
10702 };
10703 if value.is_empty() {
10704 return Err(syn::Error::new(
10705 s.span(),
10706 "`choices` entry has empty value before `:`",
10707 ));
10708 }
10709 pairs.push((value, label));
10710 }
10711 if pairs.is_empty() {
10712 return Err(syn::Error::new(
10713 s.span(),
10714 "`choices = \"…\"` must contain at least one value",
10715 ));
10716 }
10717 out.choices = Some(pairs);
10718 return Ok(());
10719 }
10720 if meta.path.is_ident("db_comment") {
10721 let s: LitStr = meta.value()?.parse()?;
10722 out.db_comment = Some(s.value());
10723 return Ok(());
10724 }
10725 if meta.path.is_ident("verbose_name") {
10726 let s: LitStr = meta.value()?.parse()?;
10727 out.verbose_name = Some(s.value());
10728 return Ok(());
10729 }
10730 if meta.path.is_ident("editable") {
10731 // Two forms accepted:
10732 // #[rustango(editable = false)] / true — explicit
10733 // #[rustango(editable)] — flag form (= true, the
10734 // default, so harmless; included for symmetry)
10735 if let Ok(v) = meta.value() {
10736 let lit: syn::LitBool = v.parse()?;
10737 out.editable = lit.value;
10738 } else {
10739 out.editable = true;
10740 }
10741 return Ok(());
10742 }
10743 if meta.path.is_ident("blank") {
10744 // Two forms accepted:
10745 // #[rustango(blank)] — flag form, true
10746 // #[rustango(blank = false)] / true — explicit
10747 if let Ok(v) = meta.value() {
10748 let lit: syn::LitBool = v.parse()?;
10749 out.blank = lit.value;
10750 } else {
10751 out.blank = true;
10752 }
10753 return Ok(());
10754 }
10755 if meta.path.is_ident("citext") {
10756 // Django-parity CITextField (#344). Two forms:
10757 // #[rustango(citext)] — flag form, true
10758 // #[rustango(citext = true)] — explicit
10759 // #[rustango(citext = false)] — explicit opt-out
10760 // String-only validation lives in the field-type
10761 // emitter (the FieldType discriminant is computed in
10762 // `detect_type`); the macro records the flag and the
10763 // DDL writer emits dialect-specific COLLATE / CITEXT.
10764 if let Ok(v) = meta.value() {
10765 let lit: syn::LitBool = v.parse()?;
10766 out.case_insensitive = lit.value;
10767 } else {
10768 out.case_insensitive = true;
10769 }
10770 return Ok(());
10771 }
10772 if meta.path.is_ident("validators") {
10773 let s: LitStr = meta.value()?.parse()?;
10774 let raw = s.value();
10775 out.validators = raw
10776 .split(',')
10777 .map(str::trim)
10778 .filter(|s| !s.is_empty())
10779 .map(str::to_owned)
10780 .collect();
10781 if out.validators.is_empty() {
10782 return Err(syn::Error::new(
10783 s.span(),
10784 "`validators = \"…\"` must list at least one name",
10785 ));
10786 }
10787 return Ok(());
10788 }
10789 if meta.path.is_ident("auto_uuid") {
10790 out.auto_uuid = true;
10791 // Implied: PK + auto + DEFAULT gen_random_uuid().
10792 // Each is also explicitly settable; the explicit
10793 // value wins if conflicting.
10794 out.primary_key = true;
10795 if out.default.is_none() {
10796 out.default = Some("gen_random_uuid()".into());
10797 }
10798 return Ok(());
10799 }
10800 if meta.path.is_ident("default_uuid_v7") {
10801 // Backend-neutral counterpart of `auto_uuid` — issue #823.
10802 // No SQL DEFAULT (the macro fills the value Rust-side
10803 // before binding); just mark the field as PK + Auto
10804 // so the insert path is the `Auto::Unset → generate`
10805 // branch.
10806 out.default_uuid_v7 = true;
10807 out.primary_key = true;
10808 return Ok(());
10809 }
10810 if meta.path.is_ident("auto_now_add") {
10811 out.auto_now_add = true;
10812 if out.default.is_none() {
10813 out.default = Some("now()".into());
10814 }
10815 return Ok(());
10816 }
10817 if meta.path.is_ident("auto_now") {
10818 out.auto_now = true;
10819 if out.default.is_none() {
10820 out.default = Some("now()".into());
10821 }
10822 return Ok(());
10823 }
10824 if meta.path.is_ident("soft_delete") {
10825 out.soft_delete = true;
10826 return Ok(());
10827 }
10828 if meta.path.is_ident("unique") {
10829 out.unique = true;
10830 return Ok(());
10831 }
10832 if meta.path.is_ident("index") {
10833 out.index = true;
10834 // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
10835 if meta.input.peek(syn::token::Paren) {
10836 meta.parse_nested_meta(|inner| {
10837 if inner.path.is_ident("unique") {
10838 out.index_unique = true;
10839 return Ok(());
10840 }
10841 if inner.path.is_ident("name") {
10842 let s: LitStr = inner.value()?.parse()?;
10843 out.index_name = Some(s.value());
10844 return Ok(());
10845 }
10846 if inner.path.is_ident("method") {
10847 let s: LitStr = inner.value()?.parse()?;
10848 let v = s.value();
10849 match v.as_str() {
10850 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
10851 out.index_method = v;
10852 }
10853 other => {
10854 return Err(inner.error(format!(
10855 "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
10856 )));
10857 }
10858 }
10859 return Ok(());
10860 }
10861 Err(inner.error(
10862 "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
10863 ))
10864 })?;
10865 }
10866 return Ok(());
10867 }
10868 Err(meta.error("unknown rustango field attribute"))
10869 })?;
10870 }
10871 Ok(out)
10872}
10873
10874/// Parse a signed integer literal, accepting optional leading `-`.
10875fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
10876 let expr: syn::Expr = meta.value()?.parse()?;
10877 match expr {
10878 syn::Expr::Lit(syn::ExprLit {
10879 lit: syn::Lit::Int(lit),
10880 ..
10881 }) => lit.base10_parse::<i64>(),
10882 syn::Expr::Unary(syn::ExprUnary {
10883 op: syn::UnOp::Neg(_),
10884 expr,
10885 ..
10886 }) => {
10887 if let syn::Expr::Lit(syn::ExprLit {
10888 lit: syn::Lit::Int(lit),
10889 ..
10890 }) = *expr
10891 {
10892 let v: i64 = lit.base10_parse()?;
10893 Ok(-v)
10894 } else {
10895 Err(syn::Error::new_spanned(expr, "expected integer literal"))
10896 }
10897 }
10898 other => Err(syn::Error::new_spanned(
10899 other,
10900 "expected integer literal (signed)",
10901 )),
10902 }
10903}
10904
10905struct FieldInfo<'a> {
10906 ident: &'a syn::Ident,
10907 column: String,
10908 primary_key: bool,
10909 /// `true` when the Rust type was `Auto<T>` — the INSERT path will
10910 /// skip this column when `Auto::Unset` and emit it under
10911 /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
10912 auto: bool,
10913 /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
10914 /// the `Column::Value` associated type for typed-column tokens.
10915 value_ty: &'a Type,
10916 /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
10917 field_type_tokens: TokenStream2,
10918 schema: TokenStream2,
10919 from_row_init: TokenStream2,
10920 /// Variant of [`Self::from_row_init`] that reads the column via
10921 /// `format!("{prefix}__{col}")` so a model can be decoded out of
10922 /// the aliased columns of a JOINed row. Drives slice 9.0d's
10923 /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
10924 /// helper that `select_related` calls when stitching loaded FKs.
10925 from_aliased_row_init: TokenStream2,
10926 /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
10927 /// relation helper emit (`Author::<child>_set`) needs to know `T`
10928 /// to point the generated method at the right child model.
10929 fk_inner: Option<Type>,
10930 /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
10931 /// `kind` (since ForeignKey detection sets `kind` to K's
10932 /// underlying type) but stored separately for clarity at the
10933 /// `FkRelation` construction site, which only sees the FK's
10934 /// surface fields.
10935 fk_pk_kind: DetectedKind,
10936 /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
10937 /// the bare `ForeignKey<T, K>`. Routes the load_related and
10938 /// fk_pk_access emitters to wrap assignments / accessors in
10939 /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
10940 /// FK column compiles end-to-end. The DDL writer reads this off
10941 /// the field schema (`nullable` flag); the macro just needs to
10942 /// keep the Rust-side codegen consistent.
10943 nullable: bool,
10944 /// `true` when this column was marked `#[rustango(auto_now)]` —
10945 /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
10946 /// column instead of the user-supplied value, so `updated_at`
10947 /// always reflects the latest write without the caller having
10948 /// to remember to set it.
10949 auto_now: bool,
10950 /// `true` when this column was marked `#[rustango(auto_now_add)]`
10951 /// — the column is server-set on INSERT (DB DEFAULT) and
10952 /// **immutable** afterwards. `update_on` / `save_on` skip the
10953 /// column entirely so a stale `created_at` value in memory never
10954 /// rewrites the persisted timestamp.
10955 auto_now_add: bool,
10956 /// `true` when this column was marked `#[rustango(soft_delete)]`.
10957 /// Triggers emission of `soft_delete_on(executor)` and
10958 /// `restore_on(executor)` on the model's inherent impl. There is
10959 /// at most one such column per model — emission asserts this.
10960 soft_delete: bool,
10961 /// `Some` when this column was marked
10962 /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
10963 /// every INSERT and UPDATE path; the database recomputes the
10964 /// value from `EXPR`. Backlog item #35.
10965 generated_as: Option<String>,
10966 /// `true` when this column was marked
10967 /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
10968 /// emit an `insert_push` that auto-fills an `Auto::Unset` value
10969 /// with `Uuid::now_v7()` before binding, so the PK is generated
10970 /// Rust-side and the column is always present in the INSERT
10971 /// statement (no DB DEFAULT requirement). Issue #823.
10972 default_uuid_v7: bool,
10973 /// `Some` when this FK field carried `#[rustango(related_name =
10974 /// "...")]`. Threaded into [`FkRelation::related_name`] and
10975 /// consumed by [`reverse_helper_tokens`] to override the default
10976 /// `<child_snake>_set` accessor name. Follow-up to #816.
10977 related_name: Option<String>,
10978}
10979
10980/// Reject table names that won't survive SQL identifier
10981/// derivation downstream. Postgres' regular-identifier rule
10982/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
10983/// through the framework's unquoted FK / index / constraint name
10984/// emission without surprises. We also disallow leading-digit and
10985/// the empty string for clarity.
10986///
10987/// Reserved-word collisions (`select`, `from`, …) aren't flagged
10988/// here — those produce a runtime error from the SQL parser,
10989/// which is loud enough; statically enumerating reserved words
10990/// across the three supported dialects is more friction than help.
10991///
10992/// Backlog item #65.
10993fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
10994 validate_sql_identifier(name, "table", span)
10995}
10996
10997/// Reject SQL identifiers that compile but break downstream SQL
10998/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
10999/// `kind` is "table" / "column" — used for the error message so users
11000/// see which attribute caused the failure.
11001fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11002 if name.is_empty() {
11003 return Err(syn::Error::new(
11004 span,
11005 format!("`{kind} = \"\"` is not a valid SQL identifier"),
11006 ));
11007 }
11008 let mut chars = name.chars();
11009 let first = chars.next().unwrap();
11010 if !(first.is_ascii_alphabetic() || first == '_') {
11011 return Err(syn::Error::new(
11012 span,
11013 format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11014 ));
11015 }
11016 for c in chars {
11017 if !(c.is_ascii_alphanumeric() || c == '_') {
11018 return Err(syn::Error::new(
11019 span,
11020 format!(
11021 "{kind} name `{name}` contains invalid character {c:?} — \
11022 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11023 Hyphens in particular break FK / index name derivation \
11024 downstream; use underscores instead (e.g. `{}`)",
11025 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11026 ),
11027 ));
11028 }
11029 }
11030 Ok(())
11031}
11032
11033fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11034 let root = rustango_root();
11035 let attrs = parse_field_attrs(field)?;
11036 let ident = field
11037 .ident
11038 .as_ref()
11039 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11040 let name = ident.to_string();
11041 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11042 let primary_key = attrs.primary_key;
11043 let DetectedType {
11044 kind,
11045 nullable,
11046 auto: detected_auto,
11047 fk_inner,
11048 } = detect_type(&field.ty)?;
11049 check_bound_compatibility(field, &attrs, kind)?;
11050 let auto = detected_auto;
11051 // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11052 // INSERT path: the user must wrap the field in `Auto<T>`, which
11053 // marks the column as DB-default-supplied. The mixin attrs then
11054 // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11055 // for `auto_now`, force the value on UPDATE too.
11056 if attrs.auto_uuid {
11057 if kind != DetectedKind::Uuid {
11058 return Err(syn::Error::new_spanned(
11059 field,
11060 "`#[rustango(auto_uuid)]` requires the field type to be \
11061 `Auto<uuid::Uuid>`",
11062 ));
11063 }
11064 if !detected_auto {
11065 return Err(syn::Error::new_spanned(
11066 field,
11067 "`#[rustango(auto_uuid)]` requires the field type to be \
11068 wrapped in `Auto<...>` so the macro skips the column on \
11069 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11070 ));
11071 }
11072 }
11073 if attrs.default_uuid_v7 {
11074 if kind != DetectedKind::Uuid {
11075 return Err(syn::Error::new_spanned(
11076 field,
11077 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11078 `Auto<uuid::Uuid>`",
11079 ));
11080 }
11081 if !detected_auto {
11082 return Err(syn::Error::new_spanned(
11083 field,
11084 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11085 wrapped in `Auto<...>` so the macro can detect the \
11086 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11087 ));
11088 }
11089 if attrs.auto_uuid {
11090 return Err(syn::Error::new_spanned(
11091 field,
11092 "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11093 `#[rustango(auto_uuid)]` — the former generates the UUID \
11094 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11095 Pick one.",
11096 ));
11097 }
11098 }
11099 if attrs.auto_now_add || attrs.auto_now {
11100 if kind != DetectedKind::DateTime {
11101 return Err(syn::Error::new_spanned(
11102 field,
11103 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11104 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11105 ));
11106 }
11107 if !detected_auto {
11108 return Err(syn::Error::new_spanned(
11109 field,
11110 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11111 the field type to be wrapped in `Auto<...>` so the macro skips \
11112 the column on INSERT and the DB DEFAULT (`now()`) fires",
11113 ));
11114 }
11115 }
11116 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11117 return Err(syn::Error::new_spanned(
11118 field,
11119 "`#[rustango(soft_delete)]` requires the field type to be \
11120 `Option<chrono::DateTime<chrono::Utc>>`",
11121 ));
11122 }
11123 let is_mixin_auto =
11124 attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11125 if detected_auto && !primary_key && !is_mixin_auto {
11126 return Err(syn::Error::new_spanned(
11127 field,
11128 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11129 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11130 `auto_now`",
11131 ));
11132 }
11133 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11134 return Err(syn::Error::new_spanned(
11135 field,
11136 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11137 SERIAL / BIGSERIAL already supplies a default sequence.",
11138 ));
11139 }
11140 if fk_inner.is_some() && primary_key {
11141 return Err(syn::Error::new_spanned(
11142 field,
11143 "`ForeignKey<T>` is not allowed on a primary-key field — \
11144 a row's PK is its own identity, not a reference to a parent.",
11145 ));
11146 }
11147 if attrs.generated_as.is_some() {
11148 if primary_key {
11149 return Err(syn::Error::new_spanned(
11150 field,
11151 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11152 primary-key field — a PK must be writable so the row \
11153 has an identity at INSERT time.",
11154 ));
11155 }
11156 if attrs.default.is_some() {
11157 return Err(syn::Error::new_spanned(
11158 field,
11159 "`#[rustango(generated_as = \"…\")]` cannot combine with \
11160 `default = \"…\"` — Postgres rejects DEFAULT on \
11161 generated columns. The expression IS the default.",
11162 ));
11163 }
11164 if detected_auto {
11165 return Err(syn::Error::new_spanned(
11166 field,
11167 "`#[rustango(generated_as = \"…\")]` is not allowed on \
11168 an `Auto<T>` field — generated columns are computed \
11169 by the DB, not server-assigned via a sequence. Use a \
11170 plain Rust type (e.g. `f64`).",
11171 ));
11172 }
11173 if fk_inner.is_some() {
11174 return Err(syn::Error::new_spanned(
11175 field,
11176 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11177 ForeignKey field.",
11178 ));
11179 }
11180 }
11181 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11182 let column_lit = column.as_str();
11183 // pgvector (#824): the `vector(dims = N)` attribute supplies the
11184 // dimension that the bare `Vector` Rust type can't carry, so emit
11185 // `FieldType::Vector(N)` here rather than the `variant_tokens`
11186 // fallback of `Vector(0)`.
11187 let field_type_tokens = if kind == DetectedKind::Vector {
11188 let root = rustango_root();
11189 let dims = attrs.vector_dims.unwrap_or(0);
11190 quote!(#root::core::FieldType::Vector(#dims))
11191 } else if kind == DetectedKind::Geometry {
11192 // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11193 // SRID that the bare `Point` Rust type can't carry, so emit
11194 // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11195 let root = rustango_root();
11196 let srid = attrs.geometry_srid.unwrap_or(0);
11197 quote!(#root::core::FieldType::Geometry(#srid))
11198 } else {
11199 kind.variant_tokens()
11200 };
11201 let max_length = optional_u32(attrs.max_length);
11202 let min = optional_i64(attrs.min);
11203 let max = optional_i64(attrs.max);
11204 let default = optional_str(attrs.default.as_deref());
11205
11206 let unique = attrs.unique;
11207 let generated_as = optional_str(attrs.generated_as.as_deref());
11208 let help_text = optional_str(attrs.help_text.as_deref());
11209 let choices = optional_choices(attrs.choices.as_deref());
11210 let db_comment = optional_str(attrs.db_comment.as_deref());
11211 let verbose_name = optional_str(attrs.verbose_name.as_deref());
11212 let editable = attrs.editable;
11213 let blank = attrs.blank;
11214 let case_insensitive = attrs.case_insensitive;
11215 let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11216 if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() {
11217 return Err(syn::Error::new_spanned(
11218 field,
11219 "`#[rustango(on_delete = \"…\")]` requires either `fk = \"<table>\"` \
11220 or `o2o = \"<table>\"` on the same field — it has no meaning on a \
11221 non-FK column.",
11222 ));
11223 }
11224 let fk_on_delete = match attrs.on_delete.as_deref() {
11225 None => quote!(::core::option::Option::None),
11226 Some(action) => {
11227 let variant = match action {
11228 "cascade" => quote!(Cascade),
11229 "restrict" => quote!(Restrict),
11230 "set_null" => quote!(SetNull),
11231 "set_default" => quote!(SetDefault),
11232 "no_action" => quote!(NoAction),
11233 // parse_field_attrs already validated this — guard against future drift.
11234 other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11235 };
11236 quote!(::core::option::Option::Some(
11237 #root::core::OnDeleteAction::#variant
11238 ))
11239 }
11240 };
11241 let schema = quote! {
11242 #root::core::FieldSchema {
11243 name: #name,
11244 column: #column_lit,
11245 ty: #field_type_tokens,
11246 nullable: #nullable,
11247 primary_key: #primary_key,
11248 relation: #relation,
11249 max_length: #max_length,
11250 min: #min,
11251 max: #max,
11252 default: #default,
11253 auto: #auto,
11254 unique: #unique,
11255 generated_as: #generated_as,
11256 help_text: #help_text,
11257 choices: #choices,
11258 db_comment: #db_comment,
11259 verbose_name: #verbose_name,
11260 editable: #editable,
11261 blank: #blank,
11262 case_insensitive: #case_insensitive,
11263 fk_on_delete: #fk_on_delete,
11264 validators: &[ #(#validators_lits),* ],
11265 }
11266 };
11267
11268 let from_row_init = quote! {
11269 #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11270 };
11271 let from_aliased_row_init = quote! {
11272 #ident: #root::sql::sqlx::Row::try_get(
11273 row,
11274 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11275 )?
11276 };
11277
11278 Ok(FieldInfo {
11279 ident,
11280 column,
11281 primary_key,
11282 auto,
11283 value_ty: &field.ty,
11284 field_type_tokens,
11285 schema,
11286 from_row_init,
11287 from_aliased_row_init,
11288 fk_inner: fk_inner.cloned(),
11289 fk_pk_kind: kind,
11290 nullable,
11291 auto_now: attrs.auto_now,
11292 auto_now_add: attrs.auto_now_add,
11293 soft_delete: attrs.soft_delete,
11294 generated_as: attrs.generated_as.clone(),
11295 default_uuid_v7: attrs.default_uuid_v7,
11296 related_name: attrs.related_name.clone(),
11297 })
11298}
11299
11300fn check_bound_compatibility(
11301 field: &syn::Field,
11302 attrs: &FieldAttrs,
11303 kind: DetectedKind,
11304) -> syn::Result<()> {
11305 if attrs.max_length.is_some() && kind != DetectedKind::String {
11306 return Err(syn::Error::new_spanned(
11307 field,
11308 "`max_length` is only valid on `String` fields (or `Option<String>`)",
11309 ));
11310 }
11311 if attrs.choices.is_some() && kind != DetectedKind::String {
11312 return Err(syn::Error::new_spanned(
11313 field,
11314 "`choices` is only valid on `String` fields (or `Option<String>`) — \
11315 integer-valued enumerations should be modeled with a Rust enum and \
11316 custom (de)serializer for now",
11317 ));
11318 }
11319 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11320 return Err(syn::Error::new_spanned(
11321 field,
11322 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11323 ));
11324 }
11325 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11326 if min > max {
11327 return Err(syn::Error::new_spanned(
11328 field,
11329 format!("`min` ({min}) is greater than `max` ({max})"),
11330 ));
11331 }
11332 }
11333 Ok(())
11334}
11335
11336fn optional_u32(value: Option<u32>) -> TokenStream2 {
11337 if let Some(v) = value {
11338 quote!(::core::option::Option::Some(#v))
11339 } else {
11340 quote!(::core::option::Option::None)
11341 }
11342}
11343
11344fn optional_i64(value: Option<i64>) -> TokenStream2 {
11345 if let Some(v) = value {
11346 quote!(::core::option::Option::Some(#v))
11347 } else {
11348 quote!(::core::option::Option::None)
11349 }
11350}
11351
11352fn optional_str(value: Option<&str>) -> TokenStream2 {
11353 if let Some(v) = value {
11354 quote!(::core::option::Option::Some(#v))
11355 } else {
11356 quote!(::core::option::Option::None)
11357 }
11358}
11359
11360fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11361 let Some(pairs) = pairs else {
11362 return quote!(::core::option::Option::None);
11363 };
11364 let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11365 quote!(::core::option::Option::Some(&[#(#entries),*]))
11366}
11367
11368fn relation_tokens(
11369 field: &syn::Field,
11370 attrs: &FieldAttrs,
11371 fk_inner: Option<&syn::Type>,
11372 table: &str,
11373) -> syn::Result<TokenStream2> {
11374 let root = rustango_root();
11375 if let Some(inner) = fk_inner {
11376 if attrs.fk.is_some() || attrs.o2o.is_some() {
11377 return Err(syn::Error::new_spanned(
11378 field,
11379 "`ForeignKey<T>` already declares the FK target via the type parameter — \
11380 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11381 ));
11382 }
11383 let on = attrs.on.as_deref().unwrap_or("id");
11384 return Ok(quote! {
11385 ::core::option::Option::Some(#root::core::Relation::Fk {
11386 to: <#inner as #root::core::Model>::SCHEMA.table,
11387 on: #on,
11388 })
11389 });
11390 }
11391 match (&attrs.fk, &attrs.o2o) {
11392 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11393 field,
11394 "`fk` and `o2o` are mutually exclusive",
11395 )),
11396 (Some(to), None) => {
11397 let on = attrs.on.as_deref().unwrap_or("id");
11398 // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11399 // the model's own table. Threaded as a literal string at
11400 // macro-expansion time to sidestep the const-eval cycle
11401 // that `Self::SCHEMA.table` would create when referenced
11402 // inside Self::SCHEMA's own initializer.
11403 let resolved = if to == "self" { table } else { to };
11404 Ok(quote! {
11405 ::core::option::Option::Some(#root::core::Relation::Fk { to: #resolved, on: #on })
11406 })
11407 }
11408 (None, Some(to)) => {
11409 let on = attrs.on.as_deref().unwrap_or("id");
11410 let resolved = if to == "self" { table } else { to };
11411 Ok(quote! {
11412 ::core::option::Option::Some(#root::core::Relation::O2O { to: #resolved, on: #on })
11413 })
11414 }
11415 (None, None) => {
11416 if attrs.on.is_some() {
11417 return Err(syn::Error::new_spanned(
11418 field,
11419 "`on` requires `fk` or `o2o`",
11420 ));
11421 }
11422 Ok(quote!(::core::option::Option::None))
11423 }
11424 }
11425}
11426
11427/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11428/// about kinds without depending on `rustango-core` (which would require a
11429/// proc-macro/normal split it doesn't have today).
11430#[derive(Clone, Copy, PartialEq, Eq)]
11431enum DetectedKind {
11432 I16,
11433 I32,
11434 I64,
11435 F32,
11436 F64,
11437 Bool,
11438 String,
11439 DateTime,
11440 Date,
11441 Time,
11442 Uuid,
11443 Json,
11444 Decimal,
11445 Binary,
11446 /// `Array<String>` → PG `text[]` (#341).
11447 ArrayText,
11448 /// `Array<i32>` → PG `integer[]` (#341).
11449 ArrayInt,
11450 /// `Array<i64>` → PG `bigint[]` (#341).
11451 ArrayBigInt,
11452 /// `Range<i32>` → PG `int4range` (#343).
11453 RangeInt,
11454 /// `Range<i64>` → PG `int8range` (#343).
11455 RangeBigInt,
11456 /// `Range<Decimal>` → PG `numrange` (#343).
11457 RangeNumeric,
11458 /// `Range<NaiveDate>` → PG `daterange` (#343).
11459 RangeDate,
11460 /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11461 RangeDateTime,
11462 /// `HStore` → PG `hstore` (#342).
11463 HStore,
11464 /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11465 /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11466 /// in at the `FieldType` emission site (not carried on this enum).
11467 Vector,
11468 /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11469 /// from the `#[rustango(geometry(srid = N))]` field attribute,
11470 /// threaded in at the `FieldType` emission site.
11471 Geometry,
11472}
11473
11474impl DetectedKind {
11475 fn variant_tokens(self) -> TokenStream2 {
11476 let root = rustango_root();
11477 match self {
11478 Self::I16 => quote!(#root::core::FieldType::I16),
11479 Self::I32 => quote!(#root::core::FieldType::I32),
11480 Self::I64 => quote!(#root::core::FieldType::I64),
11481 Self::F32 => quote!(#root::core::FieldType::F32),
11482 Self::F64 => quote!(#root::core::FieldType::F64),
11483 Self::Bool => quote!(#root::core::FieldType::Bool),
11484 Self::String => quote!(#root::core::FieldType::String),
11485 Self::DateTime => quote!(#root::core::FieldType::DateTime),
11486 Self::Date => quote!(#root::core::FieldType::Date),
11487 Self::Time => quote!(#root::core::FieldType::Time),
11488 Self::Uuid => quote!(#root::core::FieldType::Uuid),
11489 Self::Json => quote!(#root::core::FieldType::Json),
11490 Self::Decimal => quote!(#root::core::FieldType::Decimal),
11491 Self::Binary => quote!(#root::core::FieldType::Binary),
11492 Self::ArrayText => {
11493 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11494 }
11495 Self::ArrayInt => {
11496 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11497 }
11498 Self::ArrayBigInt => {
11499 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11500 }
11501 Self::RangeInt => {
11502 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11503 }
11504 Self::RangeBigInt => {
11505 quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11506 }
11507 Self::RangeNumeric => {
11508 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11509 }
11510 Self::RangeDate => {
11511 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11512 }
11513 Self::RangeDateTime => {
11514 quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11515 }
11516 Self::HStore => quote!(#root::core::FieldType::HStore),
11517 // Dimension comes from the `vector(dims = N)` attribute,
11518 // applied at the emission site; `0` here is just a fallback.
11519 Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11520 // SRID comes from the `geometry(srid = N)` attribute, applied
11521 // at the emission site; `0` here is just a fallback.
11522 Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11523 }
11524 }
11525
11526 fn is_integer(self) -> bool {
11527 matches!(self, Self::I16 | Self::I32 | Self::I64)
11528 }
11529
11530 /// `(SqlValue::<Variant>, default expr)` for emitting the
11531 /// `match SqlValue { … }` arm in `LoadRelated::__rustango_load_related`
11532 /// for a `ForeignKey<T, K>` FK whose K maps to `self`. The default
11533 /// fires only when the parent's `__rustango_pk_value` returns a
11534 /// different variant than expected, which is a compile-time bug —
11535 /// but we still need a value-typed fallback to keep the match
11536 /// total.
11537 fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
11538 let root = rustango_root();
11539 match self {
11540 Self::I16 => (quote!(I16), quote!(0i16)),
11541 Self::I32 => (quote!(I32), quote!(0i32)),
11542 Self::I64 => (quote!(I64), quote!(0i64)),
11543 Self::F32 => (quote!(F32), quote!(0f32)),
11544 Self::F64 => (quote!(F64), quote!(0f64)),
11545 Self::Bool => (quote!(Bool), quote!(false)),
11546 Self::String => (quote!(String), quote!(::std::string::String::new())),
11547 Self::DateTime => (
11548 quote!(DateTime),
11549 quote!(<#root::__chrono::DateTime<#root::__chrono::Utc> as ::std::default::Default>::default()),
11550 ),
11551 Self::Date => (
11552 quote!(Date),
11553 quote!(<#root::__chrono::NaiveDate as ::std::default::Default>::default()),
11554 ),
11555 Self::Time => (
11556 quote!(Time),
11557 quote!(<#root::__chrono::NaiveTime as ::std::default::Default>::default()),
11558 ),
11559 Self::Uuid => (quote!(Uuid), quote!(#root::__uuid::Uuid::nil())),
11560 Self::Json => (quote!(Json), quote!(#root::__serde_json::Value::Null)),
11561 Self::Decimal => (
11562 quote!(Decimal),
11563 quote!(<#root::__rust_decimal::Decimal as ::std::default::Default>::default()),
11564 ),
11565 Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
11566 // Arrays (#341) can never be a foreign-key primary key, so
11567 // this arm is never reached at runtime — but the match must
11568 // stay total. A bare empty `SqlValue::Array` is the dummy.
11569 Self::ArrayText | Self::ArrayInt | Self::ArrayBigInt => {
11570 (quote!(Array), quote!(::std::vec::Vec::new()))
11571 }
11572 // Ranges (#343) likewise can't be a FK PK — never reached.
11573 Self::RangeInt
11574 | Self::RangeBigInt
11575 | Self::RangeNumeric
11576 | Self::RangeDate
11577 | Self::RangeDateTime => (quote!(RangeLiteral), quote!(::std::string::String::new())),
11578 // HStore (#342) can't be a FK PK — never reached.
11579 Self::HStore => (quote!(HStore), quote!(::std::vec::Vec::new())),
11580 // Vector (#824) can't be a FK PK — never reached.
11581 Self::Vector => (quote!(Vector), quote!(::std::vec::Vec::new())),
11582 // Geometry (#443) can't be a FK PK — never reached (arm is
11583 // exhaustiveness-only; never interpolated into emitted code).
11584 Self::Geometry => (quote!(Geometry), quote!(::std::vec::Vec::new())),
11585 }
11586 }
11587}
11588
11589/// Result of walking a field's Rust type. `kind` is the underlying
11590/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11591/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11592/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11593/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11594#[derive(Clone, Copy)]
11595struct DetectedType<'a> {
11596 kind: DetectedKind,
11597 nullable: bool,
11598 auto: bool,
11599 fk_inner: Option<&'a syn::Type>,
11600}
11601
11602/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11603/// non-`Auto` types — the caller should already have routed Auto-only
11604/// codegen through this helper, so a `None` indicates a macro-internal
11605/// invariant break.
11606fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11607 let Type::Path(TypePath { path, qself: None }) = ty else {
11608 return None;
11609 };
11610 let last = path.segments.last()?;
11611 if last.ident != "Auto" {
11612 return None;
11613 }
11614 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11615 return None;
11616 };
11617 args.args.iter().find_map(|a| match a {
11618 syn::GenericArgument::Type(t) => Some(t),
11619 _ => None,
11620 })
11621}
11622
11623fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11624 let Type::Path(TypePath { path, qself: None }) = ty else {
11625 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11626 };
11627 let last = path
11628 .segments
11629 .last()
11630 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11631
11632 if last.ident == "Option" {
11633 let inner = generic_inner(ty, &last.arguments, "Option")?;
11634 let inner_det = detect_type(inner)?;
11635 if inner_det.nullable {
11636 return Err(syn::Error::new_spanned(
11637 ty,
11638 "nested Option is not supported",
11639 ));
11640 }
11641 if inner_det.auto {
11642 return Err(syn::Error::new_spanned(
11643 ty,
11644 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11645 ));
11646 }
11647 return Ok(DetectedType {
11648 nullable: true,
11649 ..inner_det
11650 });
11651 }
11652
11653 if last.ident == "Auto" {
11654 let inner = generic_inner(ty, &last.arguments, "Auto")?;
11655 let inner_det = detect_type(inner)?;
11656 if inner_det.auto {
11657 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11658 }
11659 if inner_det.nullable {
11660 return Err(syn::Error::new_spanned(
11661 ty,
11662 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11663 ));
11664 }
11665 if inner_det.fk_inner.is_some() {
11666 return Err(syn::Error::new_spanned(
11667 ty,
11668 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11669 ));
11670 }
11671 if !matches!(
11672 inner_det.kind,
11673 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11674 ) {
11675 return Err(syn::Error::new_spanned(
11676 ty,
11677 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11678 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11679 (DEFAULT now())",
11680 ));
11681 }
11682 return Ok(DetectedType {
11683 auto: true,
11684 ..inner_det
11685 });
11686 }
11687
11688 if last.ident == "ForeignKey" {
11689 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11690 // Resolve the FK column's underlying SQL type from `K`. When the
11691 // user wrote `ForeignKey<T>` without a key parameter, the type
11692 // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11693 // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11694 // recurse into K so the column DDL emits the right SQL type
11695 // (VARCHAR for String, UUID for Uuid, …) and the load_related
11696 // emitter knows which `SqlValue` variant to match.
11697 let kind = match key_ty {
11698 Some(k) => detect_type(k)?.kind,
11699 None => DetectedKind::I64,
11700 };
11701 return Ok(DetectedType {
11702 kind,
11703 nullable: false,
11704 auto: false,
11705 fk_inner: Some(inner),
11706 });
11707 }
11708
11709 let kind = match last.ident.to_string().as_str() {
11710 "i16" => DetectedKind::I16,
11711 "i32" => DetectedKind::I32,
11712 "i64" => DetectedKind::I64,
11713 "f32" => DetectedKind::F32,
11714 "f64" => DetectedKind::F64,
11715 "bool" => DetectedKind::Bool,
11716 "String" => DetectedKind::String,
11717 "DateTime" => DetectedKind::DateTime,
11718 "NaiveDate" => DetectedKind::Date,
11719 "NaiveTime" => DetectedKind::Time,
11720 "Uuid" => DetectedKind::Uuid,
11721 "Value" => DetectedKind::Json,
11722 "Decimal" => DetectedKind::Decimal,
11723 // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11724 // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11725 // — that would emit Binary DDL and decode-fail at runtime.
11726 "Vec" => {
11727 let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11728 if let Type::Path(TypePath { path, qself: None }) = inner {
11729 if let Some(seg) = path.segments.last() {
11730 if seg.ident == "u8" && seg.arguments.is_empty() {
11731 return Ok(DetectedType {
11732 kind: DetectedKind::Binary,
11733 nullable: false,
11734 auto: false,
11735 fk_inner: None,
11736 });
11737 }
11738 }
11739 }
11740 return Err(syn::Error::new_spanned(
11741 ty,
11742 "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11743 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11744 ));
11745 }
11746 // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11747 // `integer[]` / `bigint[]` (Django `ArrayField`, #341).
11748 "Array" => {
11749 let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11750 let elem = match inner {
11751 Type::Path(TypePath { path, qself: None }) => {
11752 path.segments.last().map(|s| s.ident.to_string())
11753 }
11754 _ => None,
11755 };
11756 let kind = match elem.as_deref() {
11757 Some("String") => DetectedKind::ArrayText,
11758 Some("i32") => DetectedKind::ArrayInt,
11759 Some("i64") => DetectedKind::ArrayBigInt,
11760 _ => {
11761 return Err(syn::Error::new_spanned(
11762 ty,
11763 "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
11764 `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
11765 ));
11766 }
11767 };
11768 return Ok(DetectedType {
11769 kind,
11770 nullable: false,
11771 auto: false,
11772 fk_inner: None,
11773 });
11774 }
11775 // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
11776 // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
11777 // `int8range` / `numrange` / `daterange` / `tstzrange` (Django
11778 // `RangeField` family, #343).
11779 "Range" => {
11780 let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
11781 let elem = match inner {
11782 Type::Path(TypePath { path, qself: None }) => {
11783 path.segments.last().map(|s| s.ident.to_string())
11784 }
11785 _ => None,
11786 };
11787 let kind = match elem.as_deref() {
11788 Some("i32") => DetectedKind::RangeInt,
11789 Some("i64") => DetectedKind::RangeBigInt,
11790 Some("Decimal") => DetectedKind::RangeNumeric,
11791 Some("NaiveDate") => DetectedKind::RangeDate,
11792 Some("DateTime") => DetectedKind::RangeDateTime,
11793 _ => {
11794 return Err(syn::Error::new_spanned(
11795 ty,
11796 "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
11797 `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
11798 `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
11799 (→ tstzrange) are supported (#343)",
11800 ));
11801 }
11802 };
11803 return Ok(DetectedType {
11804 kind,
11805 nullable: false,
11806 auto: false,
11807 fk_inner: None,
11808 });
11809 }
11810 // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
11811 // (the `CastValue` impl bridges logical↔stored); the field's own
11812 // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
11813 // schema just needs `FieldType::String`.
11814 "Cast" => {
11815 return Ok(DetectedType {
11816 kind: DetectedKind::String,
11817 nullable: false,
11818 auto: false,
11819 fk_inner: None,
11820 });
11821 }
11822 // `HStore` → PG `hstore` (Django `HStoreField`, #342). No generic
11823 // parameter — always a string→string map.
11824 "HStore" => {
11825 return Ok(DetectedType {
11826 kind: DetectedKind::HStore,
11827 nullable: false,
11828 auto: false,
11829 fk_inner: None,
11830 });
11831 }
11832 // `Vector` → pgvector `vector(N)` (#824). The dimension is
11833 // supplied by `#[rustango(vector(dims = N))]`, not the type.
11834 "Vector" => {
11835 return Ok(DetectedType {
11836 kind: DetectedKind::Vector,
11837 nullable: false,
11838 auto: false,
11839 fk_inner: None,
11840 });
11841 }
11842 // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
11843 // supplied by `#[rustango(geometry(srid = N))]`, not the type.
11844 "Point" => {
11845 return Ok(DetectedType {
11846 kind: DetectedKind::Geometry,
11847 nullable: false,
11848 auto: false,
11849 fk_inner: None,
11850 });
11851 }
11852 other => {
11853 return Err(syn::Error::new_spanned(
11854 ty,
11855 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)"),
11856 ));
11857 }
11858 };
11859 Ok(DetectedType {
11860 kind,
11861 nullable: false,
11862 auto: false,
11863 fk_inner: None,
11864 })
11865}
11866
11867fn generic_inner<'a>(
11868 ty: &'a Type,
11869 arguments: &'a PathArguments,
11870 wrapper: &str,
11871) -> syn::Result<&'a Type> {
11872 let PathArguments::AngleBracketed(args) = arguments else {
11873 return Err(syn::Error::new_spanned(
11874 ty,
11875 format!("{wrapper} requires a generic argument"),
11876 ));
11877 };
11878 args.args
11879 .iter()
11880 .find_map(|a| match a {
11881 GenericArgument::Type(t) => Some(t),
11882 _ => None,
11883 })
11884 .ok_or_else(|| {
11885 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11886 })
11887}
11888
11889/// Like [`generic_inner`] but pulls *two* type args — the first is
11890/// required, the second is optional. Used by the `ForeignKey<T, K>`
11891/// detection where K defaults to `i64` when omitted.
11892fn generic_pair<'a>(
11893 ty: &'a Type,
11894 arguments: &'a PathArguments,
11895 wrapper: &str,
11896) -> syn::Result<(&'a Type, Option<&'a Type>)> {
11897 let PathArguments::AngleBracketed(args) = arguments else {
11898 return Err(syn::Error::new_spanned(
11899 ty,
11900 format!("{wrapper} requires a generic argument"),
11901 ));
11902 };
11903 let mut types = args.args.iter().filter_map(|a| match a {
11904 GenericArgument::Type(t) => Some(t),
11905 _ => None,
11906 });
11907 let first = types.next().ok_or_else(|| {
11908 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11909 })?;
11910 let second = types.next();
11911 Ok((first, second))
11912}
11913
11914fn to_snake_case(s: &str) -> String {
11915 let mut out = String::with_capacity(s.len() + 4);
11916 for (i, ch) in s.chars().enumerate() {
11917 if ch.is_ascii_uppercase() {
11918 if i > 0 {
11919 out.push('_');
11920 }
11921 out.push(ch.to_ascii_lowercase());
11922 } else {
11923 out.push(ch);
11924 }
11925 }
11926 out
11927}
11928
11929// ============================================================
11930// #[derive(Form)] — slice 8.4B
11931// ============================================================
11932
11933/// Per-field `#[form(...)]` attributes recognised by the derive.
11934#[derive(Default)]
11935struct FormFieldAttrs {
11936 min: Option<i64>,
11937 max: Option<i64>,
11938 min_length: Option<u32>,
11939 max_length: Option<u32>,
11940 /// `#[form(clean = "fn_name")]` — Django-shape `clean_<field>` hook.
11941 /// The named static method on the form struct is called after the
11942 /// field's typed parse + length/range checks; it gets the parsed
11943 /// value by reference and returns `Result<<FieldType>, String>`.
11944 /// On Ok, the returned value replaces the parsed one; on Err, the
11945 /// message is attached to the field error list. Issue #372.
11946 clean: Option<syn::Ident>,
11947}
11948
11949/// Container-level `#[form(...)]` attributes. Currently only the
11950/// Django-shape cross-field `validate` hook (issue #373).
11951#[derive(Default)]
11952struct FormContainerAttrs {
11953 /// `#[form(validate = "fn_name")]` — Django-shape `clean()` hook.
11954 /// After every per-field parse succeeds, the named method on the
11955 /// form struct is called with `&self` and may return
11956 /// `Result<(), FormErrors>`. Errors merge into the field error
11957 /// list. Issue #373.
11958 validate: Option<syn::Ident>,
11959}
11960
11961/// Detected shape of a form field's Rust type.
11962#[derive(Clone, Copy)]
11963enum FormFieldKind {
11964 String,
11965 I16,
11966 I32,
11967 I64,
11968 F32,
11969 F64,
11970 Bool,
11971}
11972
11973impl FormFieldKind {
11974 fn parse_method(self) -> &'static str {
11975 match self {
11976 Self::I16 => "i16",
11977 Self::I32 => "i32",
11978 Self::I64 => "i64",
11979 Self::F32 => "f32",
11980 Self::F64 => "f64",
11981 // String + Bool don't go through `str::parse`; the codegen
11982 // handles them inline.
11983 Self::String | Self::Bool => "",
11984 }
11985 }
11986}
11987
11988fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
11989 let root = rustango_root();
11990 let struct_name = &input.ident;
11991
11992 let Data::Struct(data) = &input.data else {
11993 return Err(syn::Error::new_spanned(
11994 struct_name,
11995 "Form can only be derived on structs",
11996 ));
11997 };
11998 let Fields::Named(named) = &data.fields else {
11999 return Err(syn::Error::new_spanned(
12000 struct_name,
12001 "Form requires a struct with named fields",
12002 ));
12003 };
12004
12005 // #373 — container-level `#[form(validate = "fn")]` hook.
12006 let container = parse_form_container_attrs(input)?;
12007 let post_field_clean: Vec<TokenStream2> = Vec::new();
12008 let _ = post_field_clean;
12009
12010 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12011 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12012
12013 for field in &named.named {
12014 let ident = field
12015 .ident
12016 .as_ref()
12017 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12018 let attrs = parse_form_field_attrs(field)?;
12019 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12020
12021 let name_lit = ident.to_string();
12022 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12023 // #372 — append the per-field `clean_<field>` call right after
12024 // the parse block when the attribute is set. The clean fn
12025 // takes &T and returns Result<T, String>; on Err we attach
12026 // the message to the field error list without aborting
12027 // (matches Django's "collect all field errors" shape).
12028 let clean_block = if let Some(clean_fn) = &attrs.clean {
12029 quote! {
12030 if __errors.fields().get(#name_lit).is_none() {
12031 match Self::#clean_fn(&#ident) {
12032 ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
12033 ::core::result::Result::Err(__msg) => {
12034 __errors.add(#name_lit, __msg);
12035 }
12036 }
12037 }
12038 }
12039 } else {
12040 quote! {}
12041 };
12042 field_blocks.push(quote! {
12043 #parse_block
12044 #clean_block
12045 });
12046 field_idents.push(ident);
12047 }
12048
12049 // #373 — after every per-field parse + clean succeeds, call the
12050 // cross-field validator if declared. Errors merge into the
12051 // outgoing FormErrors via the existing `FormErrors::merge` helper
12052 // (same primitive the DRF serializer cross-field hook uses).
12053 let cross_field_call = if let Some(validate_fn) = &container.validate {
12054 quote! {
12055 if __errors.is_empty() {
12056 let __candidate = Self { #( #field_idents ),* };
12057 if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12058 __errors.merge(__other);
12059 }
12060 if !__errors.is_empty() {
12061 return ::core::result::Result::Err(__errors);
12062 }
12063 return ::core::result::Result::Ok(__candidate);
12064 }
12065 }
12066 } else {
12067 quote! {}
12068 };
12069
12070 Ok(quote! {
12071 impl #root::forms::Form for #struct_name {
12072 fn parse(
12073 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12074 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12075 let mut __errors = #root::forms::FormErrors::default();
12076 #( #field_blocks )*
12077 #cross_field_call
12078 if !__errors.is_empty() {
12079 return ::core::result::Result::Err(__errors);
12080 }
12081 ::core::result::Result::Ok(Self {
12082 #( #field_idents ),*
12083 })
12084 }
12085 }
12086 })
12087}
12088
12089fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12090 let mut out = FormContainerAttrs::default();
12091 for attr in &input.attrs {
12092 if !attr.path().is_ident("form") {
12093 continue;
12094 }
12095 attr.parse_nested_meta(|meta| {
12096 if meta.path.is_ident("validate") {
12097 let s: LitStr = meta.value()?.parse()?;
12098 out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12099 return Ok(());
12100 }
12101 Err(meta.error("unknown form container attribute (supported: `validate`)"))
12102 })?;
12103 }
12104 Ok(out)
12105}
12106
12107fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12108 let mut out = FormFieldAttrs::default();
12109 for attr in &field.attrs {
12110 if !attr.path().is_ident("form") {
12111 continue;
12112 }
12113 attr.parse_nested_meta(|meta| {
12114 if meta.path.is_ident("min") {
12115 let lit: syn::LitInt = meta.value()?.parse()?;
12116 out.min = Some(lit.base10_parse::<i64>()?);
12117 return Ok(());
12118 }
12119 if meta.path.is_ident("max") {
12120 let lit: syn::LitInt = meta.value()?.parse()?;
12121 out.max = Some(lit.base10_parse::<i64>()?);
12122 return Ok(());
12123 }
12124 if meta.path.is_ident("min_length") {
12125 let lit: syn::LitInt = meta.value()?.parse()?;
12126 out.min_length = Some(lit.base10_parse::<u32>()?);
12127 return Ok(());
12128 }
12129 if meta.path.is_ident("max_length") {
12130 let lit: syn::LitInt = meta.value()?.parse()?;
12131 out.max_length = Some(lit.base10_parse::<u32>()?);
12132 return Ok(());
12133 }
12134 if meta.path.is_ident("clean") {
12135 let s: LitStr = meta.value()?.parse()?;
12136 out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12137 return Ok(());
12138 }
12139 Err(meta.error(
12140 "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12141 ))
12142 })?;
12143 }
12144 Ok(out)
12145}
12146
12147fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12148 let Type::Path(TypePath { path, qself: None }) = ty else {
12149 return Err(syn::Error::new(
12150 span,
12151 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12152 ));
12153 };
12154 let last = path
12155 .segments
12156 .last()
12157 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12158
12159 if last.ident == "Option" {
12160 let inner = generic_inner(ty, &last.arguments, "Option")?;
12161 let (kind, nested) = detect_form_field(inner, span)?;
12162 if nested {
12163 return Err(syn::Error::new(
12164 span,
12165 "nested Option in Form fields is not supported",
12166 ));
12167 }
12168 return Ok((kind, true));
12169 }
12170
12171 let kind = match last.ident.to_string().as_str() {
12172 "String" => FormFieldKind::String,
12173 "i16" => FormFieldKind::I16,
12174 "i32" => FormFieldKind::I32,
12175 "i64" => FormFieldKind::I64,
12176 "f32" => FormFieldKind::F32,
12177 "f64" => FormFieldKind::F64,
12178 "bool" => FormFieldKind::Bool,
12179 other => {
12180 return Err(syn::Error::new(
12181 span,
12182 format!(
12183 "Form field type `{other}` is not supported in v0.8 — use String / \
12184 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12185 ),
12186 ));
12187 }
12188 };
12189 Ok((kind, false))
12190}
12191
12192#[allow(clippy::too_many_lines)]
12193fn render_form_field_parse(
12194 ident: &syn::Ident,
12195 name_lit: &str,
12196 kind: FormFieldKind,
12197 nullable: bool,
12198 attrs: &FormFieldAttrs,
12199) -> TokenStream2 {
12200 // Pull the raw &str from the payload. Uses variable name `data` to
12201 // match the new `Form::parse(data: &HashMap<…>)` signature.
12202 let lookup = quote! {
12203 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12204 };
12205
12206 let parsed_value = match kind {
12207 FormFieldKind::Bool => quote! {
12208 let __v: bool = match __raw {
12209 ::core::option::Option::None => false,
12210 ::core::option::Option::Some(__s) => !matches!(
12211 __s.to_ascii_lowercase().as_str(),
12212 "" | "false" | "0" | "off" | "no"
12213 ),
12214 };
12215 },
12216 FormFieldKind::String => {
12217 if nullable {
12218 quote! {
12219 let __v: ::core::option::Option<::std::string::String> = match __raw {
12220 ::core::option::Option::None => ::core::option::Option::None,
12221 ::core::option::Option::Some(__s) if __s.is_empty() => {
12222 ::core::option::Option::None
12223 }
12224 ::core::option::Option::Some(__s) => {
12225 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12226 }
12227 };
12228 }
12229 } else {
12230 quote! {
12231 let __v: ::std::string::String = match __raw {
12232 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12233 ::core::clone::Clone::clone(__s)
12234 }
12235 _ => {
12236 __errors.add(#name_lit, "This field is required.");
12237 ::std::string::String::new()
12238 }
12239 };
12240 }
12241 }
12242 }
12243 FormFieldKind::I16
12244 | FormFieldKind::I32
12245 | FormFieldKind::I64
12246 | FormFieldKind::F32
12247 | FormFieldKind::F64 => {
12248 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12249 let ty_lit = kind.parse_method();
12250 let default_val = match kind {
12251 FormFieldKind::I16 => quote! { 0i16 },
12252 FormFieldKind::I32 => quote! { 0i32 },
12253 FormFieldKind::I64 => quote! { 0i64 },
12254 FormFieldKind::F32 => quote! { 0f32 },
12255 FormFieldKind::F64 => quote! { 0f64 },
12256 _ => quote! { Default::default() },
12257 };
12258 if nullable {
12259 quote! {
12260 let __v: ::core::option::Option<#parse_ty> = match __raw {
12261 ::core::option::Option::None => ::core::option::Option::None,
12262 ::core::option::Option::Some(__s) if __s.is_empty() => {
12263 ::core::option::Option::None
12264 }
12265 ::core::option::Option::Some(__s) => {
12266 match __s.parse::<#parse_ty>() {
12267 ::core::result::Result::Ok(__n) => {
12268 ::core::option::Option::Some(__n)
12269 }
12270 ::core::result::Result::Err(__e) => {
12271 __errors.add(
12272 #name_lit,
12273 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12274 );
12275 ::core::option::Option::None
12276 }
12277 }
12278 }
12279 };
12280 }
12281 } else {
12282 quote! {
12283 let __v: #parse_ty = match __raw {
12284 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12285 match __s.parse::<#parse_ty>() {
12286 ::core::result::Result::Ok(__n) => __n,
12287 ::core::result::Result::Err(__e) => {
12288 __errors.add(
12289 #name_lit,
12290 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12291 );
12292 #default_val
12293 }
12294 }
12295 }
12296 _ => {
12297 __errors.add(#name_lit, "This field is required.");
12298 #default_val
12299 }
12300 };
12301 }
12302 }
12303 }
12304 };
12305
12306 let validators = render_form_validators(name_lit, kind, nullable, attrs);
12307
12308 quote! {
12309 // `mut` so the per-field `clean` hook (#372) can rewrite the
12310 // parsed value in-place when it returns Ok with a normalized
12311 // form (e.g. trim / lowercase).
12312 let mut #ident = {
12313 #lookup
12314 #parsed_value
12315 #validators
12316 __v
12317 };
12318 }
12319}
12320
12321fn render_form_validators(
12322 name_lit: &str,
12323 kind: FormFieldKind,
12324 nullable: bool,
12325 attrs: &FormFieldAttrs,
12326) -> TokenStream2 {
12327 let mut checks: Vec<TokenStream2> = Vec::new();
12328
12329 let val_ref = if nullable {
12330 quote! { __v.as_ref() }
12331 } else {
12332 quote! { ::core::option::Option::Some(&__v) }
12333 };
12334
12335 let is_string = matches!(kind, FormFieldKind::String);
12336 let is_numeric = matches!(
12337 kind,
12338 FormFieldKind::I16
12339 | FormFieldKind::I32
12340 | FormFieldKind::I64
12341 | FormFieldKind::F32
12342 | FormFieldKind::F64
12343 );
12344
12345 if is_string {
12346 if let Some(min_len) = attrs.min_length {
12347 let min_len_usize = min_len as usize;
12348 checks.push(quote! {
12349 if let ::core::option::Option::Some(__s) = #val_ref {
12350 if __s.len() < #min_len_usize {
12351 __errors.add(
12352 #name_lit,
12353 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12354 );
12355 }
12356 }
12357 });
12358 }
12359 if let Some(max_len) = attrs.max_length {
12360 let max_len_usize = max_len as usize;
12361 checks.push(quote! {
12362 if let ::core::option::Option::Some(__s) = #val_ref {
12363 if __s.len() > #max_len_usize {
12364 __errors.add(
12365 #name_lit,
12366 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12367 );
12368 }
12369 }
12370 });
12371 }
12372 }
12373
12374 if is_numeric {
12375 if let Some(min) = attrs.min {
12376 checks.push(quote! {
12377 if let ::core::option::Option::Some(__n) = #val_ref {
12378 if (*__n as f64) < (#min as f64) {
12379 __errors.add(
12380 #name_lit,
12381 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12382 );
12383 }
12384 }
12385 });
12386 }
12387 if let Some(max) = attrs.max {
12388 checks.push(quote! {
12389 if let ::core::option::Option::Some(__n) = #val_ref {
12390 if (*__n as f64) > (#max as f64) {
12391 __errors.add(
12392 #name_lit,
12393 ::std::format!("Ensure this value is less than or equal to {}.", #max),
12394 );
12395 }
12396 }
12397 });
12398 }
12399 }
12400
12401 quote! { #( #checks )* }
12402}
12403
12404// ============================================================
12405// #[derive(ViewSet)]
12406// ============================================================
12407
12408struct ViewSetAttrs {
12409 model: syn::Path,
12410 fields: Option<Vec<String>>,
12411 filter_fields: Vec<String>,
12412 search_fields: Vec<String>,
12413 /// (field_name, desc)
12414 ordering: Vec<(String, bool)>,
12415 page_size: Option<usize>,
12416 read_only: bool,
12417 perms: ViewSetPermsAttrs,
12418 /// `#[viewset(serializer = SomeSerializer)]` — render list /
12419 /// retrieve / create responses through this `#[derive(Serializer)]`
12420 /// type instead of the default field-level projection (requires the
12421 /// `serializer` feature). Tri-dialect.
12422 serializer: Option<syn::Path>,
12423}
12424
12425#[derive(Default)]
12426struct ViewSetPermsAttrs {
12427 list: Vec<String>,
12428 retrieve: Vec<String>,
12429 create: Vec<String>,
12430 update: Vec<String>,
12431 destroy: Vec<String>,
12432}
12433
12434fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12435 let root = rustango_root();
12436 let struct_name = &input.ident;
12437
12438 // Must be a unit struct or an empty named struct.
12439 match &input.data {
12440 Data::Struct(s) => match &s.fields {
12441 Fields::Unit | Fields::Named(_) => {}
12442 Fields::Unnamed(_) => {
12443 return Err(syn::Error::new_spanned(
12444 struct_name,
12445 "ViewSet can only be derived on a unit struct or an empty named struct",
12446 ));
12447 }
12448 },
12449 _ => {
12450 return Err(syn::Error::new_spanned(
12451 struct_name,
12452 "ViewSet can only be derived on a struct",
12453 ));
12454 }
12455 }
12456
12457 let attrs = parse_viewset_attrs(input)?;
12458 let model_path = &attrs.model;
12459
12460 // `.fields(&[...])` call — None means skip (use all scalar fields).
12461 let fields_call = if let Some(ref fields) = attrs.fields {
12462 let lits = fields.iter().map(|f| f.as_str());
12463 quote!(.fields(&[ #(#lits),* ]))
12464 } else {
12465 quote!()
12466 };
12467
12468 let filter_fields_call = if attrs.filter_fields.is_empty() {
12469 quote!()
12470 } else {
12471 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12472 quote!(.filter_fields(&[ #(#lits),* ]))
12473 };
12474
12475 let search_fields_call = if attrs.search_fields.is_empty() {
12476 quote!()
12477 } else {
12478 let lits = attrs.search_fields.iter().map(|f| f.as_str());
12479 quote!(.search_fields(&[ #(#lits),* ]))
12480 };
12481
12482 let ordering_call = if attrs.ordering.is_empty() {
12483 quote!()
12484 } else {
12485 let pairs = attrs.ordering.iter().map(|(f, desc)| {
12486 let f = f.as_str();
12487 quote!((#f, #desc))
12488 });
12489 quote!(.ordering(&[ #(#pairs),* ]))
12490 };
12491
12492 let page_size_call = if let Some(n) = attrs.page_size {
12493 quote!(.page_size(#n))
12494 } else {
12495 quote!()
12496 };
12497
12498 let read_only_call = if attrs.read_only {
12499 quote!(.read_only())
12500 } else {
12501 quote!()
12502 };
12503
12504 // `.serializer::<S>()` — reshape responses through a derived
12505 // serializer. Requires the downstream crate to enable the
12506 // `serializer` feature (the method is gated on it).
12507 let serializer_call = if let Some(ref ser) = attrs.serializer {
12508 quote!(.serializer::<#ser>())
12509 } else {
12510 quote!()
12511 };
12512
12513 let perms = &attrs.perms;
12514 let perms_call = if perms.list.is_empty()
12515 && perms.retrieve.is_empty()
12516 && perms.create.is_empty()
12517 && perms.update.is_empty()
12518 && perms.destroy.is_empty()
12519 {
12520 quote!()
12521 } else {
12522 let list_lits = perms.list.iter().map(|s| s.as_str());
12523 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12524 let create_lits = perms.create.iter().map(|s| s.as_str());
12525 let update_lits = perms.update.iter().map(|s| s.as_str());
12526 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12527 quote! {
12528 .permissions(#root::viewset::ViewSetPerms {
12529 list: ::std::vec![ #(#list_lits.to_owned()),* ],
12530 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12531 create: ::std::vec![ #(#create_lits.to_owned()),* ],
12532 update: ::std::vec![ #(#update_lits.to_owned()),* ],
12533 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
12534 })
12535 }
12536 };
12537
12538 Ok(quote! {
12539 impl #struct_name {
12540 /// Build an `axum::Router` with the six standard REST endpoints
12541 /// for this ViewSet, mounted at `prefix`.
12542 ///
12543 /// Accepts any backend's pool — a `sqlx::PgPool`,
12544 /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
12545 /// enum — because they all `Into<Pool>` (#1273). This is why
12546 /// the derive no longer names `PgPool`, which would not
12547 /// exist on a project built without the `postgres` feature.
12548 pub fn router(
12549 prefix: &str,
12550 pool: impl ::core::convert::Into<#root::sql::Pool>,
12551 ) -> #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_pool(prefix, pool.into())
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}