rustango_macros/lib.rs
1//! Proc-macros for rustango.
2//!
3//! v0.1 ships `#[derive(Model)]`, which emits:
4//! * a `Model` impl carrying a static `ModelSchema`,
5//! * an `inventory::submit!` so the model is discoverable from the registry,
6//! * an inherent `objects()` returning a `QuerySet<Self>`,
7//! * a `sqlx::FromRow` impl so query results decode into the struct.
8
9use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::quote;
12use syn::{
13 parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields, GenericArgument, LitStr,
14 PathArguments, Type, TypePath,
15};
16
17/// Resolve the consumer's local name for the `rustango` crate so the
18/// macro-emitted code keeps compiling when a downstream `Cargo.toml`
19/// renames the dep (`[dependencies] orm = { package = "rustango", … }`)
20/// or when the standalone `rustango-orm` crate ships in a future
21/// slice of the [orm-extract epic](https://github.com/ujeenet/rustango/issues/149).
22///
23/// Returns one of:
24/// - `quote!(::rustango)` — the consumer IS the `rustango` crate
25/// itself. We emit the absolute path `::rustango` (NOT `crate`)
26/// because examples and integration tests inside the rustango
27/// package compile as separate binaries — their own `crate::`
28/// namespace is the example/test file, not rustango's lib root.
29/// The absolute `::rustango` resolves correctly in both contexts
30/// (rustango lib code AND rustango/examples/*.rs).
31/// - `quote!(::ident)` — the consumer renamed the dep; emit the
32/// user's chosen ident.
33/// - `quote!(::rustango)` — fallback when `proc-macro-crate` can't
34/// read the manifest (rare; preserves today's behavior).
35///
36/// Issue [#142](https://github.com/ujeenet/rustango/issues/142).
37fn rustango_root() -> TokenStream2 {
38 use proc_macro_crate::{crate_name, FoundCrate};
39 match crate_name("rustango") {
40 Ok(FoundCrate::Itself) => quote!(::rustango),
41 Ok(FoundCrate::Name(name)) => {
42 let ident = proc_macro2::Ident::new(&name, proc_macro2::Span::call_site());
43 quote!(::#ident)
44 }
45 Err(_) => quote!(::rustango),
46 }
47}
48
49/// Derive a `Model` impl. See crate docs for the supported attributes.
50#[proc_macro_derive(Model, attributes(rustango))]
51pub fn derive_model(input: TokenStream) -> TokenStream {
52 let input = parse_macro_input!(input as DeriveInput);
53 expand(&input)
54 .unwrap_or_else(syn::Error::into_compile_error)
55 .into()
56}
57
58/// Derive a `router(prefix, pool) -> axum::Router` associated method on a
59/// marker struct, wiring the full CRUD ViewSet in one annotation.
60///
61/// ```ignore
62/// #[derive(ViewSet)]
63/// #[viewset(
64/// model = Post,
65/// fields = "id, title, body, author_id",
66/// filter_fields = "author_id",
67/// search_fields = "title, body",
68/// ordering = "-published_at",
69/// page_size = 20,
70/// )]
71/// pub struct PostViewSet;
72///
73/// // Mount into your app:
74/// let app = Router::new()
75/// .merge(PostViewSet::router("/api/posts", pool.clone()));
76/// ```
77///
78/// Attributes:
79/// * `model = TypeName` — *required*. The `#[derive(Model)]` struct whose
80/// `SCHEMA` constant drives the endpoints.
81/// * `fields = "a, b, c"` — scalar fields included in list/retrieve JSON
82/// and accepted on create/update (default: all scalar fields).
83/// * `filter_fields = "a, b"` — fields filterable via `?a=v` query params.
84/// * `search_fields = "a, b"` — fields searched by `?search=...`.
85/// * `ordering = "a, -b"` — default list ordering; prefix `-` for DESC.
86/// * `page_size = N` — default page size (default: 20, max: 1000).
87/// * `read_only` — flag; wires only `list` + `retrieve` (no mutations).
88/// * `serializer = SomeSerializer` — render list / retrieve / create
89/// responses through a `#[derive(Serializer)]` type instead of the
90/// default field-level projection (`read_only` / `source` / `method`
91/// / `write_only` overrides apply). Tri-dialect. Requires the
92/// `serializer` feature.
93/// * `permissions(list = "...", retrieve = "...", create = "...",
94/// update = "...", destroy = "...")` — codenames required per action.
95#[proc_macro_derive(ViewSet, attributes(viewset))]
96pub fn derive_viewset(input: TokenStream) -> TokenStream {
97 let input = parse_macro_input!(input as DeriveInput);
98 expand_viewset(&input)
99 .unwrap_or_else(syn::Error::into_compile_error)
100 .into()
101}
102
103/// Derive `rustango::forms::Form` (slice 8.4B). Generates a
104/// `parse(&HashMap<String, String>) -> Result<Self, FormErrors>` impl
105/// that walks every named field and:
106///
107/// * Parses the string value into the field's Rust type (`String`,
108/// `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
109/// nullable case).
110/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
111/// `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
112/// validators in declaration order, returning `FormError::Parse`
113/// on the first failure.
114///
115/// Example:
116///
117/// ```ignore
118/// #[derive(Form)]
119/// pub struct CreateItemForm {
120/// #[form(min_length = 1, max_length = 64)]
121/// pub name: String,
122/// #[form(min = 0, max = 150)]
123/// pub age: i32,
124/// pub active: bool,
125/// pub email: Option<String>,
126/// }
127///
128/// let parsed = CreateItemForm::parse(&form_map)?;
129/// ```
130#[proc_macro_derive(Form, attributes(form))]
131pub fn derive_form(input: TokenStream) -> TokenStream {
132 let input = parse_macro_input!(input as DeriveInput);
133 expand_form(&input)
134 .unwrap_or_else(syn::Error::into_compile_error)
135 .into()
136}
137
138/// Derive `rustango::serializer::ModelSerializer` for a struct.
139/// (intra-doc link disabled — the macro crate doesn't depend on
140/// `rustango` itself, so rustdoc can't resolve the path.)
141///
142/// # Container attribute (required)
143/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
144///
145/// # Field attributes
146/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
147/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
148/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
149/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
150/// - `#[serializer(method = "fn_name")]` — DRF `SerializerMethodField`: calls `Self::fn_name(&model)` for the field value; excluded from `writable_fields()`
151/// - `#[serializer(nested)]` / `nested(strict)` — auto-resolves nested serializer from a loaded `ForeignKey`; excluded from `writable_fields()`
152/// - `#[serializer(many = ChildSerializer)]` — collection of nested serializers; populated via macro-emitted `set_<field>(&[Child::Model])`; excluded from `writable_fields()`
153/// - `#[serializer(slug = "name")]` — DRF `SlugRelatedField`: clones `model.<source>.value()?.name`; excluded from `writable_fields()` (v0.44)
154/// - `#[serializer(validate = "fn_name")]` — per-field validator surfaced by `Self::validate(&self)`
155/// - `#[serializer(max_length = N)]` / `min_length` / `min` / `max` — declarative bounds checked on
156/// write; auto-inherit from the model's `FieldSchema` (`max_length`/`min`/`max`/`choices`) when
157/// no attr is given, override it when given (`min_length` is serializer-only)
158///
159/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
160#[proc_macro_derive(Serializer, attributes(serializer))]
161pub fn derive_serializer(input: TokenStream) -> TokenStream {
162 let input = parse_macro_input!(input as DeriveInput);
163 expand_serializer(&input)
164 .unwrap_or_else(syn::Error::into_compile_error)
165 .into()
166}
167
168/// Bake every `*.json` migration file in a directory into the binary
169/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
170/// of `(name, json_content)` pairs, lex-sorted by file stem.
171///
172/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
173/// behaviour as `migrate(pool, dir)` but with no filesystem access.
174/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
175/// (i.e. the crate that invokes the macro). Default is
176/// `"./migrations"` if no argument is supplied.
177///
178/// ```ignore
179/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
180/// // or:
181/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
182///
183/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
184/// ```
185///
186/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
187/// file's `name` field must equal its file stem, every `prev`
188/// reference must point to another migration in the same directory,
189/// and the JSON must parse. A broken chain — orphan `prev`, missing
190/// predecessor, malformed file — fails at macro-expansion time with
191/// a clear `compile_error!`. *No other Django-shape Rust framework
192/// validates migration chains at compile time*: Cot's migrations are
193/// imperative Rust code (no static chain), Loco's are SeaORM
194/// up/down (same), Rwf's are raw SQL (no chain at all).
195///
196/// Each migration is included via `include_str!` so cargo's rebuild
197/// detection picks up file *content* changes. **Caveat:** cargo
198/// doesn't watch directory listings, so adding or removing a
199/// migration file inside the dir won't auto-trigger a rebuild — run
200/// `cargo clean` (or just bump any other source file) when you add
201/// new migrations during embedded development.
202#[proc_macro]
203pub fn embed_migrations(input: TokenStream) -> TokenStream {
204 expand_embed_migrations(input.into())
205 .unwrap_or_else(syn::Error::into_compile_error)
206 .into()
207}
208
209/// `Q!()` — Django-shape filter syntax compile-time-resolved against
210/// typed columns. Issue #269 / T1.7.
211///
212/// Each invocation lowers to the equivalent typed-column method call:
213///
214/// ```ignore
215/// // These expand identically:
216/// Q!(User.email__icontains = "alice")
217/// User::email.ilike("%alice%")
218/// ```
219///
220/// Field-name typos fail the build (the macro emits `User::no_such_field`
221/// which doesn't exist) — the headline ergonomic win of this slice over
222/// Django's stringly-typed `__lookup` filters.
223///
224/// # Supported lookup suffixes
225///
226/// * bare `=` / `__exact` → `.eq(value)`
227/// * `__iexact` → `.ilike(value)` (case-insensitive equality, no wildcards)
228/// * `__ne` → `.ne(value)`
229/// * `__gt` / `__gte` / `__lt` / `__lte` → corresponding comparison
230/// * `__contains` / `__icontains` → `.like("%v%")` / `.ilike("%v%")`
231/// * `__startswith` / `__istartswith` → `.like("v%")` / `.ilike("v%")`
232/// * `__endswith` / `__iendswith` → `.like("%v")` / `.ilike("%v")`
233/// * `__in` → `.is_in(iterable)`
234/// * `__not_in` → `.not_in(iterable)`
235/// * `__isnull = true` → `.is_null()`; `__isnull = false` → `.is_not_null()`
236/// * `__between` accepts a tuple literal `(lo, hi)` → `.between(lo, hi)`
237/// * `__regex` / `__iregex` → `.regex(pattern)` / `.iregex(pattern)`
238///
239/// Unknown suffixes fail the build with a `compile_error!` pointing at
240/// the lookup token.
241///
242/// # Combine
243///
244/// Each `Q!()` returns a `TypedFilter<Model>` — chain via the existing
245/// `.and()` / `.or()` / `.not()` methods:
246///
247/// ```ignore
248/// User::objects()
249/// .where_(
250/// Q!(User.active = true)
251/// .and(Q!(User.email__icontains = "alice"))
252/// )
253/// .fetch(&pool).await?;
254/// ```
255///
256/// All emitted code routes through existing per-dialect writers — no new
257/// SQL emission machinery. Tri-dialect support is inherent.
258#[allow(non_snake_case)]
259#[proc_macro]
260pub fn Q(input: TokenStream) -> TokenStream {
261 expand_q(input.into())
262 .unwrap_or_else(syn::Error::into_compile_error)
263 .into()
264}
265
266/// `#[rustango::main]` — the Django-shape runserver entrypoint. Wraps
267/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
268/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
269/// functions are zero-boilerplate:
270///
271/// ```ignore
272/// #[rustango::main]
273/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
274/// rustango::server::Builder::from_env().await?
275/// .migrate("migrations").await?
276/// .api(my_app::urls::api())
277/// .seed_with(my_app::seed::run).await?
278/// .serve("0.0.0.0:8080").await
279/// }
280/// ```
281///
282/// Optional `flavor = "current_thread"` passes through to
283/// `#[tokio::main]`; default is the multi-threaded runtime.
284///
285/// Pulls `tracing-subscriber` into the rustango crate behind the
286/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
287/// out get plain `#[tokio::main]` ergonomics without the dependency.
288#[proc_macro_attribute]
289pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
290 expand_main(args.into(), item.into())
291 .unwrap_or_else(syn::Error::into_compile_error)
292 .into()
293}
294
295fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
296 let mut input: syn::ItemFn = syn::parse2(item)?;
297 if input.sig.asyncness.is_none() {
298 return Err(syn::Error::new(
299 input.sig.ident.span(),
300 "`#[rustango::main]` must wrap an `async fn`",
301 ));
302 }
303
304 // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
305 // to `#[tokio::main]`. Tokio's proc-macro internally emits
306 // `::tokio::*` paths that resolve against the user crate's deps,
307 // so calling it through the rustango re-export still requires the
308 // user to add tokio to their own Cargo.toml. Building the
309 // runtime ourselves keeps the dep transitive through the
310 // `runtime` feature on rustango.
311 //
312 // Parse optional `flavor = "current_thread"` / `flavor =
313 // "multi_thread"` from the attribute args. Unknown args are
314 // tolerated (forward-compat with tokio's own arg surface).
315 let root = rustango_root();
316 let flavor = parse_flavor(&args);
317 let builder_call = match flavor {
318 Flavor::CurrentThread => quote! {
319 #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
320 },
321 Flavor::MultiThread => quote! {
322 #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
323 },
324 };
325
326 // Detach the user body and rewrite `main` as a sync fn that
327 // builds the runtime and blocks on the async body.
328 let user_body = input.block.clone();
329 input.sig.asyncness = None;
330 input.block = syn::parse2(quote! {{
331 {
332 use #root::__private_runtime::tracing_subscriber::{self, EnvFilter};
333 // `try_init` so duplicate installers (e.g. tests already
334 // holding a subscriber) don't panic.
335 let _ = tracing_subscriber::fmt()
336 .with_env_filter(
337 EnvFilter::try_from_default_env()
338 .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
339 )
340 .try_init();
341 }
342 let __rt = #builder_call
343 .enable_all()
344 .build()
345 .expect("failed to build tokio runtime");
346 __rt.block_on(async move #user_body)
347 }})?;
348
349 Ok(quote! {
350 #input
351 })
352}
353
354enum Flavor {
355 MultiThread,
356 CurrentThread,
357}
358
359fn parse_flavor(args: &TokenStream2) -> Flavor {
360 // Cheap parser: look for the literal token sequence
361 // `flavor = "current_thread"`. Everything else (including
362 // bare `multi_thread` or no args) defaults to multi-thread.
363 let s = args.to_string();
364 if s.contains("current_thread") {
365 Flavor::CurrentThread
366 } else {
367 Flavor::MultiThread
368 }
369}
370
371/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
372struct QInput {
373 base_path: syn::Path,
374 field: syn::Ident,
375 value: syn::Expr,
376}
377
378impl syn::parse::Parse for QInput {
379 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
380 let base_path: syn::Path = input.parse()?;
381 input.parse::<syn::Token![.]>()?;
382 let field: syn::Ident = input.parse()?;
383 input.parse::<syn::Token![=]>()?;
384 let value: syn::Expr = input.parse()?;
385 Ok(QInput {
386 base_path,
387 field,
388 value,
389 })
390 }
391}
392
393fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
394 let q: QInput = syn::parse2(input)?;
395 let root = rustango_root();
396 let field_str = q.field.to_string();
397 let field_span = q.field.span();
398 let (base, suffix) = match field_str.find("__") {
399 Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
400 None => (field_str.as_str(), ""),
401 };
402 if base.is_empty() {
403 return Err(syn::Error::new(
404 field_span,
405 "Q!(): field name is empty before `__` suffix",
406 ));
407 }
408 let base_ident = syn::Ident::new(base, field_span);
409 let value = &q.value;
410 let path = &q.base_path;
411
412 // Most suffixes map directly to a Column method with the value
413 // forwarded unchanged. Some need value-shape massaging (wildcards
414 // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
415 // ISNULL). Unknown suffixes fail the build.
416 let expanded = match suffix {
417 "" | "exact" => quote! {
418 #root::core::Column::eq(#path::#base_ident, #value)
419 },
420 "ne" => quote! {
421 #root::core::Column::ne(#path::#base_ident, #value)
422 },
423 "gt" => quote! {
424 #root::core::Column::gt(#path::#base_ident, #value)
425 },
426 "gte" => quote! {
427 #root::core::Column::gte(#path::#base_ident, #value)
428 },
429 "lt" => quote! {
430 #root::core::Column::lt(#path::#base_ident, #value)
431 },
432 "lte" => quote! {
433 #root::core::Column::lte(#path::#base_ident, #value)
434 },
435 "iexact" => quote! {
436 // Django emulates `__iexact` as case-insensitive equality.
437 // The non-wildcard `ILIKE value` is semantically identical
438 // for plain strings; LIKE-metachars `%` `_` in the rhs would
439 // accidentally match more — document the caveat.
440 #root::core::Column::ilike(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
441 },
442 "contains" => quote! {
443 #root::core::Column::like(
444 #path::#base_ident,
445 ::std::format!("%{}%", #value),
446 )
447 },
448 "icontains" => quote! {
449 #root::core::Column::ilike(
450 #path::#base_ident,
451 ::std::format!("%{}%", #value),
452 )
453 },
454 "startswith" => quote! {
455 #root::core::Column::like(
456 #path::#base_ident,
457 ::std::format!("{}%", #value),
458 )
459 },
460 "istartswith" => quote! {
461 #root::core::Column::ilike(
462 #path::#base_ident,
463 ::std::format!("{}%", #value),
464 )
465 },
466 "endswith" => quote! {
467 #root::core::Column::like(
468 #path::#base_ident,
469 ::std::format!("%{}", #value),
470 )
471 },
472 "iendswith" => quote! {
473 #root::core::Column::ilike(
474 #path::#base_ident,
475 ::std::format!("%{}", #value),
476 )
477 },
478 "in" => quote! {
479 #root::core::Column::is_in(#path::#base_ident, #value)
480 },
481 "not_in" => quote! {
482 #root::core::Column::not_in(#path::#base_ident, #value)
483 },
484 "isnull" => {
485 // Must be a bool literal at macro time so we can route to
486 // is_null vs is_not_null without a runtime branch.
487 let b = match value {
488 syn::Expr::Lit(syn::ExprLit {
489 lit: syn::Lit::Bool(b),
490 ..
491 }) => b.value(),
492 _ => {
493 return Err(syn::Error::new_spanned(
494 value,
495 "Q!(): `__isnull` requires a `true` or `false` literal",
496 ));
497 }
498 };
499 if b {
500 quote! { #root::core::Column::is_null(#path::#base_ident) }
501 } else {
502 quote! { #root::core::Column::is_not_null(#path::#base_ident) }
503 }
504 }
505 "between" => {
506 // Accept a tuple literal `(lo, hi)`.
507 let tuple = match value {
508 syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
509 _ => {
510 return Err(syn::Error::new_spanned(
511 value,
512 "Q!(): `__between` requires a tuple literal `(lo, hi)`",
513 ));
514 }
515 };
516 let lo = &tuple.elems[0];
517 let hi = &tuple.elems[1];
518 quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
519 }
520 "regex" => quote! {
521 #root::core::Column::regex(#path::#base_ident, #value)
522 },
523 "iregex" => quote! {
524 #root::core::Column::iregex(#path::#base_ident, #value)
525 },
526 _ => {
527 return Err(syn::Error::new(
528 field_span,
529 format!(
530 "Q!(): unknown lookup suffix `__{}`. Supported: __exact / __iexact / __ne / __gt / __gte / __lt / __lte / __contains / __icontains / __startswith / __istartswith / __endswith / __iendswith / __in / __not_in / __isnull / __between / __regex / __iregex",
531 suffix
532 ),
533 ));
534 }
535 };
536 Ok(expanded)
537}
538
539fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
540 // Default to "./migrations" if invoked without args.
541 let path_str = if input.is_empty() {
542 "./migrations".to_string()
543 } else {
544 let lit: LitStr = syn::parse2(input)?;
545 lit.value()
546 };
547
548 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
549 syn::Error::new(
550 proc_macro2::Span::call_site(),
551 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
552 )
553 })?;
554 let abs = std::path::Path::new(&manifest).join(&path_str);
555
556 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
557 if abs.is_dir() {
558 let read = std::fs::read_dir(&abs).map_err(|e| {
559 syn::Error::new(
560 proc_macro2::Span::call_site(),
561 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
562 )
563 })?;
564 for entry in read.flatten() {
565 let path = entry.path();
566 if !path.is_file() {
567 continue;
568 }
569 if path.extension().and_then(|s| s.to_str()) != Some("json") {
570 continue;
571 }
572 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
573 continue;
574 };
575 entries.push((stem.to_owned(), path));
576 }
577 }
578 entries.sort_by(|a, b| a.0.cmp(&b.0));
579
580 // Compile-time chain validation: read each migration's JSON,
581 // pull `name` and `prev` (file-stem-keyed for the chain check),
582 // and verify every `prev` points to another migration in the
583 // slice. Mismatches between the file stem and the embedded
584 // `name` field — and broken `prev` chains — fail at MACRO
585 // EXPANSION time so a misshapen migration set never compiles.
586 //
587 // This is the v0.4 Slice 5 distinguisher: rustango's JSON
588 // migrations + a Rust proc-macro that reads them is the unique
589 // combo nothing else in the Django-shape Rust camp can match
590 // (Cot's are imperative Rust code, Loco's are SeaORM up/down,
591 // Rwf's are raw SQL — none have a static chain to validate).
592 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
593 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
594 for (stem, path) in &entries {
595 let raw = std::fs::read_to_string(path).map_err(|e| {
596 syn::Error::new(
597 proc_macro2::Span::call_site(),
598 format!(
599 "embed_migrations!: cannot read {} for chain validation: {e}",
600 path.display()
601 ),
602 )
603 })?;
604 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
605 syn::Error::new(
606 proc_macro2::Span::call_site(),
607 format!(
608 "embed_migrations!: {} is not valid JSON: {e}",
609 path.display()
610 ),
611 )
612 })?;
613 let name = json
614 .get("name")
615 .and_then(|v| v.as_str())
616 .ok_or_else(|| {
617 syn::Error::new(
618 proc_macro2::Span::call_site(),
619 format!(
620 "embed_migrations!: {} is missing the `name` field",
621 path.display()
622 ),
623 )
624 })?
625 .to_owned();
626 if name != *stem {
627 return Err(syn::Error::new(
628 proc_macro2::Span::call_site(),
629 format!(
630 "embed_migrations!: file stem `{stem}` does not match the migration's \
631 `name` field `{name}` — rename the file or fix the JSON",
632 ),
633 ));
634 }
635 let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
636 chain_names.push(name.clone());
637 prev_refs.push((name, prev));
638 }
639
640 let name_set: std::collections::HashSet<&str> =
641 chain_names.iter().map(String::as_str).collect();
642 for (name, prev) in &prev_refs {
643 if let Some(p) = prev {
644 if !name_set.contains(p.as_str()) {
645 return Err(syn::Error::new(
646 proc_macro2::Span::call_site(),
647 format!(
648 "embed_migrations!: broken migration chain — `{name}` declares \
649 prev=`{p}` but no migration with that name exists in {}",
650 abs.display()
651 ),
652 ));
653 }
654 }
655 }
656
657 let pairs: Vec<TokenStream2> = entries
658 .iter()
659 .map(|(name, path)| {
660 let path_lit = path.display().to_string();
661 quote! { (#name, ::core::include_str!(#path_lit)) }
662 })
663 .collect();
664
665 Ok(quote! {
666 {
667 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
668 __RUSTANGO_EMBEDDED
669 }
670 })
671}
672
673fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
674 let root = rustango_root();
675 let struct_name = &input.ident;
676
677 let Data::Struct(data) = &input.data else {
678 return Err(syn::Error::new_spanned(
679 struct_name,
680 "Model can only be derived on structs",
681 ));
682 };
683 let Fields::Named(named) = &data.fields else {
684 return Err(syn::Error::new_spanned(
685 struct_name,
686 "Model requires a struct with named fields",
687 ));
688 };
689
690 let container = parse_container_attrs(input)?;
691 let table = container
692 .table
693 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
694 let model_name = struct_name.to_string();
695
696 let collected = collect_fields(named, &table)?;
697
698 // Validate that #[rustango(display = "…")] names a real field.
699 if let Some((ref display, span)) = container.display {
700 if !collected.field_names.iter().any(|n| n == display) {
701 return Err(syn::Error::new(
702 span,
703 format!("`display = \"{display}\"` does not match any field on this struct"),
704 ));
705 }
706 }
707 let display = container.display.map(|(name, _)| name);
708 let app_label = container.app.clone();
709
710 // Validate admin field-name lists against declared field names.
711 // Note: `list_display` is intentionally NOT validated here. As of
712 // v0.32 it may also reference inventory-registered computed
713 // fields (via `register_admin_computed!`) whose existence the
714 // macro can't see at compile time — they're submitted from any
715 // crate that depends on rustango. The runtime list-view resolves
716 // unknown names against the inventory + silently drops the
717 // truly-bogus ones, which is the cheaper trade-off versus
718 // forcing a per-Model attr to opt out.
719 if let Some(admin) = &container.admin {
720 for (label, list) in [
721 ("search_fields", &admin.search_fields),
722 ("readonly_fields", &admin.readonly_fields),
723 ("list_filter", &admin.list_filter),
724 ] {
725 if let Some((names, span)) = list {
726 for name in names {
727 if !collected.field_names.iter().any(|n| n == name) {
728 return Err(syn::Error::new(
729 *span,
730 format!(
731 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
732 ),
733 ));
734 }
735 }
736 }
737 }
738 if let Some((pairs, span)) = &admin.ordering {
739 for (name, _) in pairs {
740 if !collected.field_names.iter().any(|n| n == name) {
741 return Err(syn::Error::new(
742 *span,
743 format!(
744 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
745 ),
746 ));
747 }
748 }
749 }
750 if let Some((groups, span)) = &admin.fieldsets {
751 for (_, fields) in groups {
752 for name in fields {
753 if !collected.field_names.iter().any(|n| n == name) {
754 return Err(syn::Error::new(
755 *span,
756 format!(
757 "`fieldsets`: \"{name}\" is not a declared field on this struct"
758 ),
759 ));
760 }
761 }
762 }
763 }
764 }
765 if let Some(audit) = &container.audit {
766 if let Some((names, span)) = &audit.track {
767 for name in names {
768 if !collected.field_names.iter().any(|n| n == name) {
769 return Err(syn::Error::new(
770 *span,
771 format!(
772 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
773 ),
774 ));
775 }
776 }
777 }
778 }
779
780 // Issue #291 / T2.5 — validate each `default_order` column name
781 // against the model's collected fields. Typos fail at macro-expand
782 // time, not at the database.
783 for (col, _desc, span) in &container.default_order {
784 if !collected.field_names.iter().any(|n| n == col) {
785 return Err(syn::Error::new(
786 *span,
787 format!(
788 "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
789 ),
790 ));
791 }
792 }
793
794 // Build the audit_track list for ModelSchema: None when no audit attr,
795 // Some(empty) when audit present without track, Some(names) when explicit.
796 let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
797 audit
798 .track
799 .as_ref()
800 .map(|(names, _)| names.clone())
801 .unwrap_or_default()
802 });
803
804 // Merge field-level indexes into the container's index list.
805 let mut all_indexes: Vec<IndexAttr> = container.indexes;
806 for field in &named.named {
807 let ident = field.ident.as_ref().expect("named");
808 let col = to_snake_case(&ident.to_string()); // column name fallback
809 // Re-parse field attrs to check for index flag
810 if let Ok(fa) = parse_field_attrs(field) {
811 if fa.index {
812 let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
813 let auto_name = if fa.index_unique {
814 format!("{table}_{col_name}_uq_idx")
815 } else {
816 format!("{table}_{col_name}_idx")
817 };
818 all_indexes.push(IndexAttr {
819 name: fa.index_name.or(Some(auto_name)),
820 columns: vec![col_name],
821 unique: fa.index_unique,
822 method: fa.index_method,
823 where_clause: None,
824 include: Vec::new(),
825 });
826 }
827 }
828 }
829
830 let model_impl = model_impl_tokens(
831 struct_name,
832 &model_name,
833 &table,
834 display.as_deref(),
835 app_label.as_deref(),
836 container.admin.as_ref(),
837 &container.default_order,
838 &collected.field_schemas,
839 collected.soft_delete_column.as_deref(),
840 container.permissions,
841 audit_track_names.as_deref(),
842 &container.m2m,
843 &all_indexes,
844 &container.checks,
845 &container.excludes,
846 &container.composite_fks,
847 &container.generic_fks,
848 container.scope.as_deref(),
849 container.is_view,
850 container.verbose_name.as_deref(),
851 container.verbose_name_plural.as_deref(),
852 container.managed,
853 container.base_manager_name.as_deref(),
854 container.order_with_respect_to.as_deref(),
855 container.proxy,
856 &container.required_db_features,
857 container.required_db_vendor.as_deref(),
858 container.default_related_name.as_deref(),
859 container.db_table_comment.as_deref(),
860 container
861 .get_latest_by
862 .as_ref()
863 .map(|(c, d)| (c.as_str(), *d)),
864 &container.extra_permissions,
865 &container.default_permissions,
866 &container.global_scopes,
867 &container.reverse_has_relations,
868 &container.generic_has_relations,
869 );
870 let module_ident = column_module_ident(struct_name);
871 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
872 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
873 let track_set: Option<std::collections::HashSet<&str>> = audit
874 .track
875 .as_ref()
876 .map(|(names, _)| names.iter().map(String::as_str).collect());
877 collected
878 .column_entries
879 .iter()
880 .filter(|c| {
881 track_set
882 .as_ref()
883 .map_or(true, |s| s.contains(c.name.as_str()))
884 })
885 .collect()
886 });
887 let inherent_impl = inherent_impl_tokens(
888 struct_name,
889 &collected,
890 collected.primary_key.as_ref(),
891 &column_consts,
892 audited_fields.as_deref(),
893 &all_indexes,
894 &container.manager_fns,
895 );
896 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
897 let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
898 let reverse_helpers = reverse_helper_tokens(
899 struct_name,
900 &collected.fk_relations,
901 container.default_related_name.as_deref(),
902 );
903 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
904 let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
905 // Issue #817 — `#[rustango(through(...))]` accessors.
906 let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
907 // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
908 let reverse_has_accessors =
909 reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
910 let generic_fk_accessors = generic_fk_accessor_tokens(
911 struct_name,
912 &container.generic_fks,
913 &collected.column_entries,
914 );
915
916 // Issue #271 / T1.9 — `#[rustango(manager(ext = "FooManagerExt"))]`
917 // emits an empty extension trait so users can add methods via
918 // `impl FooManagerExt for QuerySet<Foo>` without hand-writing the
919 // trait declaration. See `crates/rustango/src/manager.rs` for the
920 // pattern this replaces.
921 let manager_trait = container.manager_ext.as_ref().map(|name| {
922 let model_name_str = struct_name.to_string();
923 let doc = format!(
924 "Custom-Manager extension trait for [`{model_name_str}`]. \
925 Generated by `#[rustango(manager(ext = ...))]`. Add methods \
926 via `impl {name} for QuerySet<{model_name_str}> {{ ... }}`."
927 );
928 quote! {
929 #[doc = #doc]
930 pub trait #name: ::core::marker::Sized {}
931 }
932 });
933
934 Ok(quote! {
935 #model_impl
936 #inherent_impl
937 #from_row_impl
938 #column_module
939 #reverse_helpers
940 #m2m_accessors
941 #generic_m2m_accessors
942 #through_accessors
943 #reverse_has_accessors
944 #generic_fk_accessors
945 #manager_trait
946
947 #root::core::inventory::submit! {
948 #root::core::ModelEntry {
949 schema: <#struct_name as #root::core::Model>::SCHEMA,
950 // `module_path!()` evaluates at the registration site,
951 // so a Model declared in `crate::blog::models` records
952 // `"<crate>::blog::models"` and `resolved_app_label()`
953 // can infer "blog" without an explicit attribute.
954 module_path: ::core::module_path!(),
955 }
956 }
957 })
958}
959
960/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
961/// matches `field_name` against the model's FK fields and, for a
962/// match, decodes the FK target via the parent's macro-generated
963/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
964/// `ForeignKey::Loaded` on `self`.
965///
966/// Always emitted (with empty arms for FK-less models, which
967/// return `Ok(false)` for any field name) so the `T: LoadRelated`
968/// trait bound on `fetch_on` is universally satisfied — users
969/// never have to think about implementing it.
970fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
971 let root = rustango_root();
972 let arms = fk_relations.iter().map(|rel| {
973 let parent_ty = &rel.parent_type;
974 let fk_col = rel.fk_column.as_str();
975 // FK field's Rust ident matches its SQL column name in v0.8
976 // (no `column = "..."` rename ships on FK fields).
977 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
978 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
979 let assign = if rel.nullable {
980 quote! {
981 self.#field_ident = ::core::option::Option::Some(
982 #root::sql::ForeignKey::loaded(_pk, _parent),
983 );
984 }
985 } else {
986 quote! {
987 self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
988 }
989 };
990 quote! {
991 #fk_col => {
992 let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
993 // Audit #451 — multi-hop `select_related("a__b__c")`:
994 // stitch the deeper relation onto this parent first,
995 // decoding it at the accumulated `__next_alias`. The
996 // parent type also impls `LoadRelated`, so this recurses
997 // the FK chain to arbitrary depth.
998 if let ::core::option::Option::Some(__r) = __rest {
999 let _ = #root::sql::LoadRelated::__rustango_load_related(
1000 &mut _parent, row, __r, &__next_alias,
1001 )?;
1002 }
1003 // Loud-in-debug, default-in-release: a divergence
1004 // between the FK field's declared `K` (drives the
1005 // expected `SqlValue::<Variant>`) and the parent's
1006 // `__rustango_pk_value` output is a macro-internal
1007 // invariant break — surfacing the panic in dev
1008 // catches it before users hit silent PK=0 corruption.
1009 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1010 #root::core::SqlValue::#variant_ident(v) => v,
1011 _other => {
1012 ::core::debug_assert!(
1013 false,
1014 "rustango macro bug: load_related on FK `{}` expected \
1015 SqlValue::{} from parent's __rustango_pk_value but got \
1016 {:?} — file a bug at https://github.com/ujeenet/rustango",
1017 #fk_col,
1018 ::core::stringify!(#variant_ident),
1019 _other,
1020 );
1021 #default_expr
1022 }
1023 };
1024 #assign
1025 ::core::result::Result::Ok(true)
1026 }
1027 }
1028 });
1029 quote! {
1030 #[cfg(feature = "postgres")]
1031 impl #root::sql::LoadRelated for #struct_name {
1032 #[allow(unused_variables)]
1033 fn __rustango_load_related(
1034 &mut self,
1035 row: &#root::sql::sqlx::postgres::PgRow,
1036 field_name: &str,
1037 alias: &str,
1038 ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1039 // Audit #451 — split the multi-hop path: `base` is the FK
1040 // on THIS model, `__rest` (if any) is the remaining chain
1041 // to stitch onto the loaded parent. `__next_alias` is the
1042 // accumulated join alias the parent's columns live under
1043 // (`{alias}__{next-hop}`), matching `lower_select_related`.
1044 let (__base, __rest): (&str, ::core::option::Option<&str>) =
1045 match field_name.split_once("__") {
1046 ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1047 ::core::option::Option::None => (field_name, ::core::option::Option::None),
1048 };
1049 let __next_alias: ::std::string::String = match __rest {
1050 ::core::option::Option::Some(__r) => {
1051 let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1052 ::std::format!("{}__{}", alias, __rb)
1053 }
1054 ::core::option::Option::None => ::std::string::String::new(),
1055 };
1056 match __base {
1057 #( #arms )*
1058 _ => ::core::result::Result::Ok(false),
1059 }
1060 }
1061 }
1062 }
1063}
1064
1065/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1066/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1067/// which expands to a `LoadRelatedMy` impl when rustango is built with
1068/// the `mysql` feature, and to nothing otherwise. The decoded parent
1069/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1070/// decoder, also batch8) so the dual emission is symmetric across
1071/// backends.
1072fn load_related_impl_my_tokens(
1073 struct_name: &syn::Ident,
1074 fk_relations: &[FkRelation],
1075) -> TokenStream2 {
1076 let root = rustango_root();
1077 let arms = fk_relations.iter().map(|rel| {
1078 let parent_ty = &rel.parent_type;
1079 let fk_col = rel.fk_column.as_str();
1080 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1081 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1082 let assign = if rel.nullable {
1083 quote! {
1084 __self.#field_ident = ::core::option::Option::Some(
1085 #root::sql::ForeignKey::loaded(_pk, _parent),
1086 );
1087 }
1088 } else {
1089 quote! {
1090 __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1091 }
1092 };
1093 // `self` IS hygiene-tracked through macro_rules — emitted from
1094 // a different context than the `&mut self` parameter inside
1095 // the macro_rules-expanded fn. Pass it through as `__self`
1096 // and let the macro_rules rebind it to the receiver.
1097 quote! {
1098 #fk_col => {
1099 let mut _parent: #parent_ty =
1100 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1101 // Audit #451 — multi-hop: stitch the deeper relation onto
1102 // the parent at the accumulated alias (see PG twin).
1103 if let ::core::option::Option::Some(__r) = __rest {
1104 let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1105 &mut _parent, row, __r, &__next_alias,
1106 )?;
1107 }
1108 // See note in `load_related_impl_tokens` (PG twin) —
1109 // the same loud-in-debug invariant guard.
1110 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1111 #root::core::SqlValue::#variant_ident(v) => v,
1112 _other => {
1113 ::core::debug_assert!(
1114 false,
1115 "rustango macro bug: load_related on FK `{}` expected \
1116 SqlValue::{} from parent's __rustango_pk_value but got \
1117 {:?} — file a bug at https://github.com/ujeenet/rustango",
1118 #fk_col,
1119 ::core::stringify!(#variant_ident),
1120 _other,
1121 );
1122 #default_expr
1123 }
1124 };
1125 #assign
1126 ::core::result::Result::Ok(true)
1127 }
1128 }
1129 });
1130 quote! {
1131 #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1132 #( #arms )*
1133 });
1134 }
1135}
1136
1137/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1138/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1139/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1140fn load_related_impl_sqlite_tokens(
1141 struct_name: &syn::Ident,
1142 fk_relations: &[FkRelation],
1143) -> TokenStream2 {
1144 let root = rustango_root();
1145 let arms = fk_relations.iter().map(|rel| {
1146 let parent_ty = &rel.parent_type;
1147 let fk_col = rel.fk_column.as_str();
1148 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1149 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1150 let assign = if rel.nullable {
1151 quote! {
1152 __self.#field_ident = ::core::option::Option::Some(
1153 #root::sql::ForeignKey::loaded(_pk, _parent),
1154 );
1155 }
1156 } else {
1157 quote! {
1158 __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1159 }
1160 };
1161 quote! {
1162 #fk_col => {
1163 let mut _parent: #parent_ty =
1164 <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1165 // Audit #451 — multi-hop: stitch the deeper relation onto
1166 // the parent at the accumulated alias (see PG twin).
1167 if let ::core::option::Option::Some(__r) = __rest {
1168 let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1169 &mut _parent, row, __r, &__next_alias,
1170 )?;
1171 }
1172 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1173 #root::core::SqlValue::#variant_ident(v) => v,
1174 _other => {
1175 ::core::debug_assert!(
1176 false,
1177 "rustango macro bug: load_related on FK `{}` expected \
1178 SqlValue::{} from parent's __rustango_pk_value but got \
1179 {:?} — file a bug at https://github.com/ujeenet/rustango",
1180 #fk_col,
1181 ::core::stringify!(#variant_ident),
1182 _other,
1183 );
1184 #default_expr
1185 }
1186 };
1187 #assign
1188 ::core::result::Result::Ok(true)
1189 }
1190 }
1191 });
1192 quote! {
1193 #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1194 #( #arms )*
1195 });
1196 }
1197}
1198
1199/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1200/// matches `field_name` against the model's FK fields and returns
1201/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1202/// group children by parent PK.
1203///
1204/// Always emitted (with `_ => None` for FK-less models) so the
1205/// trait bound on `fetch_with_prefetch` is universally satisfied.
1206fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1207 let root = rustango_root();
1208 let arms = fk_relations.iter().map(|rel| {
1209 let fk_col = rel.fk_column.as_str();
1210 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1211 if rel.pk_kind == DetectedKind::I64 {
1212 // i64 FK — return the stored PK so prefetch_related can
1213 // group children by it. Nullable variant unwraps via
1214 // `as_ref().map(...)`: an unset (NULL) FK column yields
1215 // `None` and that child sits out of the grouping (correct
1216 // semantics — it has no parent to attach to).
1217 if rel.nullable {
1218 quote! {
1219 #fk_col => self.#field_ident
1220 .as_ref()
1221 .map(|fk| #root::sql::ForeignKey::pk(fk)),
1222 }
1223 } else {
1224 quote! {
1225 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1226 }
1227 }
1228 } else {
1229 // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1230 // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1231 // i64-keyed grouping path — the trait signature is
1232 // `Option<i64>` and a non-i64 PK can't lower into it.
1233 // The FK still works for everything else (CRUD, lazy
1234 // load via `.get()`, select_related JOINs); only the
1235 // bulk prefetch grouper needs the integer key.
1236 quote! {
1237 #fk_col => ::core::option::Option::None,
1238 }
1239 }
1240 });
1241 // PK-type-agnostic version: every FK arm emits an
1242 // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1243 // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1244 // opt OUT of the legacy i64 method (it returns None) but opt IN
1245 // here.
1246 let value_arms = fk_relations.iter().map(|rel| {
1247 let fk_col = rel.fk_column.as_str();
1248 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1249 if rel.nullable {
1250 quote! {
1251 #fk_col => self.#field_ident
1252 .as_ref()
1253 .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1254 #root::sql::ForeignKey::pk(fk)
1255 )),
1256 }
1257 } else {
1258 quote! {
1259 #fk_col => ::core::option::Option::Some(
1260 ::core::convert::Into::<#root::core::SqlValue>::into(
1261 self.#field_ident.pk()
1262 )
1263 ),
1264 }
1265 }
1266 });
1267 quote! {
1268 impl #root::sql::FkPkAccess for #struct_name {
1269 #[allow(unused_variables)]
1270 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1271 match field_name {
1272 #( #arms )*
1273 _ => ::core::option::Option::None,
1274 }
1275 }
1276 #[allow(unused_variables)]
1277 fn __rustango_fk_pk_value(
1278 &self,
1279 field_name: &str,
1280 ) -> ::core::option::Option<#root::core::SqlValue> {
1281 match field_name {
1282 #( #value_arms )*
1283 _ => ::core::option::Option::None,
1284 }
1285 }
1286 }
1287 }
1288}
1289
1290/// For every `ForeignKey<Parent>` field on `Child`, emit
1291/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1292/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
1293/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
1294/// — the canonical reverse-FK fetch. One round trip, no N+1.
1295///
1296/// **PG-only emission**: the accessor is bounded on
1297/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1298/// both of which are gated behind the `postgres` cargo feature. The
1299/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1300/// model derive itself compiles on tri-dialect / sqlite-only
1301/// downstream builds — the accessor just isn't materialised. A tri-
1302/// dialect `_set_pool` variant is a separate follow-up.
1303fn reverse_helper_tokens(
1304 child_ident: &syn::Ident,
1305 fk_relations: &[FkRelation],
1306 default_related_name: Option<&str>,
1307) -> TokenStream2 {
1308 let root = rustango_root();
1309 if fk_relations.is_empty() {
1310 return TokenStream2::new();
1311 }
1312 // Method-name resolution per FK (issue #816 + follow-up):
1313 // 1. Field-level `#[rustango(related_name = "...")]` on the FK
1314 // itself — wins over everything else. Django's
1315 // `ForeignKey(related_name="...")`.
1316 // 2. Container-level `default_related_name = "..."` on the
1317 // child — Django's `class Meta: default_related_name`.
1318 // Applies to every FK on this model that didn't override.
1319 // 3. Fallback: `<child_snake>_set` — Django's `<child>_set`
1320 // convention. `Post` → `post_set`, `BlogComment` →
1321 // `blog_comment_set`. Avoids English-plural edge cases.
1322 //
1323 // The PG-on-executor variant keeps the resolved name; the
1324 // tri-dialect `_pool` variant appends `_pool` to it (matches the
1325 // framework's convention for the `&Pool` flavor of every helper).
1326 let default_pg_suffix = default_related_name
1327 .map(str::to_owned)
1328 .unwrap_or_else(|| format!("{}_set", to_snake_case(&child_ident.to_string())));
1329 let impls = fk_relations.iter().map(|rel| {
1330 let pg_suffix = rel
1331 .related_name
1332 .clone()
1333 .unwrap_or_else(|| default_pg_suffix.clone());
1334 let pool_suffix = format!("{}_pool", pg_suffix);
1335 let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1336 let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1337 let parent_ty = &rel.parent_type;
1338 let fk_col = rel.fk_column.as_str();
1339 let doc = format!(
1340 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1341 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1342 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1343 further `{child_ident}::objects()` filters via direct queryset use."
1344 );
1345 let pool_doc = format!(
1346 "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1347 [`#root::sql::Pool`] and dispatches per backend so the \
1348 reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1349 Use this from framework code that holds a `&Pool` (admin, \
1350 tenancy resolver, viewset handlers); reach for the executor- \
1351 bound variant when you already have a typed `sqlx::Executor`."
1352 );
1353 quote! {
1354 #[cfg(feature = "postgres")]
1355 impl #parent_ty {
1356 #[doc = #doc]
1357 ///
1358 /// # Errors
1359 /// Returns [`#root::sql::ExecError`] for SQL-writing
1360 /// or driver failures.
1361 pub async fn #pg_method_ident<'_c, _E>(
1362 &self,
1363 _executor: _E,
1364 ) -> ::core::result::Result<
1365 ::std::vec::Vec<#child_ident>,
1366 #root::sql::ExecError,
1367 >
1368 where
1369 _E: #root::sql::sqlx::Executor<
1370 '_c,
1371 Database = #root::sql::sqlx::Postgres,
1372 >,
1373 {
1374 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1375 #root::query::QuerySet::<#child_ident>::new()
1376 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1377 .fetch_on(_executor)
1378 .await
1379 }
1380 }
1381
1382 impl #parent_ty {
1383 #[doc = #pool_doc]
1384 ///
1385 /// # Errors
1386 /// Returns [`#root::sql::ExecError`] for SQL-writing
1387 /// or driver failures.
1388 pub async fn #pool_method_ident(
1389 &self,
1390 pool: &#root::sql::Pool,
1391 ) -> ::core::result::Result<
1392 ::std::vec::Vec<#child_ident>,
1393 #root::sql::ExecError,
1394 > {
1395 use #root::sql::FetcherPool as _;
1396 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1397 #root::query::QuerySet::<#child_ident>::new()
1398 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1399 .fetch(pool)
1400 .await
1401 }
1402 }
1403 }
1404 });
1405 quote! { #( #impls )* }
1406}
1407
1408/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1409/// relation declared on the model.
1410/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1411/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1412///
1413/// Closes #239 + #240 — the Django-shape `comment.content_object` /
1414/// `comment.content_object = post` ergonomics on top of the existing
1415/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1416///
1417/// `column_entries` is passed so we can resolve each `ct_column` /
1418/// `pk_column` SQL name back to its Rust field ident — the macro
1419/// only sees the column-side strings in the attribute, but the
1420/// emitted accessor needs to read the actual struct field.
1421fn generic_fk_accessor_tokens(
1422 struct_name: &syn::Ident,
1423 generic_fks: &[GenericFkAttr],
1424 column_entries: &[ColumnEntry],
1425) -> TokenStream2 {
1426 let root = rustango_root();
1427 if generic_fks.is_empty() {
1428 return TokenStream2::new();
1429 }
1430 let methods = generic_fks.iter().filter_map(|gfk| {
1431 // Resolve `ct_column` + `pk_column` to the struct's Rust
1432 // field idents. A typo (column name doesn't match any field)
1433 // emits no method for that registration — the user will see
1434 // the compiler reject the SCHEMA literal anyway, so there's
1435 // a clear error path without us double-reporting.
1436 let ct_ident = column_entries
1437 .iter()
1438 .find(|c| c.column == gfk.ct_column)
1439 .map(|c| c.ident.clone())?;
1440 let pk_ident = column_entries
1441 .iter()
1442 .find(|c| c.column == gfk.pk_column)
1443 .map(|c| c.ident.clone())?;
1444
1445 let accessor_ident =
1446 syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1447 let setter_ident =
1448 syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1449 let name_literal = gfk.name.as_str();
1450
1451 Some(quote! {
1452 #[doc = concat!(
1453 "Resolve the polymorphic `",
1454 #name_literal,
1455 "` relation. Reads `self.",
1456 stringify!(#ct_ident),
1457 "` + `self.",
1458 stringify!(#pk_ident),
1459 "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1460 "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1461 #name_literal,
1462 "\", ...))]`."
1463 )]
1464 pub async fn #accessor_ident(
1465 &self,
1466 pool: &#root::sql::Pool,
1467 ) -> ::core::result::Result<
1468 ::core::option::Option<#root::__serde_json::Value>,
1469 #root::sql::ExecError,
1470 > {
1471 let gfk = #root::contenttypes::GenericForeignKey::new(
1472 self.#ct_ident as i64,
1473 self.#pk_ident as i64,
1474 );
1475 gfk.get_object(pool).await
1476 }
1477
1478 #[doc = concat!(
1479 "Set the polymorphic `",
1480 #name_literal,
1481 "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1482 stringify!(#ct_ident),
1483 "` and `self.",
1484 stringify!(#pk_ident),
1485 "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1486 #name_literal,
1487 "\", ...))]`."
1488 )]
1489 pub async fn #setter_ident<T: #root::core::Model>(
1490 &mut self,
1491 pool: &#root::sql::Pool,
1492 target_pk: i64,
1493 ) -> ::core::result::Result<(), #root::sql::ExecError> {
1494 let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1495 pool,
1496 target_pk,
1497 ).await?;
1498 self.#ct_ident = gfk.content_type_id as _;
1499 self.#pk_ident = gfk.object_pk as _;
1500 ::core::result::Result::Ok(())
1501 }
1502 })
1503 });
1504 quote! {
1505 impl #struct_name {
1506 #( #methods )*
1507 }
1508 }
1509}
1510
1511fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1512 let root = rustango_root();
1513 if m2m_relations.is_empty() {
1514 return TokenStream2::new();
1515 }
1516 let methods = m2m_relations.iter().map(|rel| {
1517 let method_name = format!("{}_m2m", rel.name);
1518 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1519 let through = rel.through.as_str();
1520 let src_col = rel.src.as_str();
1521 let dst_col = rel.dst.as_str();
1522 quote! {
1523 pub fn #method_ident(&self) -> #root::sql::M2MManager {
1524 #root::sql::M2MManager {
1525 src_pk: self.__rustango_pk_value(),
1526 through: #through,
1527 src_col: #src_col,
1528 dst_col: #dst_col,
1529 }
1530 }
1531 }
1532 });
1533 quote! {
1534 impl #struct_name {
1535 #( #methods )*
1536 }
1537 }
1538}
1539
1540/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1541/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1542fn generic_m2m_accessor_tokens(
1543 struct_name: &syn::Ident,
1544 relations: &[GenericM2MAttr],
1545) -> TokenStream2 {
1546 let root = rustango_root();
1547 if relations.is_empty() {
1548 return TokenStream2::new();
1549 }
1550 let methods = relations.iter().map(|rel| {
1551 let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1552 let through = rel.through.as_str();
1553 let pk_col = rel.pk_column.as_str();
1554 let ct_col = rel.ct_column.as_str();
1555 let related_col = rel.related_column.as_str();
1556 quote! {
1557 pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1558 #root::sql::GenericM2MManager {
1559 src_pk: self.__rustango_pk_value(),
1560 src_schema: <Self as #root::core::Model>::SCHEMA,
1561 through: #through,
1562 pk_col: #pk_col,
1563 ct_col: #ct_col,
1564 dst_col: #related_col,
1565 }
1566 }
1567 }
1568 });
1569 quote! {
1570 impl #struct_name {
1571 #( #methods )*
1572 }
1573 }
1574}
1575
1576/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1577/// associated functions for each `#[rustango(reverse_has(...))]`
1578/// attribute. Issue #830.
1579///
1580/// The two emitted functions return ready-to-use `WhereExpr` nodes
1581/// that downstream callers drop into
1582/// `QuerySet::<Self>::where_raw(...)`:
1583///
1584/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1585/// <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1586/// `whereHas` parity (without the closure-style sub-predicate
1587/// refinement — that's a follow-up; users can layer additional
1588/// predicates by constructing the SelectQuery themselves and
1589/// calling `where_raw(exists(query))` from `crate::core::subquery`).
1590/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1591/// `whereDoesntHave` parity.
1592///
1593/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1594/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1595/// machinery threads the outer-table reference through automatically
1596/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1597fn reverse_has_accessor_tokens(
1598 struct_name: &syn::Ident,
1599 reverse_has_relations: &[ReverseHasAttr],
1600) -> TokenStream2 {
1601 let root = rustango_root();
1602 if reverse_has_relations.is_empty() {
1603 return TokenStream2::new();
1604 }
1605 let methods = reverse_has_relations.iter().map(|rel| {
1606 let exists_name = format!("{}_exists_expr", rel.name);
1607 let not_exists_name = format!("{}_not_exists_expr", rel.name);
1608 let count_name = format!("{}_count", rel.name);
1609 let fetch_name = format!("{}_fetch", rel.name);
1610 let first_name = format!("{}_first", rel.name);
1611 let pluck_name = format!("{}_pluck", rel.name);
1612 let accessor_name = rel.name.as_str();
1613 let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1614 let not_exists_ident = syn::Ident::new(¬_exists_name, struct_name.span());
1615 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1616 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1617 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1618 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1619 let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1620 let child = &rel.child;
1621 let child_fk_column = rel.child_fk_column.as_str();
1622 let self_pk_column = rel.self_pk_column.as_str();
1623 let exists_doc = format!(
1624 "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1625 FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1626 Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1627 filter to {struct_name}s with at least one matching child.",
1628 );
1629 let not_exists_doc = format!(
1630 "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1631 (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1632 <outer>.{self_pk_column})`. Drop into \
1633 `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1634 {struct_name}s with **no** matching child.",
1635 );
1636 let count_doc = format!(
1637 "Eloquent `$model->{name}->count()` analog — returns \
1638 the number of `{child}` rows whose `{child_fk_column}` \
1639 matches this `{struct_name}` instance's primary key. \
1640 Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1641 {child_fk_column} = <self.pk>`.",
1642 name = rel.name,
1643 );
1644 let accessor_doc = format!(
1645 "Eloquent `$model->{name}` accessor — returns a \
1646 `QuerySet<{child}>` filtered to rows whose \
1647 `{child_fk_column}` matches this `{struct_name}` \
1648 instance's primary key. **Chainable**: compose `.filter()` \
1649 / `.order_by()` / `.limit()` etc. on top, then call \
1650 `.fetch(&pool)` (the QuerySet trait method) when \
1651 done. For the simple \"fetch all\" hot path with no \
1652 further composition, prefer the bare-name \
1653 `{name}_fetch(&pool)` companion.",
1654 name = rel.name,
1655 );
1656 let fetch_doc = format!(
1657 "Eloquent `$model->{name}->get()` — bare-name hot-path \
1658 over `{name}(&self).fetch(&pool)`. Use this when \
1659 you don't need further `.filter()` / `.order_by()` \
1660 composition; falls back to the chainable accessor when \
1661 you do. Avoids the `_pool` suffix on the most common \
1662 call-site shape.",
1663 name = rel.name,
1664 );
1665 quote! {
1666 #[doc = #accessor_doc]
1667 pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1668 #root::query::QuerySet::<#child>::new()
1669 .filter(#child_fk_column, self.__rustango_pk_value())
1670 }
1671
1672 #[doc = #fetch_doc]
1673 pub async fn #fetch_ident(
1674 &self,
1675 pool: &#root::sql::Pool,
1676 ) -> ::core::result::Result<
1677 ::std::vec::Vec<#child>,
1678 #root::sql::ExecError,
1679 > {
1680 use #root::sql::FetcherPool as _;
1681 self.#accessor_ident().fetch(pool).await
1682 }
1683
1684 /// Eloquent `$model->relation->first()` / `hasOne`
1685 /// semantics — bare-name shortcut over
1686 /// `self.<name>().first(&pool)`. Returns `None` when no
1687 /// child rows match. Useful when the relation is
1688 /// nominally many-to-one in shape but at most one row is
1689 /// expected (latest comment, primary tag, etc.).
1690 pub async fn #first_ident(
1691 &self,
1692 pool: &#root::sql::Pool,
1693 ) -> ::core::result::Result<
1694 ::core::option::Option<#child>,
1695 #root::sql::ExecError,
1696 > {
1697 self.#accessor_ident().first(pool).await
1698 }
1699
1700 /// Eloquent `$model->relation->pluck($col)` — project a
1701 /// single column from the child rows into a `Vec<U>`.
1702 /// Skips the typed `Child` decode, which is cheaper when
1703 /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1704 pub async fn #pluck_ident<U>(
1705 &self,
1706 col: &'static str,
1707 pool: &#root::sql::Pool,
1708 ) -> ::core::result::Result<
1709 ::std::vec::Vec<U>,
1710 #root::sql::ExecError,
1711 >
1712 where
1713 U: #root::sql::MaybePgScalar
1714 + #root::sql::MaybeMyScalar
1715 + #root::sql::MaybeSqliteScalar
1716 + ::core::marker::Send
1717 + ::core::marker::Unpin,
1718 {
1719 self.#accessor_ident()
1720 .values_list_flat(col)
1721 .fetch::<U>(pool)
1722 .await
1723 }
1724
1725 #[doc = #exists_doc]
1726 pub fn #exists_ident() -> #root::core::WhereExpr {
1727 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1728 let child_schema =
1729 <#child as #root::core::Model>::SCHEMA;
1730 let inner = SelectQuery {
1731 where_clause: WhereExpr::ExprCompare {
1732 lhs: Expr::Column(#child_fk_column),
1733 op: Op::Eq,
1734 rhs: Expr::OuterRef(#self_pk_column),
1735 },
1736 ..SelectQuery::new(child_schema)
1737 };
1738 WhereExpr::Exists(::std::boxed::Box::new(inner))
1739 }
1740
1741 #[doc = #not_exists_doc]
1742 pub fn #not_exists_ident() -> #root::core::WhereExpr {
1743 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1744 let child_schema =
1745 <#child as #root::core::Model>::SCHEMA;
1746 let inner = SelectQuery {
1747 where_clause: WhereExpr::ExprCompare {
1748 lhs: Expr::Column(#child_fk_column),
1749 op: Op::Eq,
1750 rhs: Expr::OuterRef(#self_pk_column),
1751 },
1752 ..SelectQuery::new(child_schema)
1753 };
1754 WhereExpr::NotExists(::std::boxed::Box::new(inner))
1755 }
1756
1757 #[doc = #count_doc]
1758 pub async fn #count_ident(
1759 &self,
1760 pool: &#root::sql::Pool,
1761 ) -> ::core::result::Result<
1762 i64,
1763 #root::sql::ExecError,
1764 > {
1765 use #root::sql::CounterPool as _;
1766 #root::query::QuerySet::<#child>::new()
1767 .filter(#child_fk_column, self.__rustango_pk_value())
1768 .count(pool)
1769 .await
1770 }
1771 }
1772 });
1773 quote! {
1774 impl #struct_name {
1775 #( #methods )*
1776 }
1777 }
1778}
1779
1780/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1781/// `#[rustango(through(...))]` attribute. Issue #817.
1782///
1783/// Each accessor builds a correlated subquery via
1784/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1785/// intermediate table, filters on the FK column pointing at this
1786/// model, and projects the intermediate PK. The outer queryset
1787/// filters the far model by its FK-to-intermediate column being in
1788/// that set.
1789///
1790/// The returned `QuerySet<Far>` is **chainable** — the subquery
1791/// lives inside a `where_raw` clause so the user's later
1792/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1793/// disturb it.
1794///
1795/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1796/// SQLite — no LATERAL or backend-specific syntax involved.
1797fn through_accessor_tokens(
1798 struct_name: &syn::Ident,
1799 through_relations: &[ThroughAttr],
1800) -> TokenStream2 {
1801 let root = rustango_root();
1802 if through_relations.is_empty() {
1803 return TokenStream2::new();
1804 }
1805 let methods = through_relations.iter().map(|rel| {
1806 let method_name = format!("{}_through", rel.name);
1807 let count_name = format!("{}_through_count", rel.name);
1808 let fetch_name = format!("{}_through_fetch", rel.name);
1809 let first_name = format!("{}_through_first", rel.name);
1810 let pluck_name = format!("{}_through_pluck", rel.name);
1811 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1812 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1813 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1814 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1815 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1816 let far = &rel.far;
1817 let intermediate = &rel.intermediate;
1818 let far_fk_column = rel.far_fk_column.as_str();
1819 let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1820 let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1821 let doc = format!(
1822 "Eloquent `hasManyThrough` accessor — returns a \
1823 `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1824 instance through the intermediate `{intermediate}` table. \
1825 Generated SQL shape: \
1826 `… WHERE {far_fk_column} IN (SELECT \
1827 {intermediate_pk_column} FROM <{intermediate}> WHERE \
1828 {intermediate_fk_column} = self.pk)`. Chainable like any \
1829 other QuerySet — compose `.filter()` / `.order_by()` / \
1830 `.limit()` etc. on top.",
1831 );
1832 let count_doc = format!(
1833 "Eloquent `$model->{name}->count()` analog for the \
1834 through-relation — returns the number of `{far}` rows \
1835 reachable through `{intermediate}`. Equivalent to \
1836 `self.{name}_through().count(pool)` but spelled \
1837 as a bare instance method for parity with the \
1838 `reverse_has` `<name>_count` shape.",
1839 name = rel.name,
1840 );
1841 quote! {
1842 #[doc = #doc]
1843 pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1844 use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1845 let intermediate_schema =
1846 <#intermediate as #root::core::Model>::SCHEMA;
1847 let sub = SelectQuery {
1848 where_clause: WhereExpr::Predicate(Filter {
1849 column: #intermediate_fk_column,
1850 op: Op::Eq,
1851 value: self.__rustango_pk_value(),
1852 }),
1853 projection: ::core::option::Option::Some(
1854 ::std::vec![#intermediate_pk_column],
1855 ),
1856 ..SelectQuery::new(intermediate_schema)
1857 };
1858 #root::query::QuerySet::<#far>::new().where_raw(
1859 WhereExpr::InSubquery {
1860 column: #far_fk_column,
1861 negated: false,
1862 subquery: ::std::boxed::Box::new(sub),
1863 },
1864 )
1865 }
1866
1867 #[doc = #count_doc]
1868 pub async fn #count_ident(
1869 &self,
1870 pool: &#root::sql::Pool,
1871 ) -> ::core::result::Result<
1872 i64,
1873 #root::sql::ExecError,
1874 > {
1875 use #root::sql::CounterPool as _;
1876 self.#method_ident().count(pool).await
1877 }
1878
1879 /// Eloquent `$model->relation->get()` for the
1880 /// through-relation — bare-name hot-path over
1881 /// `self.<name>_through().fetch(&pool)`. Use this
1882 /// when you don't need further `.filter()` /
1883 /// `.order_by()` composition; falls back to the
1884 /// chainable accessor when you do. Avoids the `_pool`
1885 /// suffix on the most common call-site shape.
1886 pub async fn #fetch_ident(
1887 &self,
1888 pool: &#root::sql::Pool,
1889 ) -> ::core::result::Result<
1890 ::std::vec::Vec<#far>,
1891 #root::sql::ExecError,
1892 > {
1893 use #root::sql::FetcherPool as _;
1894 self.#method_ident().fetch(pool).await
1895 }
1896
1897 /// Eloquent `hasOneThrough` analog — bare-name shortcut
1898 /// over `self.<name>_through().first(&pool)`. Returns
1899 /// `None` when no far rows are reachable through the
1900 /// intermediate. Useful when at most one row is
1901 /// expected (latest comment by country, primary tag,
1902 /// etc.).
1903 pub async fn #first_ident(
1904 &self,
1905 pool: &#root::sql::Pool,
1906 ) -> ::core::result::Result<
1907 ::core::option::Option<#far>,
1908 #root::sql::ExecError,
1909 > {
1910 self.#method_ident().first(pool).await
1911 }
1912
1913 /// Pluck a single column from the far rows into `Vec<U>`
1914 /// — cheaper than the typed `<Far>` decode when only one
1915 /// scalar column is needed.
1916 pub async fn #pluck_ident<U>(
1917 &self,
1918 col: &'static str,
1919 pool: &#root::sql::Pool,
1920 ) -> ::core::result::Result<
1921 ::std::vec::Vec<U>,
1922 #root::sql::ExecError,
1923 >
1924 where
1925 U: #root::sql::MaybePgScalar
1926 + #root::sql::MaybeMyScalar
1927 + #root::sql::MaybeSqliteScalar
1928 + ::core::marker::Send
1929 + ::core::marker::Unpin,
1930 {
1931 self.#method_ident()
1932 .values_list_flat(col)
1933 .fetch::<U>(pool)
1934 .await
1935 }
1936 }
1937 });
1938 quote! {
1939 impl #struct_name {
1940 #( #methods )*
1941 }
1942 }
1943}
1944
1945struct ColumnEntry {
1946 /// The struct field ident, used both for the inherent const name on
1947 /// the model and for the inner column type's name.
1948 ident: syn::Ident,
1949 /// The struct's field type, used as `Column::Value`.
1950 value_ty: Type,
1951 /// Rust-side field name (e.g. `"id"`).
1952 name: String,
1953 /// SQL-side column name (e.g. `"user_id"`).
1954 column: String,
1955 /// `#root::core::FieldType::I64` etc.
1956 field_type_tokens: TokenStream2,
1957}
1958
1959struct CollectedFields {
1960 field_schemas: Vec<TokenStream2>,
1961 from_row_inits: Vec<TokenStream2>,
1962 /// Aliased counterparts of `from_row_inits` — read columns via
1963 /// `format!("{prefix}__{col}")` aliases so a Model can be
1964 /// decoded from a JOINed row's projected target columns.
1965 from_aliased_row_inits: Vec<TokenStream2>,
1966 /// Static column-name list — used by the simple insert path
1967 /// (no `Auto<T>` fields). Aligned with `insert_values`.
1968 insert_columns: Vec<TokenStream2>,
1969 /// Static `Into<SqlValue>` expressions, one per field. Aligned
1970 /// with `insert_columns`. Used by the simple insert path only.
1971 insert_values: Vec<TokenStream2>,
1972 /// Per-field push expressions for the dynamic (Auto-aware)
1973 /// insert path. Each statement either unconditionally pushes
1974 /// `(column, value)` or, for an `Auto<T>` field, conditionally
1975 /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
1976 insert_pushes: Vec<TokenStream2>,
1977 /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
1978 /// when `has_auto == false`.
1979 returning_cols: Vec<TokenStream2>,
1980 /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
1981 /// field. Run after `insert_returning` to populate the model.
1982 auto_assigns: Vec<TokenStream2>,
1983 /// `(ident, column_literal)` pairs for every Auto field. Used by
1984 /// the bulk_insert codegen to rebuild assigns against `_row_mut`
1985 /// instead of `self`.
1986 auto_field_idents: Vec<(syn::Ident, String)>,
1987 /// #1028 — `(ident, column)` for each `generated_as` field. Drives
1988 /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
1989 /// back into the struct after insert (MySQL has no RETURNING → the
1990 /// field stays at its placeholder, deferred, matching Django 6.0).
1991 generated_field_idents: Vec<(syn::Ident, String)>,
1992 /// Inner `T` of the first `Auto<T>` field, for the MySQL
1993 /// `LAST_INSERT_ID()` assignment in `AssignAutoPkPool`.
1994 first_auto_value_ty: Option<Type>,
1995 /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
1996 /// by the all-Auto-Unset bulk path (Auto cols dropped from
1997 /// `columns`).
1998 bulk_pushes_no_auto: Vec<TokenStream2>,
1999 /// Bulk-insert per-row pushes for **all fields including Auto**.
2000 /// Used by the all-Auto-Set bulk path (Auto col included with the
2001 /// caller-supplied value).
2002 bulk_pushes_all: Vec<TokenStream2>,
2003 /// Column-name literals for non-Auto fields only (paired with
2004 /// `bulk_pushes_no_auto`).
2005 bulk_columns_no_auto: Vec<TokenStream2>,
2006 /// Column-name literals for every field including Auto (paired
2007 /// with `bulk_pushes_all`).
2008 bulk_columns_all: Vec<TokenStream2>,
2009 /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2010 /// + the loop that asserts every row matches. One pair per Auto
2011 /// field. Empty when `has_auto == false`.
2012 bulk_auto_uniformity: Vec<TokenStream2>,
2013 /// Identifier of the first Auto field, used as the witness for
2014 /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2015 first_auto_ident: Option<syn::Ident>,
2016 /// `true` if any field on the struct is `Auto<T>`.
2017 has_auto: bool,
2018 /// `true` when the primary-key field's Rust type is `Auto<T>`.
2019 /// Gates `save()` codegen — only Auto PKs let us infer
2020 /// insert-vs-update from the in-memory value.
2021 pk_is_auto: bool,
2022 /// `Assignment` constructors for every non-PK column. Drives the
2023 /// UPDATE branch of `save()`.
2024 update_assignments: Vec<TokenStream2>,
2025 /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2026 /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2027 upsert_update_columns: Vec<TokenStream2>,
2028 primary_key: Option<(syn::Ident, String)>,
2029 column_entries: Vec<ColumnEntry>,
2030 /// Rust-side field names, in declaration order. Used to validate
2031 /// container attributes like `display = "…"`.
2032 field_names: Vec<String>,
2033 /// FK fields on this child model. Drives the reverse-relation
2034 /// helper emit — for each FK, the macro adds an inherent
2035 /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2036 /// method on the parent type.
2037 fk_relations: Vec<FkRelation>,
2038 /// SQL column name of the `#[rustango(soft_delete)]` field, if
2039 /// the model has one. Drives emission of the `soft_delete_on` /
2040 /// `restore_on` inherent methods. At most one such column per
2041 /// model is allowed; collect_fields rejects duplicates.
2042 soft_delete_column: Option<String>,
2043 /// Rust field ident of the `#[rustango(soft_delete)]` field —
2044 /// companion to `soft_delete_column` for emitting predicates
2045 /// that need to read the field off `&self` (e.g. `trashed()`).
2046 soft_delete_field_ident: Option<syn::Ident>,
2047}
2048
2049#[derive(Clone)]
2050struct FkRelation {
2051 /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2052 /// helper is emitted as `impl <ParentType> { … }`.
2053 parent_type: Type,
2054 /// SQL column name on the child table for this FK (e.g. `"author"`).
2055 /// Used in the generated `WHERE <fk_column> = $1` clause.
2056 fk_column: String,
2057 /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2058 /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2059 /// default `ForeignKey<T>` (no explicit K); other kinds when the
2060 /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2061 pk_kind: DetectedKind,
2062 /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2063 /// FK column). Drives the `Some(...)` wrapping in load_related
2064 /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2065 /// the codegen matches the field's declared shape.
2066 nullable: bool,
2067 /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2068 /// override. When set, the reverse helper picks this name instead
2069 /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2070 /// #816 (issue's "Related" note re: per-FK override).
2071 related_name: Option<String>,
2072}
2073
2074fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2075 let root = rustango_root();
2076 let cap = named.named.len();
2077 let mut out = CollectedFields {
2078 field_schemas: Vec::with_capacity(cap),
2079 from_row_inits: Vec::with_capacity(cap),
2080 from_aliased_row_inits: Vec::with_capacity(cap),
2081 insert_columns: Vec::with_capacity(cap),
2082 insert_values: Vec::with_capacity(cap),
2083 insert_pushes: Vec::with_capacity(cap),
2084 returning_cols: Vec::new(),
2085 auto_assigns: Vec::new(),
2086 auto_field_idents: Vec::new(),
2087 generated_field_idents: Vec::new(),
2088 first_auto_value_ty: None,
2089 bulk_pushes_no_auto: Vec::with_capacity(cap),
2090 bulk_pushes_all: Vec::with_capacity(cap),
2091 bulk_columns_no_auto: Vec::with_capacity(cap),
2092 bulk_columns_all: Vec::with_capacity(cap),
2093 bulk_auto_uniformity: Vec::new(),
2094 first_auto_ident: None,
2095 has_auto: false,
2096 pk_is_auto: false,
2097 update_assignments: Vec::with_capacity(cap),
2098 upsert_update_columns: Vec::with_capacity(cap),
2099 primary_key: None,
2100 column_entries: Vec::with_capacity(cap),
2101 field_names: Vec::with_capacity(cap),
2102 fk_relations: Vec::new(),
2103 soft_delete_column: None,
2104 soft_delete_field_ident: None,
2105 };
2106
2107 for field in &named.named {
2108 let info = process_field(field, table)?;
2109 out.field_names.push(info.ident.to_string());
2110 out.field_schemas.push(info.schema);
2111 out.from_row_inits.push(info.from_row_init);
2112 out.from_aliased_row_inits.push(info.from_aliased_row_init);
2113 if let Some(parent_ty) = info.fk_inner.clone() {
2114 out.fk_relations.push(FkRelation {
2115 parent_type: parent_ty,
2116 fk_column: info.column.clone(),
2117 pk_kind: info.fk_pk_kind,
2118 nullable: info.nullable,
2119 related_name: info.related_name.clone(),
2120 });
2121 }
2122 if info.soft_delete {
2123 if out.soft_delete_column.is_some() {
2124 return Err(syn::Error::new_spanned(
2125 field,
2126 "only one field may be marked `#[rustango(soft_delete)]`",
2127 ));
2128 }
2129 out.soft_delete_column = Some(info.column.clone());
2130 out.soft_delete_field_ident = Some(info.ident.clone());
2131 }
2132 let column = info.column.as_str();
2133 let ident = info.ident;
2134 // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2135 // skip every write path — Postgres recomputes the value
2136 // from EXPR. Push only the column-entry record (so typed
2137 // column constants still exist for filtering / projection)
2138 // and the schema literal (already pushed above) and move
2139 // on. No insert_columns/values, no insert_pushes, no
2140 // bulk_*, no update_assignments, no upsert_update_columns,
2141 // no returning_cols.
2142 if info.generated_as.is_some() {
2143 out.column_entries.push(ColumnEntry {
2144 ident: ident.clone(),
2145 value_ty: info.value_ty.clone(),
2146 name: ident.to_string(),
2147 column: info.column.clone(),
2148 field_type_tokens: info.field_type_tokens,
2149 });
2150 // #1028 — refresh the DB-computed value after insert on
2151 // RETURNING-capable backends. The column joins `returning_cols`
2152 // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2153 // ident is recorded so the `AssignAutoPkPool` impl decodes it
2154 // back into the struct. MySQL has no `INSERT … RETURNING`, so
2155 // it keeps the placeholder (deferred refresh, matching Django).
2156 out.returning_cols.push(quote!(#column));
2157 out.generated_field_idents
2158 .push((ident.clone(), info.column.clone()));
2159 continue;
2160 }
2161 out.insert_columns.push(quote!(#column));
2162 out.insert_values.push(quote! {
2163 ::core::convert::Into::<#root::core::SqlValue>::into(
2164 ::core::clone::Clone::clone(&self.#ident)
2165 )
2166 });
2167 if info.auto {
2168 out.has_auto = true;
2169 if out.first_auto_ident.is_none() {
2170 out.first_auto_ident = Some(ident.clone());
2171 out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
2172 }
2173 // `default_uuid_v7` (issue #823) generates the PK Rust-side
2174 // before binding, so the value is already in
2175 // `self.#ident` after the insert_push — RETURNING is
2176 // redundant. Skip adding this column to returning_cols /
2177 // auto_assigns / auto_field_idents to avoid (a) an
2178 // unnecessary RETURNING column on every dialect, and (b)
2179 // the MySQL `LAST_INSERT_ID()` path that can only fill an
2180 // integer PK.
2181 if !info.default_uuid_v7 {
2182 out.returning_cols.push(quote!(#column));
2183 out.auto_field_idents
2184 .push((ident.clone(), info.column.clone()));
2185 out.auto_assigns.push(quote! {
2186 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2187 });
2188 }
2189 if info.default_uuid_v7 {
2190 // Rust-side UUIDv7 generation (issue #823, Eloquent
2191 // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2192 // and bind; Auto::Set → bind the user's value. The
2193 // column is ALWAYS present in the INSERT statement —
2194 // no RETURNING / no DB DEFAULT needed.
2195 out.insert_pushes.push(quote! {
2196 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2197 self.#ident = #root::sql::Auto::Set(
2198 #root::__uuid::Uuid::now_v7(),
2199 );
2200 }
2201 if let #root::sql::Auto::Set(_v) = &self.#ident {
2202 _columns.push(#column);
2203 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2204 ::core::clone::Clone::clone(_v)
2205 ));
2206 }
2207 });
2208 } else {
2209 out.insert_pushes.push(quote! {
2210 if let #root::sql::Auto::Set(_v) = &self.#ident {
2211 _columns.push(#column);
2212 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2213 ::core::clone::Clone::clone(_v)
2214 ));
2215 }
2216 });
2217 }
2218 // Bulk: Auto fields appear only in the all-Set path,
2219 // never in the Unset path (we drop them from `columns`).
2220 out.bulk_columns_all.push(quote!(#column));
2221 out.bulk_pushes_all.push(quote! {
2222 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2223 ::core::clone::Clone::clone(&_row.#ident)
2224 ));
2225 });
2226 // Uniformity check: every row's Auto state must match the
2227 // first row's. Mixed Set/Unset within one bulk_insert is
2228 // rejected here so the column list stays consistent.
2229 let ident_clone = ident.clone();
2230 out.bulk_auto_uniformity.push(quote! {
2231 for _r in rows.iter().skip(1) {
2232 if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2233 return ::core::result::Result::Err(
2234 #root::sql::ExecError::Sql(
2235 #root::sql::SqlError::BulkAutoMixed
2236 )
2237 );
2238 }
2239 }
2240 });
2241 } else {
2242 out.insert_pushes.push(quote! {
2243 _columns.push(#column);
2244 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2245 ::core::clone::Clone::clone(&self.#ident)
2246 ));
2247 });
2248 // Bulk: non-Auto fields appear in BOTH paths.
2249 out.bulk_columns_no_auto.push(quote!(#column));
2250 out.bulk_columns_all.push(quote!(#column));
2251 let push_expr = quote! {
2252 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2253 ::core::clone::Clone::clone(&_row.#ident)
2254 ));
2255 };
2256 out.bulk_pushes_no_auto.push(push_expr.clone());
2257 out.bulk_pushes_all.push(push_expr);
2258 }
2259 if info.primary_key {
2260 if out.primary_key.is_some() {
2261 return Err(syn::Error::new_spanned(
2262 field,
2263 "only one field may be marked `#[rustango(primary_key)]`",
2264 ));
2265 }
2266 out.primary_key = Some((ident.clone(), info.column.clone()));
2267 if info.auto {
2268 out.pk_is_auto = true;
2269 }
2270 } else if info.auto_now_add {
2271 // Immutable post-insert: skip from UPDATE entirely.
2272 } else if info.auto_now {
2273 // `auto_now` columns: bind `chrono::Utc::now()` on every
2274 // UPDATE so the column is always overridden with the
2275 // wall-clock at write time, regardless of what value the
2276 // user left in the struct field.
2277 out.update_assignments.push(quote! {
2278 #root::core::Assignment {
2279 column: #column,
2280 value: ::core::convert::Into::<#root::core::Expr>::into(
2281 ::core::convert::Into::<#root::core::SqlValue>::into(
2282 #root::__chrono::Utc::now()
2283 )
2284 ),
2285 }
2286 });
2287 out.upsert_update_columns.push(quote!(#column));
2288 } else {
2289 out.update_assignments.push(quote! {
2290 #root::core::Assignment {
2291 column: #column,
2292 value: ::core::convert::Into::<#root::core::Expr>::into(
2293 ::core::convert::Into::<#root::core::SqlValue>::into(
2294 ::core::clone::Clone::clone(&self.#ident)
2295 )
2296 ),
2297 }
2298 });
2299 out.upsert_update_columns.push(quote!(#column));
2300 }
2301 out.column_entries.push(ColumnEntry {
2302 ident: ident.clone(),
2303 value_ty: info.value_ty.clone(),
2304 name: ident.to_string(),
2305 column: info.column.clone(),
2306 field_type_tokens: info.field_type_tokens,
2307 });
2308 }
2309 Ok(out)
2310}
2311
2312fn model_impl_tokens(
2313 struct_name: &syn::Ident,
2314 model_name: &str,
2315 table: &str,
2316 display: Option<&str>,
2317 app_label: Option<&str>,
2318 admin: Option<&AdminAttrs>,
2319 default_order: &[(String, bool, proc_macro2::Span)],
2320 field_schemas: &[TokenStream2],
2321 soft_delete_column: Option<&str>,
2322 permissions: bool,
2323 audit_track: Option<&[String]>,
2324 m2m_relations: &[M2MAttr],
2325 indexes: &[IndexAttr],
2326 checks: &[CheckAttr],
2327 excludes: &[ExcludeAttr],
2328 composite_fks: &[CompositeFkAttr],
2329 generic_fks: &[GenericFkAttr],
2330 scope: Option<&str>,
2331 is_view: bool,
2332 verbose_name: Option<&str>,
2333 verbose_name_plural: Option<&str>,
2334 managed: bool,
2335 base_manager_name: Option<&str>,
2336 order_with_respect_to: Option<&str>,
2337 proxy: bool,
2338 required_db_features: &[String],
2339 required_db_vendor: Option<&str>,
2340 default_related_name: Option<&str>,
2341 db_table_comment: Option<&str>,
2342 get_latest_by: Option<(&str, bool)>,
2343 extra_permissions: &[(String, String)],
2344 default_permissions: &[String],
2345 global_scopes: &[GlobalScopeAttr],
2346 reverse_has_relations: &[ReverseHasAttr],
2347 generic_has_relations: &[GenericHasAttr],
2348) -> TokenStream2 {
2349 let root = rustango_root();
2350 let display_tokens = if let Some(name) = display {
2351 quote!(::core::option::Option::Some(#name))
2352 } else {
2353 quote!(::core::option::Option::None)
2354 };
2355 let app_label_tokens = if let Some(name) = app_label {
2356 quote!(::core::option::Option::Some(#name))
2357 } else {
2358 quote!(::core::option::Option::None)
2359 };
2360 let soft_delete_tokens = if let Some(col) = soft_delete_column {
2361 quote!(::core::option::Option::Some(#col))
2362 } else {
2363 quote!(::core::option::Option::None)
2364 };
2365 let audit_track_tokens = match audit_track {
2366 None => quote!(::core::option::Option::None),
2367 Some(names) => {
2368 let lits = names.iter().map(|n| n.as_str());
2369 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2370 }
2371 };
2372 let admin_tokens = admin_config_tokens(admin);
2373 // Default `tenant` so single-tenant projects (no `scope` attr
2374 // anywhere) keep the v0.24.x behavior. Container-attr parser
2375 // already validated the value is "registry" or "tenant".
2376 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2377 Some("registry") => quote!(#root::core::ModelScope::Registry),
2378 _ => quote!(#root::core::ModelScope::Tenant),
2379 };
2380 let verbose_name_tokens = optional_str(verbose_name);
2381 let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2382 let base_manager_name_tokens = optional_str(base_manager_name);
2383 let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2384 let required_db_features_lits: Vec<&str> =
2385 required_db_features.iter().map(String::as_str).collect();
2386 let required_db_vendor_tokens = optional_str(required_db_vendor);
2387 let default_related_name_tokens = optional_str(default_related_name);
2388 let db_table_comment_tokens = optional_str(db_table_comment);
2389 let get_latest_by_tokens = match get_latest_by {
2390 Some((col, desc)) => {
2391 quote!(::core::option::Option::Some((#col, #desc)))
2392 }
2393 None => quote!(::core::option::Option::None),
2394 };
2395 let extra_permission_tokens: Vec<_> = extra_permissions
2396 .iter()
2397 .map(|(c, l)| quote!((#c, #l)))
2398 .collect();
2399 let default_permission_tokens: Vec<_> = default_permissions
2400 .iter()
2401 .map(|action| quote!(#action))
2402 .collect();
2403 let indexes_tokens = indexes.iter().map(|idx| {
2404 // When no explicit `name = "..."` was given, derive a stable,
2405 // collision-free name from the table + columns (Django-shape
2406 // `<table>_<col>_<col>_idx`) instead of a shared literal that
2407 // would clash the moment a model declares two unnamed indexes.
2408 // Capped at Postgres's 63-char identifier limit.
2409 let derived_name = idx.name.clone().unwrap_or_else(|| {
2410 let mut n = format!("{table}_{}_idx", idx.columns.join("_"));
2411 if n.len() > 63 {
2412 n.truncate(63);
2413 }
2414 n
2415 });
2416 let name = derived_name.as_str();
2417 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2418 let unique = idx.unique;
2419 // Map the parsed method string onto the IndexMethod enum
2420 // variant — kept at the codegen layer so the IR doesn't
2421 // carry the string form.
2422 let method_variant = match idx.method.as_str() {
2423 "gin" => quote!(#root::core::IndexMethod::Gin),
2424 "gist" => quote!(#root::core::IndexMethod::Gist),
2425 "brin" => quote!(#root::core::IndexMethod::Brin),
2426 "spgist" => quote!(#root::core::IndexMethod::SpGist),
2427 "hash" => quote!(#root::core::IndexMethod::Hash),
2428 "bloom" => quote!(#root::core::IndexMethod::Bloom),
2429 _ => quote!(#root::core::IndexMethod::BTree),
2430 };
2431 let where_clause = match &idx.where_clause {
2432 Some(s) => quote!(::core::option::Option::Some(#s)),
2433 None => quote!(::core::option::Option::None),
2434 };
2435 let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2436 quote! {
2437 #root::core::IndexSchema {
2438 name: #name,
2439 columns: &[ #(#cols),* ],
2440 unique: #unique,
2441 method: #method_variant,
2442 where_clause: #where_clause,
2443 include: &[ #(#include_lits),* ],
2444 }
2445 }
2446 });
2447 let checks_tokens = checks.iter().map(|c| {
2448 let name = c.name.as_str();
2449 let expr = c.expr.as_str();
2450 quote! {
2451 #root::core::CheckConstraint {
2452 name: #name,
2453 expr: #expr,
2454 }
2455 }
2456 });
2457 let excludes_tokens = excludes.iter().map(|e| {
2458 let name = e.name.as_str();
2459 let using = e.using.as_str();
2460 let element_tokens = e.elements.iter().map(|(col, op)| {
2461 let col_s = col.as_str();
2462 let op_s = op.as_str();
2463 quote!((#col_s, #op_s))
2464 });
2465 let where_tokens = match e.where_clause.as_deref() {
2466 Some(w) => quote!(::core::option::Option::Some(#w)),
2467 None => quote!(::core::option::Option::None),
2468 };
2469 quote! {
2470 #root::core::ExclusionConstraint {
2471 name: #name,
2472 using: #using,
2473 elements: &[ #(#element_tokens),* ],
2474 where_clause: #where_tokens,
2475 }
2476 }
2477 });
2478 let composite_fk_tokens = composite_fks.iter().map(|rel| {
2479 let name = rel.name.as_str();
2480 let to = rel.to.as_str();
2481 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2482 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2483 quote! {
2484 #root::core::CompositeFkRelation {
2485 name: #name,
2486 to: #to,
2487 from: &[ #(#from_cols),* ],
2488 on: &[ #(#on_cols),* ],
2489 }
2490 }
2491 });
2492 let generic_fk_tokens = generic_fks.iter().map(|rel| {
2493 let name = rel.name.as_str();
2494 let ct_col = rel.ct_column.as_str();
2495 let pk_col = rel.pk_column.as_str();
2496 quote! {
2497 #root::core::GenericRelation {
2498 name: #name,
2499 ct_column: #ct_col,
2500 pk_column: #pk_col,
2501 }
2502 }
2503 });
2504 // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2505 // no `#[rustango(default_order = "...")]` attribute was supplied.
2506 let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2507 let col_lit = col.as_str();
2508 quote! { (#col_lit, #desc) }
2509 });
2510
2511 // Issue #820 — `global_scopes` slice literal. Empty when no
2512 // `#[rustango(global_scope(...))]` attribute was supplied. The
2513 // `apply` path is re-emitted verbatim so it resolves in the
2514 // consumer's scope; the name is stored as a string literal.
2515 let global_scope_tokens = global_scopes.iter().map(|s| {
2516 let name = s.name.as_str();
2517 let apply = &s.apply;
2518 quote! {
2519 #root::core::GlobalScope {
2520 name: #name,
2521 apply: #apply,
2522 }
2523 }
2524 });
2525
2526 let m2m_tokens = m2m_relations.iter().map(|rel| {
2527 let name = rel.name.as_str();
2528 let to = rel.to.as_str();
2529 let through = rel.through.as_str();
2530 let src = rel.src.as_str();
2531 let dst = rel.dst.as_str();
2532 let auto_create = rel.auto_create;
2533 quote! {
2534 #root::core::M2MRelation {
2535 name: #name,
2536 to: #to,
2537 through: #through,
2538 src_col: #src,
2539 dst_col: #dst,
2540 auto_create: #auto_create,
2541 }
2542 }
2543 });
2544 // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2545 // when the model declares `#[rustango(reverse_has(...))]`. Each
2546 // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2547 // a const expression. Models without reverse_has fall through to
2548 // the trait's empty default — no override emitted.
2549 let reverse_relations_override = if reverse_has_relations.is_empty() {
2550 quote!()
2551 } else {
2552 let entries = reverse_has_relations.iter().map(|rel| {
2553 let name = rel.name.as_str();
2554 let child = &rel.child;
2555 let child_fk_column = rel.child_fk_column.as_str();
2556 let self_pk_column = rel.self_pk_column.as_str();
2557 quote! {
2558 #root::core::ReverseRelation {
2559 name: #name,
2560 child_schema: <#child as #root::core::Model>::SCHEMA,
2561 child_fk_column: #child_fk_column,
2562 self_pk_column: #self_pk_column,
2563 }
2564 }
2565 });
2566 quote! {
2567 fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2568 const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2569 RELS
2570 }
2571 }
2572 };
2573 // Issue #830 — `Model::generic_reverse_relations()` override for
2574 // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2575 let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2576 quote!()
2577 } else {
2578 let entries = generic_has_relations.iter().map(|rel| {
2579 let name = rel.name.as_str();
2580 let child = &rel.child;
2581 let ct_column = rel.ct_column.as_str();
2582 let pk_column = rel.pk_column.as_str();
2583 let self_pk_column = rel.self_pk_column.as_str();
2584 quote! {
2585 #root::core::GenericReverseRelation {
2586 name: #name,
2587 child_schema: <#child as #root::core::Model>::SCHEMA,
2588 ct_column: #ct_column,
2589 pk_column: #pk_column,
2590 self_pk_column: #self_pk_column,
2591 }
2592 }
2593 });
2594 quote! {
2595 fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2596 const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2597 RELS
2598 }
2599 }
2600 };
2601 quote! {
2602 impl #root::core::Model for #struct_name {
2603 const SCHEMA: &'static #root::core::ModelSchema = &#root::core::ModelSchema {
2604 name: #model_name,
2605 table: #table,
2606 fields: &[ #(#field_schemas),* ],
2607 display: #display_tokens,
2608 app_label: #app_label_tokens,
2609 admin: #admin_tokens,
2610 soft_delete_column: #soft_delete_tokens,
2611 permissions: #permissions,
2612 audit_track: #audit_track_tokens,
2613 m2m: &[ #(#m2m_tokens),* ],
2614 indexes: &[ #(#indexes_tokens),* ],
2615 check_constraints: &[ #(#checks_tokens),* ],
2616 exclusion_constraints: &[ #(#excludes_tokens),* ],
2617 composite_relations: &[ #(#composite_fk_tokens),* ],
2618 generic_relations: &[ #(#generic_fk_tokens),* ],
2619 scope: #scope_tokens,
2620 default_order: &[ #(#default_order_tokens),* ],
2621 is_view: #is_view,
2622 verbose_name: #verbose_name_tokens,
2623 verbose_name_plural: #verbose_name_plural_tokens,
2624 managed: #managed,
2625 base_manager_name: #base_manager_name_tokens,
2626 order_with_respect_to: #order_with_respect_to_tokens,
2627 proxy: #proxy,
2628 required_db_features: &[ #(#required_db_features_lits),* ],
2629 required_db_vendor: #required_db_vendor_tokens,
2630 default_related_name: #default_related_name_tokens,
2631 db_table_comment: #db_table_comment_tokens,
2632 get_latest_by: #get_latest_by_tokens,
2633 extra_permissions: &[ #(#extra_permission_tokens),* ],
2634 default_permissions: &[ #(#default_permission_tokens),* ],
2635 global_scopes: &[ #(#global_scope_tokens),* ],
2636 };
2637
2638 #reverse_relations_override
2639 #generic_reverse_relations_override
2640 }
2641 }
2642}
2643
2644/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2645/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2646/// otherwise a static reference to a populated `AdminConfig`.
2647fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2648 let root = rustango_root();
2649 let Some(admin) = admin else {
2650 return quote!(::core::option::Option::None);
2651 };
2652
2653 let list_display = admin
2654 .list_display
2655 .as_ref()
2656 .map(|(v, _)| v.as_slice())
2657 .unwrap_or(&[]);
2658 let list_display_lits = list_display.iter().map(|s| s.as_str());
2659
2660 let search_fields = admin
2661 .search_fields
2662 .as_ref()
2663 .map(|(v, _)| v.as_slice())
2664 .unwrap_or(&[]);
2665 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2666
2667 let readonly_fields = admin
2668 .readonly_fields
2669 .as_ref()
2670 .map(|(v, _)| v.as_slice())
2671 .unwrap_or(&[]);
2672 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2673
2674 let list_filter = admin
2675 .list_filter
2676 .as_ref()
2677 .map(|(v, _)| v.as_slice())
2678 .unwrap_or(&[]);
2679 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2680
2681 let actions = admin
2682 .actions
2683 .as_ref()
2684 .map(|(v, _)| v.as_slice())
2685 .unwrap_or(&[]);
2686 let actions_lits = actions.iter().map(|s| s.as_str());
2687
2688 let fieldsets = admin
2689 .fieldsets
2690 .as_ref()
2691 .map(|(v, _)| v.as_slice())
2692 .unwrap_or(&[]);
2693 let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
2694 let title = title.as_str();
2695 let field_lits = fields.iter().map(|s| s.as_str());
2696 quote!(#root::core::Fieldset {
2697 title: #title,
2698 fields: &[ #( #field_lits ),* ],
2699 })
2700 });
2701
2702 let list_display_links = admin
2703 .list_display_links
2704 .as_ref()
2705 .map(|(v, _)| v.as_slice())
2706 .unwrap_or(&[]);
2707 let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2708
2709 let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2710 let actions_on_top = admin.actions_on_top.unwrap_or(true);
2711 let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2712 let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2713
2714 let prepopulated = admin
2715 .prepopulated_fields
2716 .as_ref()
2717 .map(|(v, _)| v.as_slice())
2718 .unwrap_or(&[]);
2719 let prepopulated_tokens = prepopulated.iter().map(|(target, sources)| {
2720 let target = target.as_str();
2721 let source_lits = sources.iter().map(|s| s.as_str());
2722 quote!(#root::core::PrepopulatedField {
2723 target: #target,
2724 sources: &[ #( #source_lits ),* ],
2725 })
2726 });
2727
2728 let raw_id_fields = admin
2729 .raw_id_fields
2730 .as_ref()
2731 .map(|(v, _)| v.as_slice())
2732 .unwrap_or(&[]);
2733 let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2734
2735 let autocomplete_fields = admin
2736 .autocomplete_fields
2737 .as_ref()
2738 .map(|(v, _)| v.as_slice())
2739 .unwrap_or(&[]);
2740 let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
2741
2742 // #352 — list_select_related accepts "all" | "none" | "field, field, …".
2743 let list_select_related_tokens = match admin.list_select_related.as_deref() {
2744 None | Some("all") => quote!(#root::core::ListSelectRelated::All),
2745 Some("none") => quote!(#root::core::ListSelectRelated::None),
2746 Some(raw) => {
2747 let names: Vec<&str> = raw
2748 .split(',')
2749 .map(str::trim)
2750 .filter(|s| !s.is_empty())
2751 .collect();
2752 quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
2753 }
2754 };
2755
2756 // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
2757 // a Vec<(String, String)>. Empty / unset → no overrides.
2758 let formfield_pairs: Vec<(&str, &str)> = admin
2759 .formfield_overrides
2760 .as_ref()
2761 .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
2762 .unwrap_or_default();
2763 let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
2764 let field = *field;
2765 let widget = *widget;
2766 quote!((#field, #widget))
2767 });
2768
2769 let list_per_page = admin.list_per_page.unwrap_or(0);
2770
2771 let ordering_pairs = admin
2772 .ordering
2773 .as_ref()
2774 .map(|(v, _)| v.as_slice())
2775 .unwrap_or(&[]);
2776 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
2777 let name = name.as_str();
2778 let desc = *desc;
2779 quote!((#name, #desc))
2780 });
2781
2782 quote! {
2783 ::core::option::Option::Some(&#root::core::AdminConfig {
2784 list_display: &[ #( #list_display_lits ),* ],
2785 search_fields: &[ #( #search_fields_lits ),* ],
2786 list_per_page: #list_per_page,
2787 ordering: &[ #( #ordering_tokens ),* ],
2788 readonly_fields: &[ #( #readonly_fields_lits ),* ],
2789 list_filter: &[ #( #list_filter_lits ),* ],
2790 actions: &[ #( #actions_lits ),* ],
2791 fieldsets: &[ #( #fieldset_tokens ),* ],
2792 list_display_links: &[ #( #list_display_links_lits ),* ],
2793 search_help_text: #search_help_text,
2794 actions_on_top: #actions_on_top,
2795 actions_on_bottom: #actions_on_bottom,
2796 date_hierarchy: #date_hierarchy,
2797 prepopulated_fields: &[ #( #prepopulated_tokens ),* ],
2798 raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
2799 autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
2800 list_select_related: #list_select_related_tokens,
2801 formfield_overrides: &[ #( #formfield_tokens ),* ],
2802 })
2803 }
2804}
2805
2806fn inherent_impl_tokens(
2807 struct_name: &syn::Ident,
2808 fields: &CollectedFields,
2809 primary_key: Option<&(syn::Ident, String)>,
2810 column_consts: &TokenStream2,
2811 audited_fields: Option<&[&ColumnEntry]>,
2812 indexes: &[IndexAttr],
2813 manager_fns: &[syn::Ident],
2814) -> TokenStream2 {
2815 let root = rustango_root();
2816 // Audit-emit fragments threaded into write paths. Non-empty only
2817 // when the model carries `#[rustango(audit(...))]`. They reborrow
2818 // `_executor` (a `&mut PgConnection` for audited models — the
2819 // macro switches the signature below) so the data write and the
2820 // audit INSERT both run on the same caller-supplied connection.
2821 let executor_passes_to_data_write = if audited_fields.is_some() {
2822 quote!(&mut *_executor)
2823 } else {
2824 quote!(_executor)
2825 };
2826 let executor_param = if audited_fields.is_some() {
2827 quote!(_executor: &mut #root::sql::sqlx::PgConnection)
2828 } else {
2829 quote!(_executor: _E)
2830 };
2831 let executor_generics = if audited_fields.is_some() {
2832 quote!()
2833 } else {
2834 quote!(<'_c, _E>)
2835 };
2836 let executor_where = if audited_fields.is_some() {
2837 quote!()
2838 } else {
2839 quote! {
2840 where
2841 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
2842 }
2843 };
2844 // For audited models the `_on` methods take `&mut PgConnection`, so
2845 // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
2846 // must acquire a connection first. Non-audited models keep the
2847 // direct delegation since `&PgPool` IS an Executor.
2848 let pool_to_save_on = if audited_fields.is_some() {
2849 quote! {
2850 let mut _conn = pool.acquire().await?;
2851 self.save_on(&mut *_conn).await
2852 }
2853 } else {
2854 quote!(self.save_on(pool).await)
2855 };
2856 let pool_to_insert_on = if audited_fields.is_some() {
2857 quote! {
2858 let mut _conn = pool.acquire().await?;
2859 self.insert_on(&mut *_conn).await
2860 }
2861 } else {
2862 quote!(self.insert_on(pool).await)
2863 };
2864 let pool_to_delete_on = if audited_fields.is_some() {
2865 quote! {
2866 let mut _conn = pool.acquire().await?;
2867 self.delete_on(&mut *_conn).await
2868 }
2869 } else {
2870 quote!(self.delete_on(pool).await)
2871 };
2872 let pool_to_bulk_insert_on = if audited_fields.is_some() {
2873 quote! {
2874 let mut _conn = pool.acquire().await?;
2875 Self::bulk_insert_on(rows, &mut *_conn).await
2876 }
2877 } else {
2878 quote!(Self::bulk_insert_on(rows, pool).await)
2879 };
2880 // Pre-existing bug surfaced by batch 22's first audited Auto<T>
2881 // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
2882 // directly, but `upsert_on` for audited models takes
2883 // `&mut PgConnection` (the audit emit needs a real connection).
2884 // Add the missing acquire shim to keep audited Auto-PK upsert
2885 // compiling.
2886 let pool_to_upsert_on = if audited_fields.is_some() {
2887 quote! {
2888 let mut _conn = pool.acquire().await?;
2889 self.upsert_on(&mut *_conn).await
2890 }
2891 } else {
2892 quote!(self.upsert_on(pool).await)
2893 };
2894
2895 // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
2896 // (audit-on-connection over &Pool needs a bi-dialect transaction
2897 // helper, deferred). Two body shapes:
2898 // - has_auto: build InsertQuery skipping Auto::Unset columns,
2899 // request Auto cols in `returning`, dispatch via
2900 // `insert_returning_pool`, then on the returned `PgRow` /
2901 // `MySqlAutoId(id)` enum — pull each Auto field from the PG
2902 // row OR drop the single i64 into the first Auto field on MySQL
2903 // (multi-Auto models on MySQL error at runtime since
2904 // `LAST_INSERT_ID()` only reports one)
2905 // - non-Auto: build InsertQuery with explicit columns/values and
2906 // call `insert_pool` (no returning needed)
2907 // pool_insert_method body for the audited Auto-PK case is moved
2908 // to after audit_pair_tokens / audit_pk_to_string (they live
2909 // ~150 lines below). This block keeps the non-audited and
2910 // non-Auto branches in place — the audited Auto-PK arm is
2911 // computed below and merged via the dispatch helper variable.
2912 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
2913 // Audited models with explicit (non-Auto) PKs go through
2914 // the non-Auto insert path below — the audit emit is one
2915 // round-trip after the INSERT inside the same tx via
2916 // audit::save_one_with_audit? No, INSERT semantics
2917 // differ. For non-Auto PK + audited, route through a
2918 // dedicated insert + audit emit on the same tx, but defer
2919 // the macro emission to the audit-bundle-aware block below
2920 // — this `quote!()` placeholder gets overwritten there.
2921 quote!()
2922 } else if audited_fields.is_some() && fields.has_auto {
2923 // Audited Auto-PK insert_pool — assembled after the audit
2924 // bundles. Placeholder; real emission below.
2925 quote!()
2926 } else if fields.has_auto {
2927 let pushes = &fields.insert_pushes;
2928 let returning_cols = &fields.returning_cols;
2929 // When every `Auto<T>` field is filled Rust-side
2930 // (`default_uuid_v7`, issue #823), there is no column to read
2931 // back from the database — `returning_cols` is empty. Route
2932 // through plain `insert_pool` instead of
2933 // `insert_returning_pool` to skip the redundant RETURNING /
2934 // LAST_INSERT_ID round-trip.
2935 if fields.returning_cols.is_empty() {
2936 quote! {
2937 /// Insert this row against either backend. Every
2938 /// `Auto<T>` PK on this model is filled Rust-side
2939 /// (e.g. `default_uuid_v7`) before binding, so no
2940 /// RETURNING round-trip is needed.
2941 ///
2942 /// # Errors
2943 /// As [`Self::insert`].
2944 pub async fn insert_pool(
2945 &mut self,
2946 pool: &#root::sql::Pool,
2947 ) -> ::core::result::Result<(), #root::sql::ExecError> {
2948 let mut _columns: ::std::vec::Vec<&'static str> =
2949 ::std::vec::Vec::new();
2950 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2951 ::std::vec::Vec::new();
2952 #( #pushes )*
2953 let _query = #root::core::InsertQuery {
2954 model: <Self as #root::core::Model>::SCHEMA,
2955 columns: _columns,
2956 values: _values,
2957 returning: ::std::vec::Vec::new(),
2958 on_conflict: ::core::option::Option::None,
2959 };
2960 #root::sql::insert_pool(pool, &_query).await
2961 }
2962
2963 /// Eloquent `Model::insertOrIgnore()` — same shape
2964 /// as the auto-PK branch above. Returns `Ok(true)`
2965 /// when inserted, `Ok(false)` when a conflict caused
2966 /// the INSERT to silently skip.
2967 ///
2968 /// # Errors
2969 /// As [`Self::insert`].
2970 pub async fn insert_or_ignore(
2971 &mut self,
2972 pool: &#root::sql::Pool,
2973 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
2974 let mut _columns: ::std::vec::Vec<&'static str> =
2975 ::std::vec::Vec::new();
2976 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2977 ::std::vec::Vec::new();
2978 #( #pushes )*
2979 let _query = #root::core::InsertQuery {
2980 model: <Self as #root::core::Model>::SCHEMA,
2981 columns: _columns,
2982 values: _values,
2983 returning: ::std::vec::Vec::new(),
2984 on_conflict: ::core::option::Option::Some(
2985 #root::core::ConflictClause::DoNothing,
2986 ),
2987 };
2988 let dialect = pool.dialect();
2989 let stmt = dialect.compile_insert(&_query)?;
2990 let rows = #root::sql::raw_execute_pool(
2991 pool, &stmt.sql, stmt.params,
2992 ).await?;
2993 ::core::result::Result::Ok(rows > 0)
2994 }
2995 }
2996 } else {
2997 quote! {
2998 /// Insert this row against either backend, populating any
2999 /// `Auto<T>` PK from the auto-assigned value.
3000 ///
3001 /// # Errors
3002 /// As [`Self::insert`].
3003 pub async fn insert_pool(
3004 &mut self,
3005 pool: &#root::sql::Pool,
3006 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3007 let mut _columns: ::std::vec::Vec<&'static str> =
3008 ::std::vec::Vec::new();
3009 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3010 ::std::vec::Vec::new();
3011 #( #pushes )*
3012 let _query = #root::core::InsertQuery {
3013 model: <Self as #root::core::Model>::SCHEMA,
3014 columns: _columns,
3015 values: _values,
3016 returning: ::std::vec![ #( #returning_cols ),* ],
3017 on_conflict: ::core::option::Option::None,
3018 };
3019 let _result = #root::sql::insert_returning_pool(
3020 pool, &_query,
3021 ).await?;
3022 #root::sql::apply_auto_pk(_result, self)
3023 }
3024
3025 /// Eloquent `Model::insertOrIgnore()` — INSERT this
3026 /// row or silently skip on unique-constraint
3027 /// violation. Returns `Ok(true)` when a row was
3028 /// inserted, `Ok(false)` when a conflict caused the
3029 /// INSERT to silently skip.
3030 ///
3031 /// **Caveat on auto-PK models**: when the row is
3032 /// skipped (conflict), this instance's `Auto<T>`
3033 /// fields stay `Unset` — no PK is back-populated
3034 /// because the server didn't auto-assign one. For
3035 /// "insert then read back the PK or the existing
3036 /// row's PK", use the `upsert` family or
3037 /// `get_or_create`.
3038 ///
3039 /// # Errors
3040 /// As [`Self::insert`].
3041 pub async fn insert_or_ignore(
3042 &mut self,
3043 pool: &#root::sql::Pool,
3044 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3045 let mut _columns: ::std::vec::Vec<&'static str> =
3046 ::std::vec::Vec::new();
3047 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3048 ::std::vec::Vec::new();
3049 #( #pushes )*
3050 let _query = #root::core::InsertQuery {
3051 model: <Self as #root::core::Model>::SCHEMA,
3052 columns: _columns,
3053 values: _values,
3054 returning: ::std::vec::Vec::new(),
3055 on_conflict: ::core::option::Option::Some(
3056 #root::core::ConflictClause::DoNothing,
3057 ),
3058 };
3059 let dialect = pool.dialect();
3060 let stmt = dialect.compile_insert(&_query)?;
3061 let rows = #root::sql::raw_execute_pool(
3062 pool, &stmt.sql, stmt.params,
3063 ).await?;
3064 ::core::result::Result::Ok(rows > 0)
3065 }
3066 }
3067 }
3068 } else {
3069 let insert_columns = &fields.insert_columns;
3070 let insert_values = &fields.insert_values;
3071 quote! {
3072 /// Insert this row into its table against either backend.
3073 /// Equivalent to [`Self::insert`] but takes
3074 /// [`#root::sql::Pool`].
3075 ///
3076 /// # Errors
3077 /// As [`Self::insert`].
3078 pub async fn insert_pool(
3079 &self,
3080 pool: &#root::sql::Pool,
3081 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3082 let _query = #root::core::InsertQuery {
3083 model: <Self as #root::core::Model>::SCHEMA,
3084 columns: ::std::vec![ #( #insert_columns ),* ],
3085 values: ::std::vec![ #( #insert_values ),* ],
3086 returning: ::std::vec::Vec::new(),
3087 on_conflict: ::core::option::Option::None,
3088 };
3089 #root::sql::insert_pool(pool, &_query).await
3090 }
3091
3092 /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3093 /// or silently skip on unique-constraint violation. Maps
3094 /// to per-dialect "INSERT ... DO NOTHING on conflict":
3095 /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3096 /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL
3097 /// `INSERT IGNORE INTO …`.
3098 ///
3099 /// Returns `Ok(true)` when a row was inserted,
3100 /// `Ok(false)` when a conflict caused the INSERT to
3101 /// silently skip.
3102 ///
3103 /// Use for "create-if-absent" patterns where you don't
3104 /// need the row back. For "find-or-create with the row
3105 /// returned", use the queryset-level
3106 /// `crate::sql::get_or_create` free function.
3107 ///
3108 /// # Errors
3109 /// As [`Self::insert`], plus any dialect-specific
3110 /// translation error from the ConflictClause writer.
3111 pub async fn insert_or_ignore(
3112 &self,
3113 pool: &#root::sql::Pool,
3114 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3115 let _query = #root::core::InsertQuery {
3116 model: <Self as #root::core::Model>::SCHEMA,
3117 columns: ::std::vec![ #( #insert_columns ),* ],
3118 values: ::std::vec![ #( #insert_values ),* ],
3119 returning: ::std::vec::Vec::new(),
3120 on_conflict: ::core::option::Option::Some(
3121 #root::core::ConflictClause::DoNothing,
3122 ),
3123 };
3124 let dialect = pool.dialect();
3125 let stmt = dialect.compile_insert(&_query)?;
3126 let rows = #root::sql::raw_execute_pool(pool, &stmt.sql, stmt.params).await?;
3127 ::core::result::Result::Ok(rows > 0)
3128 }
3129 }
3130 };
3131
3132 // pool_save_method moved to after audit_pair_tokens /
3133 // audit_pk_to_string (they live ~70 lines below) — needed for
3134 // the audited branch which builds an UpdateQuery + PendingEntry
3135 // and dispatches via audit::save_one_with_audit.
3136
3137 // pool_delete_method moved to after audit_pair_tokens / audit_pk_to_string
3138 // are computed (they live ~80 lines below).
3139
3140 // Build the (column, JSON value) pair list used by every
3141 // snapshot-style audit emission. Reused across delete_on,
3142 // soft_delete_on, restore_on, and (later) bulk paths. Empty
3143 // when the model isn't audited.
3144 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3145 .map(|tracked| {
3146 tracked
3147 .iter()
3148 .map(|c| {
3149 let column_lit = c.column.as_str();
3150 let ident = &c.ident;
3151 quote! {
3152 (
3153 #column_lit,
3154 #root::__serde_json::to_value(&self.#ident)
3155 .unwrap_or(#root::__serde_json::Value::Null),
3156 )
3157 }
3158 })
3159 .collect()
3160 })
3161 .unwrap_or_default();
3162 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3163 if fields.pk_is_auto {
3164 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3165 } else {
3166 quote!(::std::format!("{}", &self.#pk_ident))
3167 }
3168 } else {
3169 quote!(::std::string::String::new())
3170 };
3171 let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
3172 if audited_fields.is_some() {
3173 let pairs = audit_pair_tokens.iter();
3174 let pk_str = audit_pk_to_string.clone();
3175 quote! {
3176 let _audit_entry = #root::audit::PendingEntry {
3177 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3178 entity_pk: #pk_str,
3179 operation: #op_path,
3180 source: #root::audit::current_source(),
3181 changes: #root::audit::snapshot_changes(&[
3182 #( #pairs ),*
3183 ]),
3184 };
3185 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3186 }
3187 } else {
3188 quote!()
3189 }
3190 };
3191 let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create));
3192 let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete));
3193 let audit_softdelete_emit = make_op_emit(quote!(#root::audit::AuditOp::SoftDelete));
3194 let audit_restore_emit = make_op_emit(quote!(#root::audit::AuditOp::Restore));
3195
3196 // `save_pool(&Pool)` — emitted for every model with a PK.
3197 // Audited Auto-PK models are deferred (the Auto::Unset →
3198 // insert_pool path needs the audited-insert flow from a future
3199 // batch). Three body shapes:
3200 // - non-audited, plain PK: build UpdateQuery + dispatch through
3201 // sql::update_pool
3202 // - non-audited, Auto-PK: same, but Auto::Unset routes to
3203 // self.insert_pool which already handles RETURNING / LAST_INSERT_ID
3204 // - audited, plain PK: build UpdateQuery + PendingEntry, dispatch
3205 // through audit::save_one_with_audit (per-backend tx wraps
3206 // UPDATE + audit emit atomically). Snapshot-style audit (post-
3207 // write field values) — diff-style audit (with pre-UPDATE
3208 // SELECT for `before` values) needs per-tracked-column codegen
3209 // that doesn't fit the runtime-helper pattern; legacy &PgPool
3210 // `save` keeps the diff for now.
3211 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3212 let pk_column_lit = pk_col.as_str();
3213 let assignments = &fields.update_assignments;
3214 if audited_fields.is_some() {
3215 if fields.pk_is_auto {
3216 // Auto-PK + audited: defer. The Auto::Unset insert
3217 // path needs a transactional INSERT + LAST_INSERT_ID
3218 // + audit emit flow — that's a follow-up batch.
3219 quote!()
3220 } else {
3221 let pairs = audit_pair_tokens.iter();
3222 let pairs2 = audit_pair_tokens.iter();
3223 let pk_str = audit_pk_to_string.clone();
3224 let pk_str2 = audit_pk_to_string.clone();
3225 quote! {
3226 /// Save (UPDATE) this row against either backend
3227 /// with audit emission inside the same transaction.
3228 /// Bi-dialect counterpart of [`Self::save`] for
3229 /// audited models with non-`Auto<T>` PKs.
3230 ///
3231 /// Captures **post-write** field state (snapshot
3232 /// audit). The legacy &PgPool [`Self::save`]
3233 /// captures BEFORE+AFTER for true diff audit;
3234 /// porting that to the &Pool path needs runtime
3235 /// per-tracked-column decoding and is deferred.
3236 ///
3237 /// # Errors
3238 /// As [`Self::save`].
3239 pub async fn save_pool(
3240 &mut self,
3241 pool: &#root::sql::Pool,
3242 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3243 let _query = #root::core::UpdateQuery {
3244 model: <Self as #root::core::Model>::SCHEMA,
3245 set: ::std::vec![ #( #assignments ),* ],
3246 where_clause: #root::core::WhereExpr::Predicate(
3247 #root::core::Filter {
3248 column: #pk_column_lit,
3249 op: #root::core::Op::Eq,
3250 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3251 ::core::clone::Clone::clone(&self.#pk_ident)
3252 ),
3253 }
3254 ),
3255 };
3256 let _audit_entry = #root::audit::PendingEntry {
3257 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3258 entity_pk: #pk_str,
3259 operation: #root::audit::AuditOp::Update,
3260 source: #root::audit::current_source(),
3261 changes: #root::audit::snapshot_changes(&[
3262 #( #pairs ),*
3263 ]),
3264 };
3265 let _affected = #root::audit::save_one_with_audit(
3266 pool, &_query, &_audit_entry,
3267 ).await?;
3268 ::core::result::Result::Ok(_affected)
3269 }
3270
3271 /// `save_pool` narrowed to a Rust-field allowlist — issue #66
3272 /// (Django `Model.save(update_fields=[...])`).
3273 /// Audit emission shrinks to the same column set so
3274 /// the audit log reflects exactly what was written.
3275 ///
3276 /// # Errors
3277 /// As [`Self::save_pool`], plus
3278 /// [`#root::core::QueryError::UnknownField`] wrapped
3279 /// in `ExecError::Query` for unknown field names.
3280 pub async fn save_partial(
3281 &mut self,
3282 fields: &[&str],
3283 pool: &#root::sql::Pool,
3284 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3285 if fields.is_empty() {
3286 #root::__tracing::warn!(
3287 target: "rustango::save_partial",
3288 model = <Self as #root::core::Model>::SCHEMA.name,
3289 "save_partial called with empty field list — no-op"
3290 );
3291 return ::core::result::Result::Ok(0);
3292 }
3293 let _schema = <Self as #root::core::Model>::SCHEMA;
3294 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3295 ::std::collections::HashSet::with_capacity(fields.len());
3296 for f in fields {
3297 match _schema.field(f) {
3298 ::core::option::Option::Some(fs) => {
3299 _wanted_cols.insert(fs.column);
3300 }
3301 ::core::option::Option::None => {
3302 return ::core::result::Result::Err(
3303 #root::sql::ExecError::Query(
3304 #root::core::QueryError::UnknownField {
3305 model: _schema.name,
3306 field: (*f).to_owned(),
3307 }
3308 )
3309 );
3310 }
3311 }
3312 }
3313 let _full: ::std::vec::Vec<#root::core::Assignment> =
3314 ::std::vec![ #( #assignments ),* ];
3315 let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3316 .into_iter()
3317 .filter(|a| _wanted_cols.contains(a.column))
3318 .collect();
3319 if _filtered.is_empty() {
3320 #root::__tracing::warn!(
3321 target: "rustango::save_partial",
3322 model = _schema.name,
3323 "save_partial: every named field maps to a non-assignable column — no-op"
3324 );
3325 return ::core::result::Result::Ok(0);
3326 }
3327 let _query = #root::core::UpdateQuery {
3328 model: _schema,
3329 set: _filtered,
3330 where_clause: #root::core::WhereExpr::Predicate(
3331 #root::core::Filter {
3332 column: #pk_column_lit,
3333 op: #root::core::Op::Eq,
3334 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3335 ::core::clone::Clone::clone(&self.#pk_ident)
3336 ),
3337 }
3338 ),
3339 };
3340 // Narrow the audit snapshot to the same column set.
3341 let _all_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3342 ::std::vec![ #( #pairs2 ),* ];
3343 let _narrowed: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3344 _all_pairs
3345 .into_iter()
3346 .filter(|(col, _)| _wanted_cols.contains(col))
3347 .collect();
3348 let _audit_entry = #root::audit::PendingEntry {
3349 entity_table: _schema.table,
3350 entity_pk: #pk_str2,
3351 operation: #root::audit::AuditOp::Update,
3352 source: #root::audit::current_source(),
3353 changes: #root::audit::snapshot_changes(&_narrowed),
3354 };
3355 let _affected = #root::audit::save_one_with_audit(
3356 pool, &_query, &_audit_entry,
3357 ).await?;
3358 ::core::result::Result::Ok(_affected)
3359 }
3360
3361 /// Typed-column counterpart of [`Self::save_partial`] —
3362 /// issue #67. `fields` is a tuple of [`Column`]
3363 /// constants whose `Model` matches `Self`; typos and
3364 /// model mismatches surface at *compile time*
3365 /// (`Author::name` inside a `Post::save_partial_typed`
3366 /// call is a type error, no runtime check).
3367 ///
3368 /// ```ignore
3369 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3370 /// ```
3371 ///
3372 /// Lowers to [`Self::save_partial`] under the hood;
3373 /// audit narrowing + every other semantic is identical.
3374 ///
3375 /// [`Column`]: #root::core::Column
3376 ///
3377 /// # Errors
3378 /// As [`Self::save_partial`].
3379 pub async fn save_partial_typed<
3380 L: #root::core::TypedFieldList<Self>,
3381 >(
3382 &mut self,
3383 fields: L,
3384 pool: &#root::sql::Pool,
3385 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3386 let _names = fields.rust_field_names();
3387 let _refs: ::std::vec::Vec<&str> =
3388 _names.iter().copied().collect();
3389 self.save_partial(&_refs, pool).await
3390 }
3391 }
3392 }
3393 } else {
3394 let dispatch_unset = if fields.pk_is_auto {
3395 quote! {
3396 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3397 return self.insert_pool(pool).await.map(|()| 1u64);
3398 }
3399 }
3400 } else {
3401 quote!()
3402 };
3403 quote! {
3404 /// Save this row to its table against either backend.
3405 /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3406 /// `UPDATE` keyed on the PK.
3407 ///
3408 /// # Errors
3409 /// As [`Self::save`].
3410 pub async fn save_pool(
3411 &mut self,
3412 pool: &#root::sql::Pool,
3413 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3414 #dispatch_unset
3415 let _query = #root::core::UpdateQuery {
3416 model: <Self as #root::core::Model>::SCHEMA,
3417 set: ::std::vec![ #( #assignments ),* ],
3418 where_clause: #root::core::WhereExpr::Predicate(
3419 #root::core::Filter {
3420 column: #pk_column_lit,
3421 op: #root::core::Op::Eq,
3422 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3423 ::core::clone::Clone::clone(&self.#pk_ident)
3424 ),
3425 }
3426 ),
3427 };
3428 let _affected = #root::sql::update_pool(pool, &_query).await?;
3429 ::core::result::Result::Ok(_affected)
3430 }
3431
3432 /// Save (UPDATE) only the listed Rust-side fields,
3433 /// leaving every other column untouched. Issue #66 —
3434 /// Django's `Model.save(update_fields=[...])` shape.
3435 ///
3436 /// `fields` are Rust-side struct field names; the macro
3437 /// resolves each to its SQL column. Unknown field
3438 /// names return [`#root::core::QueryError::UnknownField`]
3439 /// wrapped in `ExecError::Query`. An empty list is a
3440 /// no-op (returns `Ok(())` and logs a `tracing::warn!`),
3441 /// matching Django's "nothing to do" semantic.
3442 ///
3443 /// Use this when:
3444 /// * you only mutated a couple of fields on a wide row
3445 /// (avoid re-writing every column on every save), or
3446 /// * two writers diverged after their initial read and
3447 /// you want to preserve the other writer's changes to
3448 /// columns you didn't touch.
3449 ///
3450 /// Auto-PK models with an unset PK return
3451 /// [`#root::core::QueryError::UnknownField`] with
3452 /// field name `<pk>` — `save_partial` is an
3453 /// UPDATE-only path. Call [`Self::insert_pool`]
3454 /// (or [`Self::save_pool`] which dispatches based on
3455 /// PK state) for the INSERT case.
3456 ///
3457 /// # Errors
3458 /// As [`Self::save_pool`], plus `UnknownField` for
3459 /// unknown / empty / Auto-Unset cases.
3460 pub async fn save_partial(
3461 &mut self,
3462 fields: &[&str],
3463 pool: &#root::sql::Pool,
3464 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3465 if fields.is_empty() {
3466 #root::__tracing::warn!(
3467 target: "rustango::save_partial",
3468 model = <Self as #root::core::Model>::SCHEMA.name,
3469 "save_partial called with empty field list — no-op"
3470 );
3471 return ::core::result::Result::Ok(0);
3472 }
3473 let _schema = <Self as #root::core::Model>::SCHEMA;
3474 // Validate field names against the schema.
3475 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3476 ::std::collections::HashSet::with_capacity(fields.len());
3477 for f in fields {
3478 match _schema.field(f) {
3479 ::core::option::Option::Some(fs) => {
3480 _wanted_cols.insert(fs.column);
3481 }
3482 ::core::option::Option::None => {
3483 return ::core::result::Result::Err(
3484 #root::sql::ExecError::Query(
3485 #root::core::QueryError::UnknownField {
3486 model: _schema.name,
3487 field: (*f).to_owned(),
3488 }
3489 )
3490 );
3491 }
3492 }
3493 }
3494 // Build the full assignment vec, then keep only the
3495 // assignments whose column is in `_wanted_cols`.
3496 let _full: ::std::vec::Vec<#root::core::Assignment> =
3497 ::std::vec![ #( #assignments ),* ];
3498 let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3499 .into_iter()
3500 .filter(|a| _wanted_cols.contains(a.column))
3501 .collect();
3502 if _filtered.is_empty() {
3503 // All field names valid, but they all map to
3504 // non-assignable slots (PK column, computed/
3505 // virtual fields, relations without an
3506 // assignment). Same no-op semantic as Django.
3507 #root::__tracing::warn!(
3508 target: "rustango::save_partial",
3509 model = _schema.name,
3510 "save_partial: every named field maps to a non-assignable column — no-op"
3511 );
3512 return ::core::result::Result::Ok(0);
3513 }
3514 let _query = #root::core::UpdateQuery {
3515 model: _schema,
3516 set: _filtered,
3517 where_clause: #root::core::WhereExpr::Predicate(
3518 #root::core::Filter {
3519 column: #pk_column_lit,
3520 op: #root::core::Op::Eq,
3521 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3522 ::core::clone::Clone::clone(&self.#pk_ident)
3523 ),
3524 }
3525 ),
3526 };
3527 let _affected = #root::sql::update_pool(pool, &_query).await?;
3528 ::core::result::Result::Ok(_affected)
3529 }
3530
3531 /// Typed-column counterpart of [`Self::save_partial`] —
3532 /// issue #67. `fields` is a tuple of [`Column`]
3533 /// constants whose `Model` matches `Self`; typos and
3534 /// model mismatches surface at *compile time*
3535 /// (`Author::name` inside a `Post::save_partial_typed`
3536 /// call is a type error, no runtime check).
3537 ///
3538 /// ```ignore
3539 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3540 /// ```
3541 ///
3542 /// Lowers to [`Self::save_partial`] under the hood — the
3543 /// tuple is reduced to a `&[&str]` slice of Rust-side
3544 /// field names and forwarded.
3545 ///
3546 /// [`Column`]: #root::core::Column
3547 ///
3548 /// # Errors
3549 /// As [`Self::save_partial`].
3550 pub async fn save_partial_typed<
3551 L: #root::core::TypedFieldList<Self>,
3552 >(
3553 &mut self,
3554 fields: L,
3555 pool: &#root::sql::Pool,
3556 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3557 let _names = fields.rust_field_names();
3558 let _refs: ::std::vec::Vec<&str> =
3559 _names.iter().copied().collect();
3560 self.save_partial(&_refs, pool).await
3561 }
3562 }
3563 }
3564 } else {
3565 quote!()
3566 };
3567
3568 // Audited `insert_pool` (overrides the placeholder set higher up
3569 // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3570 // audited models get insert_pool routing through
3571 // audit::insert_one_with_audit (per-backend tx wraps INSERT
3572 // + auto-PK readback + audit emit). Snapshot-style audit (the
3573 // PendingEntry's `changes` carries post-write field values).
3574 let pool_insert_method = if audited_fields.is_some() {
3575 if let Some(_) = primary_key {
3576 let pushes = if fields.has_auto {
3577 fields.insert_pushes.clone()
3578 } else {
3579 // For non-Auto-PK models, the macro normally builds
3580 // {columns, values} from fields.insert_columns +
3581 // fields.insert_values rather than insert_pushes.
3582 // Map those into the pushes shape.
3583 fields
3584 .insert_columns
3585 .iter()
3586 .zip(&fields.insert_values)
3587 .map(|(col, val)| {
3588 quote! {
3589 _columns.push(#col);
3590 _values.push(#val);
3591 }
3592 })
3593 .collect()
3594 };
3595 let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
3596 fields.returning_cols.clone()
3597 } else {
3598 // Non-Auto-PK: still need RETURNING something for the
3599 // audit helper's contract (it errors on empty
3600 // returning). Return the PK column so the audit row
3601 // can carry the assigned PK back. Some non-Auto PKs
3602 // are server-side-default (e.g. UUIDv4 default), so
3603 // RETURNING is genuinely useful.
3604 primary_key
3605 .map(|(_, col)| {
3606 let lit = col.as_str();
3607 vec![quote!(#lit)]
3608 })
3609 .unwrap_or_default()
3610 };
3611 let pairs = audit_pair_tokens.iter();
3612 let pk_str = audit_pk_to_string.clone();
3613 quote! {
3614 /// Insert this row against either backend with audit
3615 /// emission inside the same transaction. Bi-dialect
3616 /// counterpart of [`Self::insert`] for audited models.
3617 ///
3618 /// Snapshot-style audit (post-write field values).
3619 ///
3620 /// # Errors
3621 /// As [`Self::insert`].
3622 pub async fn insert_pool(
3623 &mut self,
3624 pool: &#root::sql::Pool,
3625 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3626 let mut _columns: ::std::vec::Vec<&'static str> =
3627 ::std::vec::Vec::new();
3628 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3629 ::std::vec::Vec::new();
3630 #( #pushes )*
3631 let _query = #root::core::InsertQuery {
3632 model: <Self as #root::core::Model>::SCHEMA,
3633 columns: _columns,
3634 values: _values,
3635 returning: ::std::vec![ #( #returning_cols ),* ],
3636 on_conflict: ::core::option::Option::None,
3637 };
3638 let _audit_entry = #root::audit::PendingEntry {
3639 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3640 entity_pk: #pk_str,
3641 operation: #root::audit::AuditOp::Create,
3642 source: #root::audit::current_source(),
3643 changes: #root::audit::snapshot_changes(&[
3644 #( #pairs ),*
3645 ]),
3646 };
3647 let _result = #root::audit::insert_one_with_audit(
3648 pool, &_query, &_audit_entry,
3649 ).await?;
3650 #root::sql::apply_auto_pk(_result, self)
3651 }
3652 }
3653 } else {
3654 quote!()
3655 }
3656 } else {
3657 // Keep the non-audited pool_insert_method we built earlier.
3658 pool_insert_method
3659 };
3660
3661 // Update audited save_pool: now that insert_pool is wired for
3662 // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3663 // insert_pool. Non-audited save_pool already does this.
3664 // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3665 // Replaces the snapshot-only emission with a per-backend transaction
3666 // body that:
3667 // 1. SELECTs the tracked columns by PK (typed Row::try_get per
3668 // column), capturing BEFORE values
3669 // 2. compiles the UPDATE via pool.dialect() and runs it on the tx
3670 // 3. builds AFTER pairs from &self
3671 // 4. diffs BEFORE/AFTER, emits one PendingEntry with
3672 // AuditOp::Update + diff_changes(...) on the same tx connection
3673 // 5. commits
3674 //
3675 // Per-backend arms inline the SQL string + placeholder shape, then
3676 // share the `audit_before_pair_tokens` decoder block (Row::try_get
3677 // is polymorphic over Row type — the same tokens work against
3678 // PgRow and MySqlRow as long as the field's Rust type implements
3679 // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3680 // primitives + chrono/uuid/serde_json::Value all do).
3681 let pool_save_method = if let Some(tracked) = audited_fields {
3682 if let Some((pk_ident, pk_col)) = primary_key {
3683 let pk_column_lit = pk_col.as_str();
3684 // Two iterators — quote!'s `#(#var)*` consumes the
3685 // iterator, and we need to splice the same after-pairs
3686 // sequence into both per-backend arms.
3687 let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
3688 let pk_str = audit_pk_to_string.clone();
3689 // Per-tracked-column BEFORE-pair token list. Each entry
3690 // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3691 // The Row alias resolves to PgRow / MySqlRow per call site,
3692 // so the same template generates both the PG and MySQL bodies.
3693 let mk_before_pairs =
3694 |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
3695 tracked
3696 .iter()
3697 .map(|c| {
3698 let column_lit = c.column.as_str();
3699 let value_ty = &c.value_ty;
3700 quote! {
3701 (
3702 #column_lit,
3703 match #getter::<#value_ty>(
3704 _audit_before_row, #column_lit,
3705 ) {
3706 ::core::result::Result::Ok(v) => {
3707 #root::__serde_json::to_value(&v)
3708 .unwrap_or(#root::__serde_json::Value::Null)
3709 }
3710 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3711 },
3712 )
3713 }
3714 })
3715 .collect()
3716 };
3717 let before_pairs_pg: Vec<proc_macro2::TokenStream> =
3718 mk_before_pairs(quote!(#root::sql::try_get_returning));
3719 let before_pairs_my: Vec<proc_macro2::TokenStream> =
3720 mk_before_pairs(quote!(#root::sql::try_get_returning_my));
3721 let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
3722 mk_before_pairs(quote!(#root::sql::try_get_returning_sqlite));
3723 let pg_select_cols: String = tracked
3724 .iter()
3725 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
3726 .collect::<Vec<_>>()
3727 .join(", ");
3728 let my_select_cols: String = tracked
3729 .iter()
3730 .map(|c| format!("`{}`", c.column.replace('`', "``")))
3731 .collect::<Vec<_>>()
3732 .join(", ");
3733 // SQLite uses double-quote identifier quoting (same as
3734 // Postgres in default config), so the column-list shape
3735 // matches PG.
3736 let sqlite_select_cols: String = pg_select_cols.clone();
3737 let pk_value_for_bind = if fields.pk_is_auto {
3738 quote!(self.#pk_ident.get().copied().unwrap_or_default())
3739 } else {
3740 quote!(::core::clone::Clone::clone(&self.#pk_ident))
3741 };
3742 let assignments = &fields.update_assignments;
3743 let unset_dispatch = if fields.has_auto {
3744 quote! {
3745 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3746 return self.insert_pool(pool).await.map(|()| 1u64);
3747 }
3748 }
3749 } else {
3750 quote!()
3751 };
3752 quote! {
3753 /// Save this row against either backend with audit
3754 /// emission (diff-style: BEFORE+AFTER) inside the
3755 /// same transaction. Auto::Unset PK routes to
3756 /// insert_pool. Bi-dialect counterpart of
3757 /// [`Self::save`] for audited models.
3758 ///
3759 /// The audit row's `changes` JSON contains one
3760 /// `{ "field": { "before": …, "after": … } }` entry
3761 /// per tracked column whose value actually changed
3762 /// — same shape as the existing &PgPool save() emits.
3763 ///
3764 /// # Errors
3765 /// As [`Self::save`].
3766 pub async fn save_pool(
3767 &mut self,
3768 pool: &#root::sql::Pool,
3769 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3770 #unset_dispatch
3771 let _query = #root::core::UpdateQuery {
3772 model: <Self as #root::core::Model>::SCHEMA,
3773 set: ::std::vec![ #( #assignments ),* ],
3774 where_clause: #root::core::WhereExpr::Predicate(
3775 #root::core::Filter {
3776 column: #pk_column_lit,
3777 op: #root::core::Op::Eq,
3778 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3779 ::core::clone::Clone::clone(&self.#pk_ident)
3780 ),
3781 }
3782 ),
3783 };
3784 let _after_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3785 ::std::vec![ #( #after_pairs_pg ),* ];
3786 #root::audit::save_one_with_diff(
3787 pool,
3788 &_query,
3789 #pk_column_lit,
3790 ::core::convert::Into::<#root::core::SqlValue>::into(
3791 #pk_value_for_bind,
3792 ),
3793 <Self as #root::core::Model>::SCHEMA.table,
3794 #pk_str,
3795 _after_pairs,
3796 #pg_select_cols,
3797 #my_select_cols,
3798 #sqlite_select_cols,
3799 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
3800 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
3801 |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
3802 ).await
3803 }
3804 }
3805 } else {
3806 quote!()
3807 }
3808 } else {
3809 pool_save_method
3810 };
3811
3812 // `delete_pool(&Pool)` — emitted for every model with a PK. Two
3813 // body shapes:
3814 // - non-audited: simple dispatch through `sql::delete_pool`
3815 // - audited: routes through `audit::delete_one_with_audit`,
3816 // which opens a per-backend transaction wrapping DELETE +
3817 // audit emit so the data write and audit row commit atomically.
3818 let pool_delete_method = {
3819 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
3820 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
3821 if let Some(pk_ident) = pk_ident_for_pool {
3822 if audited_fields.is_some() {
3823 let pairs = audit_pair_tokens.iter();
3824 let pk_str = audit_pk_to_string.clone();
3825 quote! {
3826 /// Delete this row against either backend with audit
3827 /// emission inside the same transaction. Bi-dialect
3828 /// counterpart of [`Self::delete`] for audited models.
3829 ///
3830 /// # Errors
3831 /// As [`Self::delete`].
3832 pub async fn delete_pool(
3833 &self,
3834 pool: &#root::sql::Pool,
3835 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3836 let _query = #root::core::DeleteQuery {
3837 model: <Self as #root::core::Model>::SCHEMA,
3838 where_clause: #root::core::WhereExpr::Predicate(
3839 #root::core::Filter {
3840 column: #pk_column_lit,
3841 op: #root::core::Op::Eq,
3842 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3843 ::core::clone::Clone::clone(&self.#pk_ident)
3844 ),
3845 }
3846 ),
3847 };
3848 let _audit_entry = #root::audit::PendingEntry {
3849 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3850 entity_pk: #pk_str,
3851 operation: #root::audit::AuditOp::Delete,
3852 source: #root::audit::current_source(),
3853 changes: #root::audit::snapshot_changes(&[
3854 #( #pairs ),*
3855 ]),
3856 };
3857 #root::audit::delete_one_with_audit(
3858 pool, &_query, &_audit_entry,
3859 ).await
3860 }
3861 }
3862 } else {
3863 quote! {
3864 /// Delete the row identified by this instance's primary key
3865 /// against either backend. Equivalent to [`Self::delete`] but
3866 /// takes [`#root::sql::Pool`] and dispatches per backend.
3867 ///
3868 /// # Errors
3869 /// As [`Self::delete`].
3870 pub async fn delete_pool(
3871 &self,
3872 pool: &#root::sql::Pool,
3873 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3874 let _query = #root::core::DeleteQuery {
3875 model: <Self as #root::core::Model>::SCHEMA,
3876 where_clause: #root::core::WhereExpr::Predicate(
3877 #root::core::Filter {
3878 column: #pk_column_lit,
3879 op: #root::core::Op::Eq,
3880 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3881 ::core::clone::Clone::clone(&self.#pk_ident)
3882 ),
3883 }
3884 ),
3885 };
3886 #root::sql::delete_pool(pool, &_query).await
3887 }
3888 }
3889 }
3890 } else {
3891 quote!()
3892 }
3893 };
3894
3895 // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
3896 // matching this instance's PK and overwrite the in-memory state
3897 // with the freshly-fetched columns. Django's `refresh_from_db`.
3898 // Issue #825. Only emitted when the model declares a PK; non-PK
3899 // models can't address a specific row.
3900 //
3901 // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
3902 // every field from `self`; resets the PK to `Auto::Unset` when
3903 // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
3904 // a fresh autoincrement value. Non-Auto PKs preserve the source
3905 // PK — the caller must overwrite before insert. Pure-Rust, no
3906 // I/O, no dialect surface.
3907 let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
3908 let other_field_clones: Vec<TokenStream2> = fields
3909 .column_entries
3910 .iter()
3911 .filter(|c| &c.ident != pk_ident)
3912 .map(|c| {
3913 let ident = &c.ident;
3914 quote! {
3915 #ident: ::core::clone::Clone::clone(&self.#ident)
3916 }
3917 })
3918 .collect();
3919 let pk_clone_token = if fields.pk_is_auto {
3920 quote! { #pk_ident: #root::sql::Auto::Unset }
3921 } else {
3922 quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
3923 };
3924 let replicate_doc = if fields.pk_is_auto {
3925 quote! {
3926 /// Eloquent-style `replicate()` — return a clone of this
3927 /// row with the primary key reset to [`Auto::Unset`] so
3928 /// the copy is ready for `insert_pool` / `save_pool` to
3929 /// allocate a fresh autoincrement value. Every other
3930 /// field is `Clone`d verbatim. Issue #825.
3931 ///
3932 /// `auto_now_add` / `auto_now` timestamp fields are
3933 /// **not** reset (Eloquent's `replicate` doesn't reset
3934 /// them either) — pass them through the normal insert
3935 /// path if you want fresh values, or assign them
3936 /// explicitly after the call.
3937 }
3938 } else {
3939 quote! {
3940 /// Eloquent-style `replicate()` — clone this row
3941 /// verbatim. Because the primary key is **not** an
3942 /// `Auto<T>`, the clone keeps the source PK; the
3943 /// caller must overwrite `copy.<pk>` before inserting
3944 /// to avoid a unique-key violation. Issue #825.
3945 }
3946 };
3947 // 2026-06-07 — field-name / shortcut collision guard.
3948 //
3949 // The macro emits both `pub const <field>: <field>_col = ...`
3950 // (per-field typed-column const, used by the typed-builder
3951 // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
3952 // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
3953 // model has a field named e.g. `count`, both items would land
3954 // in the same inherent impl with the same name, and the
3955 // compiler rejects the derive with "duplicate definitions".
3956 //
3957 // Drop the conflicting shortcut for that model. Callers can
3958 // still reach the same behavior via
3959 // `QuerySet::<Model>::default().count(&pool)`.
3960 let column_names: ::std::collections::HashSet<String> = fields
3961 .column_entries
3962 .iter()
3963 .map(|c| c.ident.to_string())
3964 .collect();
3965 let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
3966 if column_names.contains(name) {
3967 quote! {}
3968 } else {
3969 tokens
3970 }
3971 };
3972 let count_method = emit_if_no_field_collision(
3973 "count",
3974 quote! {
3975 /// Count rows of this model — `SELECT COUNT(*) FROM
3976 /// <table>`. Eloquent `Model::count()` parity.
3977 ///
3978 /// Skipped on models that already declare a field named
3979 /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
3980 /// in that case.
3981 ///
3982 /// # Errors
3983 /// As [`CounterPool::count`].
3984 ///
3985 /// [`CounterPool::count`]: rustango::sql::CounterPool::count
3986 pub async fn count(
3987 pool: &#root::sql::Pool,
3988 ) -> ::core::result::Result<i64, #root::sql::ExecError> {
3989 use #root::sql::CounterPool as _;
3990 #root::query::QuerySet::<Self>::default()
3991 .count(pool)
3992 .await
3993 }
3994 },
3995 );
3996 let value_method = emit_if_no_field_collision(
3997 "value",
3998 quote! {
3999 /// Pluck a single scalar from the first row.
4000 /// Eloquent `Model::query()->value($col)` parity.
4001 ///
4002 /// Skipped on models that already declare a field named
4003 /// `value`. Drop into
4004 /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4005 ///
4006 /// # Errors
4007 /// As `ValuesFlatQuerySet::first`.
4008 pub async fn value<U>(
4009 col: &str,
4010 pool: &#root::sql::Pool,
4011 ) -> ::core::result::Result<
4012 ::core::option::Option<U>,
4013 #root::sql::ExecError,
4014 >
4015 where
4016 U: #root::sql::MaybePgScalar
4017 + #root::sql::MaybeMyScalar
4018 + #root::sql::MaybeSqliteScalar
4019 + ::core::marker::Send
4020 + ::core::marker::Unpin,
4021 {
4022 let _col_static: &'static str = Self::__resolve_col(col)?;
4023 #root::query::QuerySet::<Self>::default()
4024 .values_list_flat(_col_static)
4025 .first::<U>(pool)
4026 .await
4027 }
4028 },
4029 );
4030 let sum_method = emit_if_no_field_collision(
4031 "sum",
4032 quote! {
4033 /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4034 /// Skipped on models that already declare a field named
4035 /// `sum`.
4036 ///
4037 /// # Errors
4038 /// As [`#root::sql::fetch_aggregate_pool`].
4039 pub async fn sum<U>(
4040 col: &str,
4041 pool: &#root::sql::Pool,
4042 ) -> ::core::result::Result<
4043 ::core::option::Option<U>,
4044 #root::sql::ExecError,
4045 >
4046 where
4047 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4048 + #root::sql::MaybeMyFromRow
4049 + #root::sql::MaybeSqliteFromRow
4050 + ::core::marker::Send
4051 + ::core::marker::Unpin,
4052 {
4053 Self::__aggregate_one_pool::<U>(
4054 col,
4055 |c| #root::core::AggregateExpr::Sum(c),
4056 pool,
4057 )
4058 .await
4059 }
4060 },
4061 );
4062 let avg_method = emit_if_no_field_collision(
4063 "avg",
4064 quote! {
4065 /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4066 /// models that already declare a field named `avg`.
4067 ///
4068 /// # Errors
4069 /// As [`#root::sql::fetch_aggregate_pool`].
4070 pub async fn avg<U>(
4071 col: &str,
4072 pool: &#root::sql::Pool,
4073 ) -> ::core::result::Result<
4074 ::core::option::Option<U>,
4075 #root::sql::ExecError,
4076 >
4077 where
4078 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4079 + #root::sql::MaybeMyFromRow
4080 + #root::sql::MaybeSqliteFromRow
4081 + ::core::marker::Send
4082 + ::core::marker::Unpin,
4083 {
4084 Self::__aggregate_one_pool::<U>(
4085 col,
4086 |c| #root::core::AggregateExpr::Avg(c),
4087 pool,
4088 )
4089 .await
4090 }
4091 },
4092 );
4093 let min_method = emit_if_no_field_collision(
4094 "min",
4095 quote! {
4096 /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4097 /// models that already declare a field named `min`.
4098 ///
4099 /// # Errors
4100 /// As [`#root::sql::fetch_aggregate_pool`].
4101 pub async fn min<U>(
4102 col: &str,
4103 pool: &#root::sql::Pool,
4104 ) -> ::core::result::Result<
4105 ::core::option::Option<U>,
4106 #root::sql::ExecError,
4107 >
4108 where
4109 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4110 + #root::sql::MaybeMyFromRow
4111 + #root::sql::MaybeSqliteFromRow
4112 + ::core::marker::Send
4113 + ::core::marker::Unpin,
4114 {
4115 Self::__aggregate_one_pool::<U>(
4116 col,
4117 |c| #root::core::AggregateExpr::Min(c),
4118 pool,
4119 )
4120 .await
4121 }
4122 },
4123 );
4124 let max_method = emit_if_no_field_collision(
4125 "max",
4126 quote! {
4127 /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4128 /// models that already declare a field named `max`.
4129 ///
4130 /// # Errors
4131 /// As [`#root::sql::fetch_aggregate_pool`].
4132 pub async fn max<U>(
4133 col: &str,
4134 pool: &#root::sql::Pool,
4135 ) -> ::core::result::Result<
4136 ::core::option::Option<U>,
4137 #root::sql::ExecError,
4138 >
4139 where
4140 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4141 + #root::sql::MaybeMyFromRow
4142 + #root::sql::MaybeSqliteFromRow
4143 + ::core::marker::Send
4144 + ::core::marker::Unpin,
4145 {
4146 Self::__aggregate_one_pool::<U>(
4147 col,
4148 |c| #root::core::AggregateExpr::Max(c),
4149 pool,
4150 )
4151 .await
4152 }
4153 },
4154 );
4155 let first_method = emit_if_no_field_collision(
4156 "first",
4157 quote! {
4158 /// First row of this model. Eloquent `Model::first()`.
4159 /// Skipped on models that already declare a field named
4160 /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4161 ///
4162 /// # Errors
4163 /// As `QuerySet::first`.
4164 pub async fn first(
4165 pool: &#root::sql::Pool,
4166 ) -> ::core::result::Result<
4167 ::core::option::Option<Self>,
4168 #root::sql::ExecError,
4169 > {
4170 #root::query::QuerySet::<Self>::default()
4171 .first(pool)
4172 .await
4173 }
4174 },
4175 );
4176 let last_method = emit_if_no_field_collision(
4177 "last",
4178 quote! {
4179 /// Last row of this model by primary-key DESC.
4180 /// Eloquent `Model::query()->latest('id')->first()`
4181 /// parity — fetches the highest-PK row without
4182 /// requiring the caller to spell the PK column.
4183 /// Returns `None` on an empty table.
4184 ///
4185 /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4186 /// Skipped on models that already declare a field
4187 /// named `last`.
4188 ///
4189 /// # Errors
4190 /// As `QuerySet::last`.
4191 pub async fn last(
4192 pool: &#root::sql::Pool,
4193 ) -> ::core::result::Result<
4194 ::core::option::Option<Self>,
4195 #root::sql::ExecError,
4196 > {
4197 #root::query::QuerySet::<Self>::default()
4198 .last(pool)
4199 .await
4200 }
4201 },
4202 );
4203 quote! {
4204 /// Re-SELECT this row by its primary key and overwrite
4205 /// every in-memory field with the freshly-fetched value.
4206 /// Django's [`Model.refresh_from_db`]. Issue #825.
4207 ///
4208 /// Use this when the row may have been modified by another
4209 /// process / connection / job since you read it — e.g. after
4210 /// a queued task callback, or to re-sync stale UI state
4211 /// before re-saving.
4212 ///
4213 /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4214 /// when the primary key no longer matches any row (e.g.
4215 /// the row was deleted concurrently).
4216 ///
4217 /// # Errors
4218 /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4219 /// the PK no longer exists.
4220 ///
4221 /// [`Model.refresh_from_db`]: https://docs.djangoproject.com/en/5.1/ref/models/instances/#django.db.models.Model.refresh_from_db
4222 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4223 pub async fn refresh_from_db(
4224 &mut self,
4225 pool: &#root::sql::Pool,
4226 ) -> ::core::result::Result<(), #root::sql::ExecError> {
4227 use #root::sql::FetcherPool as _;
4228 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4229 ::core::clone::Clone::clone(&self.#pk_ident),
4230 );
4231 let mut _rows: ::std::vec::Vec<Self> =
4232 #root::query::QuerySet::<Self>::default()
4233 .filter(::core::stringify!(#pk_ident), _pk_val)
4234 .limit(1)
4235 .fetch(pool)
4236 .await?;
4237 match _rows.into_iter().next() {
4238 ::core::option::Option::Some(_fresh) => {
4239 *self = _fresh;
4240 ::core::result::Result::Ok(())
4241 }
4242 ::core::option::Option::None => ::core::result::Result::Err(
4243 #root::sql::ExecError::Driver(
4244 #root::sql::sqlx::Error::RowNotFound,
4245 ),
4246 ),
4247 }
4248 }
4249
4250 /// Atomically increment the integer column `col` by
4251 /// `by` for this row. Equivalent to
4252 /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4253 /// Eloquent `Model::increment($col, $by)` / Django
4254 /// `Model.objects.filter(pk=…).update(col=F('col')+$by)`
4255 /// parity.
4256 ///
4257 /// **Doesn't mutate `self`** — the in-memory copy is now
4258 /// stale; call [`Self::refresh_from_db_pool`] /
4259 /// [`Self::fresh_pool`] to re-sync. Returns the rows-
4260 /// affected count (0 when the PK doesn't match any row,
4261 /// 1 on success).
4262 ///
4263 /// `col` is the Rust field name as a string; unknown
4264 /// fields surface as `UnknownField` at runtime. Negative
4265 /// `by` values atomically decrement (see also
4266 /// [`Self::decrement_pool`]).
4267 ///
4268 /// # Errors
4269 /// As [`UpdaterPool::execute_pool`].
4270 ///
4271 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4272 pub async fn increment(
4273 &self,
4274 col: &str,
4275 by: i64,
4276 pool: &#root::sql::Pool,
4277 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4278 Self::__increment_one(self, col, by, pool).await
4279 }
4280
4281 /// Sibling of [`Self::increment`] — atomically
4282 /// decrement this row's `col` by `by`. Eloquent
4283 /// `$model->decrement($col, $by)` parity. Equivalent to
4284 /// `self.increment(col, -by, &pool)`; the separate name
4285 /// keeps call sites readable.
4286 ///
4287 /// # Errors
4288 /// As [`Self::increment`].
4289 pub async fn decrement(
4290 &self,
4291 col: &str,
4292 by: i64,
4293 pool: &#root::sql::Pool,
4294 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4295 Self::__increment_one(self, col, -by, pool).await
4296 }
4297
4298 /// Bulk-increment: add `by` to `col` on every row of the
4299 /// table. Eloquent `Model::query()->increment($col, $by)`
4300 /// parity. Use for counters, score adjustments, view
4301 /// rollups.
4302 ///
4303 /// # Errors
4304 /// As [`UpdaterPool::execute_pool`].
4305 ///
4306 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4307 pub async fn increment_each(
4308 col: &str,
4309 by: i64,
4310 pool: &#root::sql::Pool,
4311 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4312 Self::__increment_all(col, by, pool).await
4313 }
4314
4315 /// Sibling of [`Self::increment_each`] — bulk-decrement.
4316 ///
4317 /// # Errors
4318 /// As [`Self::increment_each`].
4319 pub async fn decrement_each(
4320 col: &str,
4321 by: i64,
4322 pool: &#root::sql::Pool,
4323 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4324 Self::__increment_all(col, -by, pool).await
4325 }
4326
4327 /// Internal: forward to
4328 /// [`#root::sql::model_shortcuts::increment_one_pool`].
4329 /// One-line wrapper kept as a per-Model method so the
4330 /// macro's emitted `increment` / `decrement` instance
4331 /// calls don't have to thread `Self` through manually.
4332 #[doc(hidden)]
4333 pub async fn __increment_one(
4334 this: &Self,
4335 col: &str,
4336 by: i64,
4337 pool: &#root::sql::Pool,
4338 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4339 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4340 ::core::clone::Clone::clone(&this.#pk_ident),
4341 );
4342 #root::sql::model_shortcuts::increment_one_pool::<Self>(
4343 ::core::stringify!(#pk_ident),
4344 _pk_val,
4345 col,
4346 by,
4347 pool,
4348 )
4349 .await
4350 }
4351
4352 /// Internal: forward to
4353 /// [`#root::sql::model_shortcuts::increment_all_pool`].
4354 #[doc(hidden)]
4355 pub async fn __increment_all(
4356 col: &str,
4357 by: i64,
4358 pool: &#root::sql::Pool,
4359 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4360 #root::sql::model_shortcuts::increment_all_pool::<Self>(col, by, pool).await
4361 }
4362
4363 /// Internal: forward to
4364 /// [`#root::sql::model_shortcuts::resolve_col`].
4365 #[doc(hidden)]
4366 pub fn __resolve_col(
4367 col: &str,
4368 ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4369 #root::sql::model_shortcuts::resolve_col::<Self>(col)
4370 }
4371
4372 /// Internal: forward to
4373 /// [`#root::sql::model_shortcuts::add_signed_expr`].
4374 #[doc(hidden)]
4375 #[must_use]
4376 pub fn __add_signed_expr(
4377 col_static: &'static str,
4378 signed_by: i64,
4379 ) -> #root::core::Expr {
4380 #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4381 }
4382
4383 /// Re-SELECT this row by its primary key and return a
4384 /// **new** instance with the freshly-fetched fields.
4385 /// Eloquent `Model::fresh()` parity — non-mutating
4386 /// counterpart of [`Self::refresh_from_db_pool`].
4387 ///
4388 /// Returns `Ok(None)` when the row was deleted
4389 /// concurrently — vs [`Self::refresh_from_db_pool`]
4390 /// which surfaces that as `RowNotFound` because
4391 /// in-place mutation has nothing to write to.
4392 ///
4393 /// Useful when you want to compare the in-memory
4394 /// instance against the persisted state (audit-style
4395 /// diffs, conflict detection) without mutating the
4396 /// reference you already hold.
4397 ///
4398 /// # Errors
4399 /// As [`FetcherPool::fetch`].
4400 ///
4401 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4402 pub async fn fresh(
4403 &self,
4404 pool: &#root::sql::Pool,
4405 ) -> ::core::result::Result<
4406 ::core::option::Option<Self>,
4407 #root::sql::ExecError,
4408 > {
4409 use #root::sql::FetcherPool as _;
4410 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4411 ::core::clone::Clone::clone(&self.#pk_ident),
4412 );
4413 let _rows: ::std::vec::Vec<Self> =
4414 #root::query::QuerySet::<Self>::default()
4415 .filter(::core::stringify!(#pk_ident), _pk_val)
4416 .limit(1)
4417 .fetch(pool)
4418 .await?;
4419 ::core::result::Result::Ok(_rows.into_iter().next())
4420 }
4421
4422 #replicate_doc
4423 #[must_use]
4424 pub fn replicate(&self) -> Self {
4425 Self {
4426 #pk_clone_token,
4427 #( #other_field_clones, )*
4428 }
4429 }
4430
4431 #first_method
4432
4433 #last_method
4434
4435 /// Throwing counterpart of [`Self::first_pool`] —
4436 /// errors with `RowNotFound` when the table is empty.
4437 /// Eloquent `Model::firstOrFail()` parity.
4438 ///
4439 /// # Errors
4440 /// As [`Self::first_pool`]; additionally
4441 /// [`sqlx::Error::RowNotFound`] on empty tables.
4442 ///
4443 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4444 pub async fn first_or_fail(
4445 pool: &#root::sql::Pool,
4446 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4447 // Route through the queryset rather than `Self::first`,
4448 // which is suppressed on models with a field named
4449 // `first` (field/shortcut collision guard).
4450 match #root::query::QuerySet::<Self>::default().first(pool).await? {
4451 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4452 ::core::option::Option::None => ::core::result::Result::Err(
4453 #root::sql::ExecError::Driver(
4454 #root::sql::sqlx::Error::RowNotFound,
4455 ),
4456 ),
4457 }
4458 }
4459
4460 /// Single-column projection — `SELECT <col> FROM
4461 /// <table>`. Returns `Vec<U>` where each element is the
4462 /// decoded value of the column. Eloquent
4463 /// `Model::pluck($column)` / Django
4464 /// `Model.objects.values_list('col', flat=True)` parity.
4465 ///
4466 /// Thin wrapper over `QuerySet::<Self>::default()
4467 /// .values_list_flat(col).fetch::<U>(pool)`. `U` must
4468 /// be decodable from the column's SQL type on every
4469 /// dialect the binary targets (common picks: `i64` /
4470 /// `i32` / `String` / `bool` / `f64`).
4471 ///
4472 /// # Errors
4473 /// As `ValuesFlatQuerySet::fetch`.
4474 pub async fn pluck<U>(
4475 col: &'static str,
4476 pool: &#root::sql::Pool,
4477 ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4478 where
4479 U: #root::sql::MaybePgScalar
4480 + #root::sql::MaybeMyScalar
4481 + #root::sql::MaybeSqliteScalar
4482 + ::core::marker::Send
4483 + ::core::marker::Unpin,
4484 {
4485 #root::query::QuerySet::<Self>::default()
4486 .values_list_flat(col)
4487 .fetch::<U>(pool)
4488 .await
4489 }
4490
4491 /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4492 /// stream every row of this model in batches of `n`,
4493 /// invoking the callback once per batch. Stable PK-ASC
4494 /// ordering so the LIMIT/OFFSET pagination is
4495 /// deterministic across drivers.
4496 ///
4497 /// The callback is async — it can do further DB work
4498 /// (writes, related-row lookups, queue dispatch) per
4499 /// batch. Return `Err(...)` from the callback to abort
4500 /// the iteration early; the error bubbles up.
4501 ///
4502 /// **When to use**: bulk processing flows that can't
4503 /// fit the whole table in memory — sending newsletters,
4504 /// running data migrations, computing summary
4505 /// statistics. For small / known-bounded tables, plain
4506 /// `Self::all(&pool)` is simpler.
4507 ///
4508 /// **Caveat**: ascending OFFSET pagination is O(N²) on
4509 /// large tables. For multi-million-row scans prefer
4510 /// keyset-by-PK (the standard "WHERE id > last_seen"
4511 /// shape) over `chunk(...)`.
4512 ///
4513 /// Skipped on models without a primary key — chunking
4514 /// needs a stable order to avoid skipping / repeating
4515 /// rows across batches.
4516 pub async fn chunk<F, Fut>(
4517 n: i64,
4518 pool: &#root::sql::Pool,
4519 mut cb: F,
4520 ) -> ::core::result::Result<(), #root::sql::ExecError>
4521 where
4522 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4523 Fut: ::core::future::Future<
4524 Output = ::core::result::Result<(), #root::sql::ExecError>,
4525 >,
4526 {
4527 use #root::sql::FetcherPool as _;
4528 let pk_col = match Self::primary_key_column() {
4529 ::core::option::Option::Some(c) => c,
4530 ::core::option::Option::None => {
4531 return ::core::result::Result::Ok(());
4532 }
4533 };
4534 let mut offset: i64 = 0;
4535 loop {
4536 let rows: ::std::vec::Vec<Self> =
4537 #root::query::QuerySet::<Self>::default()
4538 .order_by(&[(pk_col, false)])
4539 .limit(n)
4540 .offset(offset)
4541 .fetch(pool)
4542 .await?;
4543 if rows.is_empty() {
4544 return ::core::result::Result::Ok(());
4545 }
4546 let len = rows.len() as i64;
4547 cb(rows).await?;
4548 if len < n {
4549 return ::core::result::Result::Ok(());
4550 }
4551 offset += n;
4552 }
4553 }
4554
4555 /// Eloquent `Model::chunkById($n, fn (...))` — same
4556 /// per-batch callback shape as [`Self::chunk`], but uses
4557 /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4558 /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4559 /// — the right choice for multi-million-row sweeps.
4560 ///
4561 /// Requires the primary key to be a signed integer type
4562 /// (`i64` / `i32`); the keyset comparison rides on
4563 /// `__rustango_pk_value()` lowering through
4564 /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4565 /// non-integer PKs (UUID, String) — those should use
4566 /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4567 /// keyset loop.
4568 ///
4569 /// Callback errors abort iteration; the error bubbles up
4570 /// unchanged. Empty table → callback invoked zero times.
4571 pub async fn chunk_by_id<F, Fut>(
4572 n: i64,
4573 pool: &#root::sql::Pool,
4574 mut cb: F,
4575 ) -> ::core::result::Result<(), #root::sql::ExecError>
4576 where
4577 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4578 Fut: ::core::future::Future<
4579 Output = ::core::result::Result<(), #root::sql::ExecError>,
4580 >,
4581 {
4582 use #root::sql::FetcherPool as _;
4583 let pk_col = match Self::primary_key_column() {
4584 ::core::option::Option::Some(c) => c,
4585 ::core::option::Option::None => {
4586 return ::core::result::Result::Ok(());
4587 }
4588 };
4589 // Track the largest PK seen so the next batch picks
4590 // up from there. `i64::MIN` as the sentinel — the
4591 // very first iteration's `> MIN` matches every row,
4592 // so the loop entry is uniform with subsequent
4593 // iterations.
4594 let mut last_seen: i64 = i64::MIN;
4595 loop {
4596 let key = ::std::format!("{}__gt", pk_col);
4597 let rows: ::std::vec::Vec<Self> =
4598 #root::query::QuerySet::<Self>::default()
4599 .filter(key.as_str(), last_seen)
4600 .order_by(&[(pk_col, false)])
4601 .limit(n)
4602 .fetch(pool)
4603 .await?;
4604 if rows.is_empty() {
4605 return ::core::result::Result::Ok(());
4606 }
4607 let len = rows.len() as i64;
4608 // Capture the last row's PK BEFORE moving rows
4609 // into the callback.
4610 let max_pk = match rows
4611 .last()
4612 .map(|r| r.__rustango_pk_value())
4613 {
4614 ::core::option::Option::Some(
4615 #root::core::SqlValue::I64(v),
4616 ) => v,
4617 ::core::option::Option::Some(
4618 #root::core::SqlValue::I32(v),
4619 ) => i64::from(v),
4620 _ => return ::core::result::Result::Ok(()),
4621 };
4622 cb(rows).await?;
4623 if len < n {
4624 return ::core::result::Result::Ok(());
4625 }
4626 last_seen = max_pk;
4627 }
4628 }
4629
4630 /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4631 /// per-row callback companion to [`Self::chunk`].
4632 /// Streams every row in keyset-paginated batches of
4633 /// `batch` size, calling `cb` once per row.
4634 ///
4635 /// Inherits the keyset-paginated scan of
4636 /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4637 /// non-integer PKs are silently a no-op).
4638 ///
4639 /// ```ignore
4640 /// Post::each(500, &pool, |p| async move {
4641 /// reindex(p).await?;
4642 /// Ok(())
4643 /// }).await?;
4644 /// ```
4645 ///
4646 /// Return `Err(...)` from the callback to abort
4647 /// iteration; the error bubbles up unchanged.
4648 pub async fn each<F, Fut>(
4649 batch: i64,
4650 pool: &#root::sql::Pool,
4651 mut cb: F,
4652 ) -> ::core::result::Result<(), #root::sql::ExecError>
4653 where
4654 F: ::core::ops::FnMut(Self) -> Fut,
4655 Fut: ::core::future::Future<
4656 Output = ::core::result::Result<(), #root::sql::ExecError>,
4657 >,
4658 {
4659 use #root::sql::FetcherPool as _;
4660 let pk_col = match Self::primary_key_column() {
4661 ::core::option::Option::Some(c) => c,
4662 ::core::option::Option::None => {
4663 return ::core::result::Result::Ok(());
4664 }
4665 };
4666 let mut last_seen: i64 = i64::MIN;
4667 loop {
4668 let key = ::std::format!("{}__gt", pk_col);
4669 let rows: ::std::vec::Vec<Self> =
4670 #root::query::QuerySet::<Self>::default()
4671 .filter(key.as_str(), last_seen)
4672 .order_by(&[(pk_col, false)])
4673 .limit(batch)
4674 .fetch(pool)
4675 .await?;
4676 if rows.is_empty() {
4677 return ::core::result::Result::Ok(());
4678 }
4679 let len = rows.len() as i64;
4680 let max_pk = match rows
4681 .last()
4682 .map(|r| r.__rustango_pk_value())
4683 {
4684 ::core::option::Option::Some(
4685 #root::core::SqlValue::I64(v),
4686 ) => v,
4687 ::core::option::Option::Some(
4688 #root::core::SqlValue::I32(v),
4689 ) => i64::from(v),
4690 _ => return ::core::result::Result::Ok(()),
4691 };
4692 for row in rows {
4693 cb(row).await?;
4694 }
4695 if len < batch {
4696 return ::core::result::Result::Ok(());
4697 }
4698 last_seen = max_pk;
4699 }
4700 }
4701
4702 /// Delete every row of this model — `TRUNCATE TABLE
4703 /// <table> RESTART IDENTITY CASCADE` on Postgres,
4704 /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4705 /// support `TRUNCATE` inside foreign-key constraints
4706 /// or — for SQLite — at all). Eloquent `Model::truncate()`
4707 /// / Django `Model.objects.all().delete()` parity.
4708 ///
4709 /// **Use only in tests / fixture-reset flows.** Production
4710 /// writes through this would silently bypass the
4711 /// `pre_delete` / `post_delete` signals (no per-row hooks
4712 /// fire on a TRUNCATE / bulk DELETE FROM) and lose every
4713 /// row's audit-log entry.
4714 ///
4715 /// # Errors
4716 /// As [`raw_execute_pool`].
4717 ///
4718 /// [`raw_execute_pool`]: rustango::sql::raw_execute_pool
4719 pub async fn truncate(
4720 pool: &#root::sql::Pool,
4721 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4722 let _table = <Self as #root::core::Model>::SCHEMA.table;
4723 let _dialect = pool.dialect();
4724 let _quoted = _dialect.quote_ident(_table);
4725 let _sql = if _dialect.name() == "postgres" {
4726 ::std::format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", _quoted)
4727 } else {
4728 ::std::format!("DELETE FROM {}", _quoted)
4729 };
4730 #root::sql::raw_execute_pool(pool, &_sql, ::std::vec::Vec::new()).await
4731 }
4732
4733 /// Bulk-delete every row whose primary key is in
4734 /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4735 /// Returns the affected row count.
4736 ///
4737 /// Eloquent `Model::destroy([1, 2, 3])` / Django
4738 /// `Model.objects.filter(pk__in=[...]).delete()` parity.
4739 /// Empty `pks` is a no-op (returns 0).
4740 ///
4741 /// Accepts any iterable whose elements are
4742 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4743 /// `[i64; N]`, etc.
4744 ///
4745 /// # Errors
4746 /// As `delete_pool`.
4747 pub async fn destroy<V>(
4748 pks: impl ::core::iter::IntoIterator<Item = V>,
4749 pool: &#root::sql::Pool,
4750 ) -> ::core::result::Result<u64, #root::sql::ExecError>
4751 where
4752 V: ::core::convert::Into<#root::core::SqlValue>,
4753 {
4754 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4755 pks.into_iter().map(::core::convert::Into::into).collect();
4756 if _values.is_empty() {
4757 return ::core::result::Result::Ok(0);
4758 }
4759 let _query = #root::core::DeleteQuery {
4760 model: <Self as #root::core::Model>::SCHEMA,
4761 where_clause: #root::core::WhereExpr::Predicate(
4762 #root::core::Filter {
4763 column: <Self as #root::core::Model>::SCHEMA
4764 .primary_key()
4765 .ok_or_else(|| {
4766 #root::sql::ExecError::Sql(
4767 #root::sql::SqlError::MissingPrimaryKey,
4768 )
4769 })?
4770 .column,
4771 op: #root::core::Op::In,
4772 value: #root::core::SqlValue::List(_values),
4773 },
4774 ),
4775 };
4776 #root::sql::delete_pool(pool, &_query).await
4777 }
4778
4779 /// Fetch every row where `<col> = <val>`. Eloquent
4780 /// `Model::where($col, $val)->get()` / Django
4781 /// `Model.objects.filter(col=val).all()` parity.
4782 ///
4783 /// Thin wrapper over `QuerySet::<Self>::default()
4784 /// .filter(col, val).fetch(pool)`. For one row,
4785 /// use [`Self::first_where_pool`]; for a chain that
4786 /// needs further `.filter()` / `.order_by()` /
4787 /// `.limit()`, drop down to `Self::query().filter(...)`
4788 /// directly.
4789 ///
4790 /// `val` accepts any value `Into<SqlValue>` so plain
4791 /// strings, ints, UUIDs, etc. all work.
4792 ///
4793 /// # Errors
4794 /// As [`FetcherPool::fetch`].
4795 ///
4796 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4797 pub async fn where_(
4798 col: &str,
4799 val: impl ::core::convert::Into<#root::core::SqlValue>,
4800 pool: &#root::sql::Pool,
4801 ) -> ::core::result::Result<
4802 ::std::vec::Vec<Self>,
4803 #root::sql::ExecError,
4804 > {
4805 use #root::sql::FetcherPool as _;
4806 #root::query::QuerySet::<Self>::default()
4807 .filter(col, val)
4808 .fetch(pool)
4809 .await
4810 }
4811
4812 /// Fetch every row where `<col> IN (vals)`. Eloquent
4813 /// `Model::whereIn($col, $vals)->get()` parity. Empty
4814 /// `vals` returns no rows (matches SQL's empty-IN
4815 /// semantics).
4816 ///
4817 /// `vals` accepts any iterable whose items are
4818 /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
4819 /// etc.
4820 ///
4821 /// # Errors
4822 /// As [`FetcherPool::fetch`].
4823 ///
4824 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4825 pub async fn where_in<V>(
4826 col: &str,
4827 vals: impl ::core::iter::IntoIterator<Item = V>,
4828 pool: &#root::sql::Pool,
4829 ) -> ::core::result::Result<
4830 ::std::vec::Vec<Self>,
4831 #root::sql::ExecError,
4832 >
4833 where
4834 V: ::core::convert::Into<#root::core::SqlValue>,
4835 {
4836 use #root::sql::FetcherPool as _;
4837 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4838 vals.into_iter().map(::core::convert::Into::into).collect();
4839 if _values.is_empty() {
4840 return ::core::result::Result::Ok(::std::vec::Vec::new());
4841 }
4842 let _key = ::std::format!("{}__in", col);
4843 #root::query::QuerySet::<Self>::default()
4844 .filter(&_key, #root::core::SqlValue::List(_values))
4845 .fetch(pool)
4846 .await
4847 }
4848
4849 /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
4850 /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
4851 /// `vals` returns every row (matches SQL's empty-NOT-IN
4852 /// semantics — vacuously true for every row).
4853 ///
4854 /// # Errors
4855 /// As [`FetcherPool::fetch`].
4856 ///
4857 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4858 pub async fn where_not_in<V>(
4859 col: &str,
4860 vals: impl ::core::iter::IntoIterator<Item = V>,
4861 pool: &#root::sql::Pool,
4862 ) -> ::core::result::Result<
4863 ::std::vec::Vec<Self>,
4864 #root::sql::ExecError,
4865 >
4866 where
4867 V: ::core::convert::Into<#root::core::SqlValue>,
4868 {
4869 use #root::sql::FetcherPool as _;
4870 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4871 vals.into_iter().map(::core::convert::Into::into).collect();
4872 if _values.is_empty() {
4873 return #root::query::QuerySet::<Self>::default()
4874 .fetch(pool)
4875 .await;
4876 }
4877 let _key = ::std::format!("{}__not_in", col);
4878 #root::query::QuerySet::<Self>::default()
4879 .filter(&_key, #root::core::SqlValue::List(_values))
4880 .fetch(pool)
4881 .await
4882 }
4883
4884 /// Fetch every row where `<col> IS NULL`. Eloquent
4885 /// `Model::whereNull($col)->get()` parity.
4886 ///
4887 /// # Errors
4888 /// As [`FetcherPool::fetch`].
4889 ///
4890 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4891 pub async fn where_null(
4892 col: &str,
4893 pool: &#root::sql::Pool,
4894 ) -> ::core::result::Result<
4895 ::std::vec::Vec<Self>,
4896 #root::sql::ExecError,
4897 > {
4898 use #root::sql::FetcherPool as _;
4899 let _key = ::std::format!("{}__isnull", col);
4900 #root::query::QuerySet::<Self>::default()
4901 .filter(&_key, #root::core::SqlValue::Bool(true))
4902 .fetch(pool)
4903 .await
4904 }
4905
4906 /// Fetch every row where `<col> IS NOT NULL`. Eloquent
4907 /// `Model::whereNotNull($col)->get()` parity.
4908 ///
4909 /// # Errors
4910 /// As [`FetcherPool::fetch`].
4911 ///
4912 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4913 pub async fn where_not_null(
4914 col: &str,
4915 pool: &#root::sql::Pool,
4916 ) -> ::core::result::Result<
4917 ::std::vec::Vec<Self>,
4918 #root::sql::ExecError,
4919 > {
4920 use #root::sql::FetcherPool as _;
4921 let _key = ::std::format!("{}__isnull", col);
4922 #root::query::QuerySet::<Self>::default()
4923 .filter(&_key, #root::core::SqlValue::Bool(false))
4924 .fetch(pool)
4925 .await
4926 }
4927
4928 /// Fetch up to `n` rows in random order. Eloquent
4929 /// `Model::inRandomOrder()->limit($n)->get()` /
4930 /// `Model::query()->inRandomOrder()->get()->take($n)`
4931 /// parity. **Performance caveat**: random ordering
4932 /// forces a full table scan + per-row random key sort;
4933 /// the optimizer cannot use an index. Prefer a
4934 /// `pk >= rand_offset LIMIT N` walk for huge tables.
4935 ///
4936 /// # Errors
4937 /// As [`FetcherPool::fetch`].
4938 ///
4939 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4940 pub async fn random_n(
4941 n: i64,
4942 pool: &#root::sql::Pool,
4943 ) -> ::core::result::Result<
4944 ::std::vec::Vec<Self>,
4945 #root::sql::ExecError,
4946 > {
4947 use #root::sql::FetcherPool as _;
4948 #root::query::QuerySet::<Self>::default()
4949 .order_random()
4950 .limit(n)
4951 .fetch(pool)
4952 .await
4953 }
4954
4955 /// Fetch one row in random order. Eloquent
4956 /// `Model::inRandomOrder()->first()` parity. Same
4957 /// performance caveat as [`Self::random_n_pool`].
4958 ///
4959 /// # Errors
4960 /// As [`FetcherPool::fetch`].
4961 ///
4962 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4963 pub async fn random(
4964 pool: &#root::sql::Pool,
4965 ) -> ::core::result::Result<
4966 ::core::option::Option<Self>,
4967 #root::sql::ExecError,
4968 > {
4969 ::core::result::Result::Ok(
4970 Self::random_n(1, pool).await?.into_iter().next(),
4971 )
4972 }
4973
4974 /// Fetch every row ordered ASC by `field`. Eloquent
4975 /// `Model::oldest($field)->get()` parity — the multi-row
4976 /// counterpart of [`Self::earliest_pool`].
4977 ///
4978 /// # Errors
4979 /// As [`FetcherPool::fetch`].
4980 ///
4981 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4982 pub async fn oldest(
4983 field: &str,
4984 pool: &#root::sql::Pool,
4985 ) -> ::core::result::Result<
4986 ::std::vec::Vec<Self>,
4987 #root::sql::ExecError,
4988 > {
4989 use #root::sql::FetcherPool as _;
4990 #root::query::QuerySet::<Self>::default()
4991 .order_by(&[(field, false)])
4992 .fetch(pool)
4993 .await
4994 }
4995
4996 /// Fetch every row ordered DESC by `field`. Eloquent
4997 /// `Model::latest($field)->get()` parity — the multi-row
4998 /// counterpart of [`Self::latest_pool`].
4999 ///
5000 /// # Errors
5001 /// As [`FetcherPool::fetch`].
5002 ///
5003 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5004 pub async fn newest(
5005 field: &str,
5006 pool: &#root::sql::Pool,
5007 ) -> ::core::result::Result<
5008 ::std::vec::Vec<Self>,
5009 #root::sql::ExecError,
5010 > {
5011 use #root::sql::FetcherPool as _;
5012 #root::query::QuerySet::<Self>::default()
5013 .order_by(&[(field, true)])
5014 .fetch(pool)
5015 .await
5016 }
5017
5018 /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5019 /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5020 /// Routes through the existing `__year` lookup suffix
5021 /// (issue #829).
5022 ///
5023 /// # Errors
5024 /// As [`FetcherPool::fetch`].
5025 ///
5026 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5027 pub async fn where_year(
5028 col: &str,
5029 year: i64,
5030 pool: &#root::sql::Pool,
5031 ) -> ::core::result::Result<
5032 ::std::vec::Vec<Self>,
5033 #root::sql::ExecError,
5034 > {
5035 use #root::sql::FetcherPool as _;
5036 let _key = ::std::format!("{}__year", col);
5037 #root::query::QuerySet::<Self>::default()
5038 .filter(&_key, #root::core::SqlValue::I64(year))
5039 .fetch(pool)
5040 .await
5041 }
5042
5043 /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5044 /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5045 /// `month` is 1–12.
5046 ///
5047 /// # Errors
5048 /// As [`FetcherPool::fetch`].
5049 ///
5050 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5051 pub async fn where_month(
5052 col: &str,
5053 month: i64,
5054 pool: &#root::sql::Pool,
5055 ) -> ::core::result::Result<
5056 ::std::vec::Vec<Self>,
5057 #root::sql::ExecError,
5058 > {
5059 use #root::sql::FetcherPool as _;
5060 let _key = ::std::format!("{}__month", col);
5061 #root::query::QuerySet::<Self>::default()
5062 .filter(&_key, #root::core::SqlValue::I64(month))
5063 .fetch(pool)
5064 .await
5065 }
5066
5067 /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5068 /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5069 /// `day` is 1–31.
5070 ///
5071 /// # Errors
5072 /// As [`FetcherPool::fetch`].
5073 ///
5074 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5075 pub async fn where_day(
5076 col: &str,
5077 day: i64,
5078 pool: &#root::sql::Pool,
5079 ) -> ::core::result::Result<
5080 ::std::vec::Vec<Self>,
5081 #root::sql::ExecError,
5082 > {
5083 use #root::sql::FetcherPool as _;
5084 let _key = ::std::format!("{}__day", col);
5085 #root::query::QuerySet::<Self>::default()
5086 .filter(&_key, #root::core::SqlValue::I64(day))
5087 .fetch(pool)
5088 .await
5089 }
5090
5091 /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5092 /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5093 /// `hour` is 0–23.
5094 ///
5095 /// # Errors
5096 /// As [`FetcherPool::fetch`].
5097 ///
5098 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5099 pub async fn where_hour(
5100 col: &str,
5101 hour: i64,
5102 pool: &#root::sql::Pool,
5103 ) -> ::core::result::Result<
5104 ::std::vec::Vec<Self>,
5105 #root::sql::ExecError,
5106 > {
5107 use #root::sql::FetcherPool as _;
5108 let _key = ::std::format!("{}__hour", col);
5109 #root::query::QuerySet::<Self>::default()
5110 .filter(&_key, #root::core::SqlValue::I64(hour))
5111 .fetch(pool)
5112 .await
5113 }
5114
5115 /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5116 /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5117 /// `minute` is 0–59.
5118 ///
5119 /// # Errors
5120 /// As [`FetcherPool::fetch`].
5121 ///
5122 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5123 pub async fn where_minute(
5124 col: &str,
5125 minute: i64,
5126 pool: &#root::sql::Pool,
5127 ) -> ::core::result::Result<
5128 ::std::vec::Vec<Self>,
5129 #root::sql::ExecError,
5130 > {
5131 use #root::sql::FetcherPool as _;
5132 let _key = ::std::format!("{}__minute", col);
5133 #root::query::QuerySet::<Self>::default()
5134 .filter(&_key, #root::core::SqlValue::I64(minute))
5135 .fetch(pool)
5136 .await
5137 }
5138
5139 /// Fetch every row where `<col> LIKE <pattern>` —
5140 /// caller-supplied pattern (must include `%` / `_`
5141 /// wildcards manually). Eloquent `Model::whereLike`
5142 /// parity.
5143 ///
5144 /// # Errors
5145 /// As [`FetcherPool::fetch`].
5146 ///
5147 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5148 pub async fn where_like(
5149 col: &str,
5150 pattern: impl ::core::convert::Into<::std::string::String>,
5151 pool: &#root::sql::Pool,
5152 ) -> ::core::result::Result<
5153 ::std::vec::Vec<Self>,
5154 #root::sql::ExecError,
5155 > {
5156 use #root::sql::FetcherPool as _;
5157 let _key = ::std::format!("{}__like", col);
5158 #root::query::QuerySet::<Self>::default()
5159 .filter(
5160 &_key,
5161 #root::core::SqlValue::String(pattern.into()),
5162 )
5163 .fetch(pool)
5164 .await
5165 }
5166
5167 /// Fetch every row where `<col> ILIKE <pattern>` —
5168 /// case-insensitive LIKE (PG native, MySQL/SQLite
5169 /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5170 ///
5171 /// # Errors
5172 /// As [`FetcherPool::fetch`].
5173 ///
5174 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5175 pub async fn where_ilike(
5176 col: &str,
5177 pattern: impl ::core::convert::Into<::std::string::String>,
5178 pool: &#root::sql::Pool,
5179 ) -> ::core::result::Result<
5180 ::std::vec::Vec<Self>,
5181 #root::sql::ExecError,
5182 > {
5183 use #root::sql::FetcherPool as _;
5184 let _key = ::std::format!("{}__ilike", col);
5185 #root::query::QuerySet::<Self>::default()
5186 .filter(
5187 &_key,
5188 #root::core::SqlValue::String(pattern.into()),
5189 )
5190 .fetch(pool)
5191 .await
5192 }
5193
5194 /// Fetch every row where `<col>` starts with `prefix`
5195 /// (auto-appends `%`). Django `__startswith` / Eloquent
5196 /// `whereLike("col", "$prefix%")` parity.
5197 ///
5198 /// # Errors
5199 /// As [`FetcherPool::fetch`].
5200 ///
5201 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5202 pub async fn where_starts_with(
5203 col: &str,
5204 prefix: impl ::core::convert::Into<::std::string::String>,
5205 pool: &#root::sql::Pool,
5206 ) -> ::core::result::Result<
5207 ::std::vec::Vec<Self>,
5208 #root::sql::ExecError,
5209 > {
5210 use #root::sql::FetcherPool as _;
5211 let _key = ::std::format!("{}__startswith", col);
5212 #root::query::QuerySet::<Self>::default()
5213 .filter(
5214 &_key,
5215 #root::core::SqlValue::String(prefix.into()),
5216 )
5217 .fetch(pool)
5218 .await
5219 }
5220
5221 /// Fetch every row where `<col>` ends with `suffix`
5222 /// (auto-prepends `%`). Django `__endswith` / Eloquent
5223 /// `whereLike("col", "%$suffix")` parity.
5224 ///
5225 /// # Errors
5226 /// As [`FetcherPool::fetch`].
5227 ///
5228 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5229 pub async fn where_ends_with(
5230 col: &str,
5231 suffix: impl ::core::convert::Into<::std::string::String>,
5232 pool: &#root::sql::Pool,
5233 ) -> ::core::result::Result<
5234 ::std::vec::Vec<Self>,
5235 #root::sql::ExecError,
5236 > {
5237 use #root::sql::FetcherPool as _;
5238 let _key = ::std::format!("{}__endswith", col);
5239 #root::query::QuerySet::<Self>::default()
5240 .filter(
5241 &_key,
5242 #root::core::SqlValue::String(suffix.into()),
5243 )
5244 .fetch(pool)
5245 .await
5246 }
5247
5248 /// Fetch every row where `<col>` contains `substr`
5249 /// (auto-wraps with `%`). Django `__contains` /
5250 /// Eloquent `whereLike("col", "%$substr%")` parity.
5251 ///
5252 /// # Errors
5253 /// As [`FetcherPool::fetch`].
5254 ///
5255 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5256 pub async fn where_contains(
5257 col: &str,
5258 substr: impl ::core::convert::Into<::std::string::String>,
5259 pool: &#root::sql::Pool,
5260 ) -> ::core::result::Result<
5261 ::std::vec::Vec<Self>,
5262 #root::sql::ExecError,
5263 > {
5264 use #root::sql::FetcherPool as _;
5265 let _key = ::std::format!("{}__contains", col);
5266 #root::query::QuerySet::<Self>::default()
5267 .filter(
5268 &_key,
5269 #root::core::SqlValue::String(substr.into()),
5270 )
5271 .fetch(pool)
5272 .await
5273 }
5274
5275 /// Fetch every row where `<col> > val`. Eloquent
5276 /// `Model::where($col, ">", $val)->get()` parity.
5277 ///
5278 /// # Errors
5279 /// As [`FetcherPool::fetch`].
5280 ///
5281 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5282 pub async fn where_gt(
5283 col: &str,
5284 val: impl ::core::convert::Into<#root::core::SqlValue>,
5285 pool: &#root::sql::Pool,
5286 ) -> ::core::result::Result<
5287 ::std::vec::Vec<Self>,
5288 #root::sql::ExecError,
5289 > {
5290 use #root::sql::FetcherPool as _;
5291 let _key = ::std::format!("{}__gt", col);
5292 #root::query::QuerySet::<Self>::default()
5293 .filter(&_key, val)
5294 .fetch(pool)
5295 .await
5296 }
5297
5298 /// Fetch every row where `<col> >= val`. Eloquent
5299 /// `Model::where($col, ">=", $val)->get()` parity.
5300 ///
5301 /// # Errors
5302 /// As [`FetcherPool::fetch`].
5303 ///
5304 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5305 pub async fn where_gte(
5306 col: &str,
5307 val: impl ::core::convert::Into<#root::core::SqlValue>,
5308 pool: &#root::sql::Pool,
5309 ) -> ::core::result::Result<
5310 ::std::vec::Vec<Self>,
5311 #root::sql::ExecError,
5312 > {
5313 use #root::sql::FetcherPool as _;
5314 let _key = ::std::format!("{}__gte", col);
5315 #root::query::QuerySet::<Self>::default()
5316 .filter(&_key, val)
5317 .fetch(pool)
5318 .await
5319 }
5320
5321 /// Fetch every row where `<col> < val`. Eloquent
5322 /// `Model::where($col, "<", $val)->get()` parity.
5323 ///
5324 /// # Errors
5325 /// As [`FetcherPool::fetch`].
5326 ///
5327 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5328 pub async fn where_lt(
5329 col: &str,
5330 val: impl ::core::convert::Into<#root::core::SqlValue>,
5331 pool: &#root::sql::Pool,
5332 ) -> ::core::result::Result<
5333 ::std::vec::Vec<Self>,
5334 #root::sql::ExecError,
5335 > {
5336 use #root::sql::FetcherPool as _;
5337 let _key = ::std::format!("{}__lt", col);
5338 #root::query::QuerySet::<Self>::default()
5339 .filter(&_key, val)
5340 .fetch(pool)
5341 .await
5342 }
5343
5344 /// Fetch every row where `<col> <= val`. Eloquent
5345 /// `Model::where($col, "<=", $val)->get()` parity.
5346 ///
5347 /// # Errors
5348 /// As [`FetcherPool::fetch`].
5349 ///
5350 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5351 pub async fn where_lte(
5352 col: &str,
5353 val: impl ::core::convert::Into<#root::core::SqlValue>,
5354 pool: &#root::sql::Pool,
5355 ) -> ::core::result::Result<
5356 ::std::vec::Vec<Self>,
5357 #root::sql::ExecError,
5358 > {
5359 use #root::sql::FetcherPool as _;
5360 let _key = ::std::format!("{}__lte", col);
5361 #root::query::QuerySet::<Self>::default()
5362 .filter(&_key, val)
5363 .fetch(pool)
5364 .await
5365 }
5366
5367 /// Fetch every row where `<col> <> val`. Eloquent
5368 /// `Model::where($col, "!=", $val)->get()` parity.
5369 ///
5370 /// # Errors
5371 /// As [`FetcherPool::fetch`].
5372 ///
5373 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5374 pub async fn where_ne(
5375 col: &str,
5376 val: impl ::core::convert::Into<#root::core::SqlValue>,
5377 pool: &#root::sql::Pool,
5378 ) -> ::core::result::Result<
5379 ::std::vec::Vec<Self>,
5380 #root::sql::ExecError,
5381 > {
5382 use #root::sql::FetcherPool as _;
5383 let _key = ::std::format!("{}__ne", col);
5384 #root::query::QuerySet::<Self>::default()
5385 .filter(&_key, val)
5386 .fetch(pool)
5387 .await
5388 }
5389
5390 /// Fetch every row matching `<col> = val` for ANY of the
5391 /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5392 /// parity. Empty `cols` returns no rows.
5393 ///
5394 /// Resolves each `&str` column to its SCHEMA-registered
5395 /// `&'static str` once and builds a single OR-composed
5396 /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5397 ///
5398 /// # Errors
5399 /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5400 /// when any column is not declared on the model.
5401 ///
5402 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5403 pub async fn where_any(
5404 cols: &[&str],
5405 val: impl ::core::convert::Into<#root::core::SqlValue>,
5406 pool: &#root::sql::Pool,
5407 ) -> ::core::result::Result<
5408 ::std::vec::Vec<Self>,
5409 #root::sql::ExecError,
5410 > {
5411 Self::__where_multi(cols, val, false, pool).await
5412 }
5413
5414 /// Fetch every row matching `<col> = val` for ALL listed
5415 /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5416 /// Empty `cols` returns every row (vacuous AND).
5417 ///
5418 /// # Errors
5419 /// As [`Self::where_any`].
5420 pub async fn where_all(
5421 cols: &[&str],
5422 val: impl ::core::convert::Into<#root::core::SqlValue>,
5423 pool: &#root::sql::Pool,
5424 ) -> ::core::result::Result<
5425 ::std::vec::Vec<Self>,
5426 #root::sql::ExecError,
5427 > {
5428 Self::__where_multi(cols, val, true, pool).await
5429 }
5430
5431 /// Internal: build a Q expression composing `cols` via
5432 /// AND (`all`) or OR (`!all`), then fetch. Backs
5433 /// `where_any` / `where_all`.
5434 #[doc(hidden)]
5435 pub async fn __where_multi(
5436 cols: &[&str],
5437 val: impl ::core::convert::Into<#root::core::SqlValue>,
5438 all: bool,
5439 pool: &#root::sql::Pool,
5440 ) -> ::core::result::Result<
5441 ::std::vec::Vec<Self>,
5442 #root::sql::ExecError,
5443 > {
5444 #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5445 .await
5446 }
5447
5448 /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5449 /// parity / Django `Model.objects.all()[:n]`. PK-ordered
5450 /// is NOT guaranteed without an explicit `order_by` —
5451 /// drop into `Self::query()` for that.
5452 ///
5453 /// # Errors
5454 /// As [`FetcherPool::fetch`].
5455 ///
5456 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5457 pub async fn take(
5458 n: i64,
5459 pool: &#root::sql::Pool,
5460 ) -> ::core::result::Result<
5461 ::std::vec::Vec<Self>,
5462 #root::sql::ExecError,
5463 > {
5464 use #root::sql::FetcherPool as _;
5465 #root::query::QuerySet::<Self>::default()
5466 .limit(n)
5467 .fetch(pool)
5468 .await
5469 }
5470
5471 /// Fetch the page-th window of `per_page` rows
5472 /// (1-indexed). Eloquent
5473 /// `Model::query()->forPage($page, $perPage)->get()`
5474 /// parity. The DB scans an `OFFSET (page - 1) * per_page
5475 /// LIMIT per_page`; for large offsets this is O(N) —
5476 /// prefer keyset pagination via PK on hot paths.
5477 ///
5478 /// # Errors
5479 /// As [`FetcherPool::fetch`].
5480 ///
5481 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5482 pub async fn for_page(
5483 page: i64,
5484 per_page: i64,
5485 pool: &#root::sql::Pool,
5486 ) -> ::core::result::Result<
5487 ::std::vec::Vec<Self>,
5488 #root::sql::ExecError,
5489 > {
5490 use #root::sql::FetcherPool as _;
5491 let _offset = if page > 1 { (page - 1) * per_page } else { 0 };
5492 #root::query::QuerySet::<Self>::default()
5493 .limit(per_page)
5494 .offset(_offset)
5495 .fetch(pool)
5496 .await
5497 }
5498
5499 /// Eloquent `Model::paginate($per_page, $page)` — fetch
5500 /// one page of rows AND the total row count in one
5501 /// call. Returns `(rows, total)`. Two queries: a
5502 /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5503 /// for the total.
5504 ///
5505 /// Useful for paginated UIs that need both the visible
5506 /// rows AND a "Page X of Y" / total-count widget. For
5507 /// large tables prefer keyset pagination + cached count;
5508 /// every call to `paginate` re-counts the full table.
5509 ///
5510 /// Same 1-indexed `page` convention as [`Self::for_page`].
5511 ///
5512 /// # Errors
5513 /// As [`Self::for_page`] and [`Self::count`].
5514 pub async fn paginate(
5515 page: i64,
5516 per_page: i64,
5517 pool: &#root::sql::Pool,
5518 ) -> ::core::result::Result<
5519 (::std::vec::Vec<Self>, i64),
5520 #root::sql::ExecError,
5521 > {
5522 // Route through the queryset rather than `Self::count`:
5523 // the `count` inherent method is suppressed on models
5524 // that declare a field named `count` (field/shortcut
5525 // collision guard), and `paginate` must still compile
5526 // for those models.
5527 let total = {
5528 use #root::sql::CounterPool as _;
5529 #root::query::QuerySet::<Self>::default()
5530 .count(pool)
5531 .await?
5532 };
5533 let rows = Self::for_page(page, per_page, pool).await?;
5534 ::core::result::Result::Ok((rows, total))
5535 }
5536
5537 /// Bulk-update — set `set_col = set_val` on every row
5538 /// matching `where_col = where_val`. Returns affected row
5539 /// count. Eloquent
5540 /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5541 /// parity.
5542 ///
5543 /// For multi-column updates drop into the queryset
5544 /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5545 ///
5546 /// # Errors
5547 /// As [`UpdaterPool::execute_pool`].
5548 ///
5549 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5550 pub async fn update_where(
5551 where_col: &str,
5552 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5553 set_col: &str,
5554 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5555 pool: &#root::sql::Pool,
5556 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5557 use #root::sql::UpdaterPool as _;
5558 #root::query::QuerySet::<Self>::default()
5559 .filter(where_col, where_val)
5560 .update()
5561 .set(set_col, set_val)
5562 .execute_pool(pool)
5563 .await
5564 }
5565
5566 /// Bulk-delete — remove every row matching
5567 /// `where_col = where_val`. Returns affected row count.
5568 /// Eloquent
5569 /// `Model::where($where_col, $where_val)->delete()` parity.
5570 ///
5571 /// For more complex filters drop into the queryset
5572 /// builder + `Self::query().filter(...).delete().execute_pool(&pool)`.
5573 ///
5574 /// # Errors
5575 /// As [`#root::sql::delete_pool`].
5576 pub async fn delete_where(
5577 where_col: &str,
5578 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5579 pool: &#root::sql::Pool,
5580 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5581 let _query = #root::core::DeleteQuery {
5582 model: <Self as #root::core::Model>::SCHEMA,
5583 where_clause: #root::core::WhereExpr::Predicate(
5584 #root::core::Filter {
5585 column: <Self as #root::core::Model>::SCHEMA
5586 .field(where_col)
5587 .ok_or_else(|| {
5588 #root::sql::ExecError::Query(
5589 #root::core::QueryError::UnknownField {
5590 model: <Self as #root::core::Model>::SCHEMA.name,
5591 field: ::std::string::ToString::to_string(where_col),
5592 },
5593 )
5594 })?
5595 .column,
5596 op: #root::core::Op::Eq,
5597 value: ::core::convert::Into::into(where_val),
5598 },
5599 ),
5600 };
5601 #root::sql::delete_pool(pool, &_query).await
5602 }
5603
5604 /// Bulk-update — set `set_col = set_val` on EVERY row of
5605 /// this model's table. **No WHERE clause** — use with
5606 /// care. Eloquent `Model::query()->update([$col => $val])`
5607 /// parity.
5608 ///
5609 /// Use for backfills, one-shot reset flows, etc. The
5610 /// returned count is rows affected.
5611 ///
5612 /// # Errors
5613 /// As [`UpdaterPool::execute_pool`].
5614 ///
5615 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5616 pub async fn update_all(
5617 set_col: &str,
5618 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5619 pool: &#root::sql::Pool,
5620 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5621 use #root::sql::UpdaterPool as _;
5622 #root::query::QuerySet::<Self>::default()
5623 .update()
5624 .set(set_col, set_val)
5625 .execute_pool(pool)
5626 .await
5627 }
5628
5629 /// Fetch every row where `<col> NOT LIKE <pattern>`.
5630 /// Eloquent `Model::whereNotLike` parity. Pattern is
5631 /// passed verbatim — caller controls `%` / `_`.
5632 ///
5633 /// # Errors
5634 /// As [`FetcherPool::fetch`].
5635 ///
5636 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5637 pub async fn where_not_like(
5638 col: &str,
5639 pattern: impl ::core::convert::Into<::std::string::String>,
5640 pool: &#root::sql::Pool,
5641 ) -> ::core::result::Result<
5642 ::std::vec::Vec<Self>,
5643 #root::sql::ExecError,
5644 > {
5645 use #root::sql::FetcherPool as _;
5646 let _key = ::std::format!("{}__not_like", col);
5647 #root::query::QuerySet::<Self>::default()
5648 .filter(
5649 &_key,
5650 #root::core::SqlValue::String(pattern.into()),
5651 )
5652 .fetch(pool)
5653 .await
5654 }
5655
5656 /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5657 /// case-insensitive `NOT LIKE` (PG native, MySQL /
5658 /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5659 ///
5660 /// # Errors
5661 /// As [`FetcherPool::fetch`].
5662 ///
5663 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5664 pub async fn where_not_ilike(
5665 col: &str,
5666 pattern: impl ::core::convert::Into<::std::string::String>,
5667 pool: &#root::sql::Pool,
5668 ) -> ::core::result::Result<
5669 ::std::vec::Vec<Self>,
5670 #root::sql::ExecError,
5671 > {
5672 use #root::sql::FetcherPool as _;
5673 let _key = ::std::format!("{}__not_ilike", col);
5674 #root::query::QuerySet::<Self>::default()
5675 .filter(
5676 &_key,
5677 #root::core::SqlValue::String(pattern.into()),
5678 )
5679 .fetch(pool)
5680 .await
5681 }
5682
5683 /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5684 /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5685 /// parity.
5686 ///
5687 /// # Errors
5688 /// As [`FetcherPool::fetch`].
5689 ///
5690 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5691 pub async fn where_not_between(
5692 col: &str,
5693 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5694 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5695 pool: &#root::sql::Pool,
5696 ) -> ::core::result::Result<
5697 ::std::vec::Vec<Self>,
5698 #root::sql::ExecError,
5699 > {
5700 use #root::sql::FetcherPool as _;
5701 let _key = ::std::format!("{}__not_between", col);
5702 let _vals = #root::core::SqlValue::List(::std::vec![
5703 ::core::convert::Into::into(lo),
5704 ::core::convert::Into::into(hi),
5705 ]);
5706 #root::query::QuerySet::<Self>::default()
5707 .filter(&_key, _vals)
5708 .fetch(pool)
5709 .await
5710 }
5711
5712 /// Returns the SQL table name for this model. Eloquent
5713 /// `$model->getTable()` parity.
5714 #[must_use]
5715 pub fn table_name() -> &'static str {
5716 <Self as #root::core::Model>::SCHEMA.table
5717 }
5718
5719 /// Returns the SQL column name of this model's primary
5720 /// key, or `None` when the model has no
5721 /// `#[rustango(primary_key)]`. Eloquent
5722 /// `$model->getKeyName()` parity.
5723 #[must_use]
5724 pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5725 <Self as #root::core::Model>::SCHEMA
5726 .primary_key()
5727 .map(|f| f.column)
5728 }
5729
5730 /// Fetch every row where `<col> BETWEEN lo AND hi`
5731 /// (inclusive on both ends — same as SQL). Eloquent
5732 /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5733 ///
5734 /// # Errors
5735 /// As [`FetcherPool::fetch`].
5736 ///
5737 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5738 pub async fn where_between(
5739 col: &str,
5740 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5741 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5742 pool: &#root::sql::Pool,
5743 ) -> ::core::result::Result<
5744 ::std::vec::Vec<Self>,
5745 #root::sql::ExecError,
5746 > {
5747 use #root::sql::FetcherPool as _;
5748 let _key = ::std::format!("{}__between", col);
5749 let _vals = #root::core::SqlValue::List(::std::vec![
5750 ::core::convert::Into::into(lo),
5751 ::core::convert::Into::into(hi),
5752 ]);
5753 #root::query::QuerySet::<Self>::default()
5754 .filter(&_key, _vals)
5755 .fetch(pool)
5756 .await
5757 }
5758
5759 /// Fetch the first row where `<col> = <val>`. Returns
5760 /// `Ok(None)` when no row matches. Eloquent
5761 /// `Model::firstWhere($col, $val)` / Django
5762 /// `Model.objects.filter(col=val).first()` parity.
5763 ///
5764 /// Thin wrapper over `QuerySet::<Self>::default()
5765 /// .filter(col, val).first(pool)`. Use this when you
5766 /// want one row identified by a non-PK column (e.g.
5767 /// `User::first_where_pool("email", "x@y.com", &pool)`).
5768 ///
5769 /// `val` accepts any value `Into<SqlValue>` so plain
5770 /// strings, ints, UUIDs, etc. all work.
5771 ///
5772 /// # Errors
5773 /// As `QuerySet::first`.
5774 pub async fn first_where(
5775 col: &str,
5776 val: impl ::core::convert::Into<#root::core::SqlValue>,
5777 pool: &#root::sql::Pool,
5778 ) -> ::core::result::Result<
5779 ::core::option::Option<Self>,
5780 #root::sql::ExecError,
5781 > {
5782 #root::query::QuerySet::<Self>::default()
5783 .filter(col, val)
5784 .first(pool)
5785 .await
5786 }
5787
5788 #value_method
5789
5790 /// Fetch the row with the largest `field` value —
5791 /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
5792 /// `Ok(None)` for an empty table. Eloquent
5793 /// `Model::latest($field)->first()` / Django
5794 /// `Model.objects.latest(field)` (non-throwing) parity.
5795 /// Thin wrapper over `QuerySet::<Self>::default()
5796 /// .latest(field, pool)`.
5797 ///
5798 /// **Field name** is the Rust field ident as a string
5799 /// (not the SQL column). Unknown fields surface as
5800 /// `ExecError::Query(QueryError::UnknownField)` at
5801 /// compile time.
5802 ///
5803 /// # Errors
5804 /// As `QuerySet::latest`.
5805 pub async fn latest(
5806 field: &str,
5807 pool: &#root::sql::Pool,
5808 ) -> ::core::result::Result<
5809 ::core::option::Option<Self>,
5810 #root::sql::ExecError,
5811 > {
5812 #root::query::QuerySet::<Self>::default()
5813 .latest(field, pool)
5814 .await
5815 }
5816
5817 /// Sibling of [`Self::latest_pool`] — fetches the row
5818 /// with the smallest `field` value (`ORDER BY <field>
5819 /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
5820 /// ->first()` / Django `Model.objects.earliest(field)`
5821 /// parity.
5822 ///
5823 /// # Errors
5824 /// As [`Self::latest_pool`].
5825 pub async fn earliest(
5826 field: &str,
5827 pool: &#root::sql::Pool,
5828 ) -> ::core::result::Result<
5829 ::core::option::Option<Self>,
5830 #root::sql::ExecError,
5831 > {
5832 #root::query::QuerySet::<Self>::default()
5833 .earliest(field, pool)
5834 .await
5835 }
5836
5837 #count_method
5838
5839 /// `true` when the table contains at least one row.
5840 /// Eloquent `Model::query()->exists()` / Django
5841 /// `Model.objects.exists()` parity. Thin wrapper over
5842 /// `QuerySet::<Self>::default().exists(pool)`.
5843 ///
5844 /// # Errors
5845 /// As [`ExistsPool::exists`].
5846 ///
5847 /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
5848 pub async fn exists(
5849 pool: &#root::sql::Pool,
5850 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5851 use #root::sql::ExistsPool as _;
5852 #root::query::QuerySet::<Self>::default()
5853 .exists(pool)
5854 .await
5855 }
5856
5857 /// Inverse of [`Self::exists`] — returns `true` when
5858 /// the table has zero rows. Eloquent
5859 /// `Model::doesntExist()` parity.
5860 ///
5861 /// # Errors
5862 /// As [`Self::exists`].
5863 pub async fn doesnt_exist(
5864 pool: &#root::sql::Pool,
5865 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5866 Self::exists(pool).await.map(|e| !e)
5867 }
5868
5869 /// Eloquent `Model::query()->whereKey($pk)->exists()` —
5870 /// `true` when a row with primary key `pk` exists in
5871 /// the table. Sugar over
5872 /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
5873 ///
5874 /// Differs from [`Self::exists`] (which checks "any row
5875 /// in the table") by checking a specific PK existence.
5876 /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
5877 /// because the row is never materialized — the SQL is
5878 /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
5879 ///
5880 /// # Errors
5881 /// As [`#root::sql::ExistsPool::contains_pk`].
5882 pub async fn contains_pk(
5883 pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
5884 pool: &#root::sql::Pool,
5885 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5886 use #root::sql::ExistsPool as _;
5887 #root::query::QuerySet::<Self>::default()
5888 .contains_pk(pool, pk)
5889 .await
5890 }
5891
5892 #sum_method
5893 #avg_method
5894 #min_method
5895 #max_method
5896
5897 /// Internal: forward to
5898 /// [`#root::sql::model_shortcuts::aggregate_one_pool`].
5899 /// Backs `sum` / `avg` / `min` / `max`.
5900 #[doc(hidden)]
5901 pub async fn __aggregate_one_pool<U>(
5902 col: &str,
5903 build: fn(&'static str) -> #root::core::AggregateExpr,
5904 pool: &#root::sql::Pool,
5905 ) -> ::core::result::Result<
5906 ::core::option::Option<U>,
5907 #root::sql::ExecError,
5908 >
5909 where
5910 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
5911 + #root::sql::MaybeMyFromRow
5912 + #root::sql::MaybeSqliteFromRow
5913 + ::core::marker::Send
5914 + ::core::marker::Unpin,
5915 {
5916 #root::sql::model_shortcuts::aggregate_one_pool::<Self, U>(col, build, pool)
5917 .await
5918 }
5919
5920 /// Fetch every row of this model from `pool`. Eloquent
5921 /// `Model::all()` parity — a thin wrapper over
5922 /// `QuerySet::<Self>::default().fetch(pool)`.
5923 ///
5924 /// **Use with care on large tables** — there's no
5925 /// pagination or limit; the entire table is materialized
5926 /// into memory. For anything beyond fixture / lookup
5927 /// tables, page through `QuerySet::<Self>::default()
5928 /// .order_by(...).limit(N).offset(M).fetch(pool)`
5929 /// or stream via `.iterator(chunk_size)`.
5930 ///
5931 /// # Errors
5932 /// As [`FetcherPool::fetch`].
5933 ///
5934 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5935 pub async fn all(
5936 pool: &#root::sql::Pool,
5937 ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
5938 {
5939 use #root::sql::FetcherPool as _;
5940 #root::query::QuerySet::<Self>::default()
5941 .fetch(pool)
5942 .await
5943 }
5944
5945 /// Look up every row whose primary key is in `pks`.
5946 /// Returns the matching rows in **inventory** order — NOT
5947 /// the order of `pks`. Empty `pks` returns an empty
5948 /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
5949 /// with a list) / Django `Model.objects.filter(pk__in=[...])`
5950 /// parity.
5951 ///
5952 /// Thin wrapper over `QuerySet::<Self>::default()
5953 /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
5954 /// Caller-supplied PKs that don't match a row are
5955 /// silently skipped (the returned vec is shorter than
5956 /// the input list). For an order-preserving / "fail
5957 /// when any missing" variant, build the queryset
5958 /// explicitly with `in_bulk` instead.
5959 ///
5960 /// Accepts any iterable whose elements are
5961 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
5962 /// `Vec<Uuid>`, etc.
5963 ///
5964 /// # Errors
5965 /// As [`FetcherPool::fetch`].
5966 ///
5967 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5968 pub async fn find_many<V>(
5969 pks: impl ::core::iter::IntoIterator<Item = V>,
5970 pool: &#root::sql::Pool,
5971 ) -> ::core::result::Result<
5972 ::std::vec::Vec<Self>,
5973 #root::sql::ExecError,
5974 >
5975 where
5976 V: ::core::convert::Into<#root::core::SqlValue>,
5977 {
5978 use #root::sql::FetcherPool as _;
5979 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5980 pks.into_iter().map(::core::convert::Into::into).collect();
5981 if _values.is_empty() {
5982 return ::core::result::Result::Ok(::std::vec::Vec::new());
5983 }
5984 let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
5985 #root::query::QuerySet::<Self>::default()
5986 .filter(&_key, #root::core::SqlValue::List(_values))
5987 .fetch(pool)
5988 .await
5989 }
5990
5991 /// Look up the row whose primary key equals `pk`. Returns
5992 /// `Ok(None)` when no row matches; this is the
5993 /// non-throwing counterpart of Django's `.get(pk=…)`
5994 /// (which raises `DoesNotExist`). Eloquent `Model::find`
5995 /// shape — accepts any value `Into<SqlValue>`.
5996 ///
5997 /// One-liner shortcut for the common
5998 /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
5999 /// .limit(1).fetch(pool).await?.into_iter().next()`
6000 /// dance.
6001 ///
6002 /// # Errors
6003 /// As [`FetcherPool::fetch`].
6004 ///
6005 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6006 pub async fn find(
6007 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6008 pool: &#root::sql::Pool,
6009 ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6010 {
6011 use #root::sql::FetcherPool as _;
6012 let _pk_val: #root::core::SqlValue = pk.into();
6013 let mut _rows: ::std::vec::Vec<Self> =
6014 #root::query::QuerySet::<Self>::default()
6015 .filter(::core::stringify!(#pk_ident), _pk_val)
6016 .limit(1)
6017 .fetch(pool)
6018 .await?;
6019 ::core::result::Result::Ok(_rows.into_iter().next())
6020 }
6021
6022 /// Look up the row whose primary key equals `pk`. Errors
6023 /// when no row matches — the throwing counterpart of
6024 /// [`Self::find_pool`]. Eloquent `Model::findOrFail` /
6025 /// Django `Model.objects.get(pk=…)` (which raises
6026 /// `DoesNotExist`) parity.
6027 ///
6028 /// Translates the miss into
6029 /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6030 /// so callers can `?`-bubble straight through the typical
6031 /// `ExecError` error chain.
6032 ///
6033 /// # Errors
6034 /// As [`Self::find_pool`]; additionally
6035 /// [`sqlx::Error::RowNotFound`] when no row matches.
6036 ///
6037 /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6038 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6039 pub async fn find_or_fail(
6040 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6041 pool: &#root::sql::Pool,
6042 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6043 match Self::find(pk, pool).await? {
6044 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6045 ::core::option::Option::None => ::core::result::Result::Err(
6046 #root::sql::ExecError::Driver(
6047 #root::sql::sqlx::Error::RowNotFound,
6048 ),
6049 ),
6050 }
6051 }
6052
6053 /// Look up multiple rows by primary key, **failing** if
6054 /// any requested PK is missing. Eloquent
6055 /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6056 /// inventory order (NOT request order — same as
6057 /// [`Self::find_many`]).
6058 ///
6059 /// Empty `pks` returns an empty `Vec` (no rows requested
6060 /// → nothing to fail on).
6061 ///
6062 /// Differs from [`Self::find_many`] in that this method
6063 /// requires every PK to resolve. If even one is missing,
6064 /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6065 /// in `ExecError::Driver`). Rows are deduped at the SQL
6066 /// layer, so passing the same PK twice in `pks` counts as
6067 /// one expected row.
6068 ///
6069 /// # Errors
6070 /// As [`Self::find_many`], plus `RowNotFound` when the
6071 /// returned row count is less than the count of distinct
6072 /// requested PKs.
6073 pub async fn find_many_or_fail<V>(
6074 pks: impl ::core::iter::IntoIterator<Item = V>,
6075 pool: &#root::sql::Pool,
6076 ) -> ::core::result::Result<
6077 ::std::vec::Vec<Self>,
6078 #root::sql::ExecError,
6079 >
6080 where
6081 V: ::core::convert::Into<#root::core::SqlValue>,
6082 {
6083 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6084 pks.into_iter().map(::core::convert::Into::into).collect();
6085 if _values.is_empty() {
6086 return ::core::result::Result::Ok(::std::vec::Vec::new());
6087 }
6088 // Dedup the requested PK list before counting so
6089 // duplicate-PK requests don't false-fail (the SQL
6090 // `IN (...)` clause naturally dedups too).
6091 let mut _seen: ::std::collections::HashSet<
6092 ::std::string::String,
6093 > = ::std::collections::HashSet::new();
6094 for v in &_values {
6095 _seen.insert(v.to_display_string());
6096 }
6097 let _expected = _seen.len();
6098 let _rows = Self::find_many(_values, pool).await?;
6099 if _rows.len() < _expected {
6100 return ::core::result::Result::Err(
6101 #root::sql::ExecError::Driver(
6102 #root::sql::sqlx::Error::RowNotFound,
6103 ),
6104 );
6105 }
6106 ::core::result::Result::Ok(_rows)
6107 }
6108
6109 /// Find by primary key or run `fallback` to produce a
6110 /// default row to return. Eloquent
6111 /// `Model::findOr($pk, fn() => …)` parity.
6112 ///
6113 /// Unlike [`Self::find_or_fail_pool`] (which raises on
6114 /// miss), this is the "give me something sensible"
6115 /// branch: typical use is "fetch the user's row, else
6116 /// fall back to an anonymous/guest stub".
6117 ///
6118 /// `fallback` runs only when no row matches — the DB
6119 /// round-trip happens unconditionally.
6120 ///
6121 /// # Errors
6122 /// As [`Self::find_pool`].
6123 pub async fn find_or<F>(
6124 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6125 pool: &#root::sql::Pool,
6126 fallback: F,
6127 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6128 where
6129 F: ::core::ops::FnOnce() -> Self,
6130 {
6131 ::core::result::Result::Ok(
6132 Self::find(pk, pool).await?.unwrap_or_else(fallback),
6133 )
6134 }
6135
6136 /// Same as [`Self::find_or`] but also returns a `bool`
6137 /// indicating whether the row was found in the DB
6138 /// (`true`) or freshly built from `fallback` (`false`).
6139 /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6140 /// in PHP the returned model also exposes `->exists`;
6141 /// here that's surfaced as the second tuple element.
6142 ///
6143 /// Useful for edit-or-create form handlers where the
6144 /// caller needs to know whether to PATCH or POST when
6145 /// the user submits the form.
6146 ///
6147 /// # Errors
6148 /// As [`Self::find`].
6149 pub async fn find_or_new<F>(
6150 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6151 pool: &#root::sql::Pool,
6152 fallback: F,
6153 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6154 where
6155 F: ::core::ops::FnOnce() -> Self,
6156 {
6157 match Self::find(pk, pool).await? {
6158 ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6159 ::core::option::Option::None => {
6160 ::core::result::Result::Ok((fallback(), false))
6161 }
6162 }
6163 }
6164
6165 /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6166 /// [`Self::find_or_new`] but **persists** the new row
6167 /// when the PK isn't found. Returns `(row, exists: bool)`
6168 /// — `exists=true` when the PK was found,
6169 /// `false` when a fresh row was inserted.
6170 ///
6171 /// ```ignore
6172 /// let (post, found) = Post::find_or_insert(
6173 /// pk,
6174 /// &pool,
6175 /// || Post { id: Auto::default(), title: "new".into() },
6176 /// ).await?;
6177 /// // `found` true → returned existing row;
6178 /// // `found` false → fallback was inserted, `post.id` populated.
6179 /// ```
6180 ///
6181 /// **Caveat**: two concurrent calls that both miss the
6182 /// find can both INSERT, violating uniqueness. Wrap in a
6183 /// transaction or rely on a UNIQUE constraint + handle
6184 /// the conflict error if you need race-free semantics.
6185 ///
6186 /// # Errors
6187 /// As [`Self::find`] and [`Self::save_pool`].
6188 pub async fn find_or_insert<F>(
6189 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6190 pool: &#root::sql::Pool,
6191 fallback: F,
6192 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6193 where
6194 F: ::core::ops::FnOnce() -> Self,
6195 {
6196 if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6197 return ::core::result::Result::Ok((_row, true));
6198 }
6199 let mut _new = fallback();
6200 _new.save_pool(pool).await?;
6201 ::core::result::Result::Ok((_new, false))
6202 }
6203
6204 /// Fetch the first row of the table, or run `fallback`
6205 /// when the table is empty. Eloquent
6206 /// `Model::firstOr(fn() => …)` parity.
6207 ///
6208 /// # Errors
6209 /// As [`Self::first_pool`].
6210 pub async fn first_or<F>(
6211 pool: &#root::sql::Pool,
6212 fallback: F,
6213 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6214 where
6215 F: ::core::ops::FnOnce() -> Self,
6216 {
6217 // Route through the queryset rather than `Self::first`,
6218 // which is suppressed on models with a field named
6219 // `first` (field/shortcut collision guard).
6220 ::core::result::Result::Ok(
6221 #root::query::QuerySet::<Self>::default()
6222 .first(pool)
6223 .await?
6224 .unwrap_or_else(fallback),
6225 )
6226 }
6227
6228 /// Fetch exactly one row matching `<col> = <val>`. Errors
6229 /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6230 /// or more than one matches
6231 /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6232 /// Eloquent `Model::sole($col, $val)` parity.
6233 ///
6234 /// # Errors
6235 /// As [`Self::where_pool`] plus the explicit
6236 /// `RowNotFound` / `MultipleRowsReturned` cases above.
6237 pub async fn sole(
6238 col: &str,
6239 val: impl ::core::convert::Into<#root::core::SqlValue>,
6240 pool: &#root::sql::Pool,
6241 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6242 let mut _rows = Self::where_(col, val, pool).await?;
6243 match _rows.len() {
6244 0 => ::core::result::Result::Err(
6245 #root::sql::ExecError::Driver(
6246 #root::sql::sqlx::Error::RowNotFound,
6247 ),
6248 ),
6249 1 => ::core::result::Result::Ok(_rows.remove(0)),
6250 n => ::core::result::Result::Err(
6251 #root::sql::ExecError::MultipleRowsReturned {
6252 op: "sole",
6253 table: <Self as #root::core::Model>::SCHEMA.name,
6254 count: n,
6255 },
6256 ),
6257 }
6258 }
6259 }
6260 } else {
6261 quote!()
6262 };
6263
6264 // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6265 // the non-audited `_pool` methods but execute against an open
6266 // `PoolTx` so the writes participate in the caller's transaction.
6267 // Auditing inside TX is deferred; these always use the plain
6268 // executor primitives regardless of whether the model is audited.
6269 let tx_insert_method = if fields.has_auto {
6270 let pushes = &fields.insert_pushes;
6271 let returning_cols = &fields.returning_cols;
6272 quote! {
6273 /// Insert this row inside an open transaction, populating
6274 /// any `Auto<T>` PK from the auto-assigned value. Works
6275 /// against any backend that `tx` wraps.
6276 ///
6277 /// # Errors
6278 /// As [`Self::insert_pool`].
6279 pub async fn insert_tx(
6280 &mut self,
6281 tx: &mut #root::sql::PoolTx<'_>,
6282 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6283 let mut _columns: ::std::vec::Vec<&'static str> =
6284 ::std::vec::Vec::new();
6285 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6286 ::std::vec::Vec::new();
6287 #( #pushes )*
6288 let _query = #root::core::InsertQuery {
6289 model: <Self as #root::core::Model>::SCHEMA,
6290 columns: _columns,
6291 values: _values,
6292 returning: ::std::vec![ #( #returning_cols ),* ],
6293 on_conflict: ::core::option::Option::None,
6294 };
6295 let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6296 #root::sql::apply_auto_pk(_result, self)
6297 }
6298 }
6299 } else {
6300 let insert_columns = &fields.insert_columns;
6301 let insert_values = &fields.insert_values;
6302 quote! {
6303 /// Insert this row inside an open transaction.
6304 ///
6305 /// # Errors
6306 /// As [`Self::insert_pool`].
6307 pub async fn insert_tx(
6308 &self,
6309 tx: &mut #root::sql::PoolTx<'_>,
6310 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6311 let _query = #root::core::InsertQuery {
6312 model: <Self as #root::core::Model>::SCHEMA,
6313 columns: ::std::vec![ #( #insert_columns ),* ],
6314 values: ::std::vec![ #( #insert_values ),* ],
6315 returning: ::std::vec::Vec::new(),
6316 on_conflict: ::core::option::Option::None,
6317 };
6318 #root::sql::insert_tx(tx, &_query).await
6319 }
6320 }
6321 };
6322
6323 let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6324 let pk_column_lit = pk_col.as_str();
6325 let assignments = &fields.update_assignments;
6326 let dispatch_unset = if fields.pk_is_auto {
6327 quote! {
6328 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6329 return self.insert_tx(tx).await.map(|()| 1u64);
6330 }
6331 }
6332 } else {
6333 quote!()
6334 };
6335 quote! {
6336 /// Save this row inside an open transaction. `INSERT` when
6337 /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6338 /// PK. Works against any backend that `tx` wraps.
6339 ///
6340 /// # Errors
6341 /// As [`Self::save_pool`].
6342 pub async fn save_tx(
6343 &mut self,
6344 tx: &mut #root::sql::PoolTx<'_>,
6345 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6346 #dispatch_unset
6347 let _query = #root::core::UpdateQuery {
6348 model: <Self as #root::core::Model>::SCHEMA,
6349 set: ::std::vec![ #( #assignments ),* ],
6350 where_clause: #root::core::WhereExpr::Predicate(
6351 #root::core::Filter {
6352 column: #pk_column_lit,
6353 op: #root::core::Op::Eq,
6354 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6355 ::core::clone::Clone::clone(&self.#pk_ident)
6356 ),
6357 }
6358 ),
6359 };
6360 let _affected = #root::sql::update_tx(tx, &_query).await?;
6361 ::core::result::Result::Ok(_affected)
6362 }
6363 }
6364 } else {
6365 quote!()
6366 };
6367
6368 let tx_delete_method = {
6369 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6370 let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6371 if let Some(pk_ident) = pk_ident_for_tx {
6372 quote! {
6373 /// Delete the row identified by this instance's PK
6374 /// inside an open transaction. Works against any backend
6375 /// that `tx` wraps.
6376 ///
6377 /// # Errors
6378 /// As [`Self::delete_pool`].
6379 pub async fn delete_tx(
6380 &self,
6381 tx: &mut #root::sql::PoolTx<'_>,
6382 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6383 let _query = #root::core::DeleteQuery {
6384 model: <Self as #root::core::Model>::SCHEMA,
6385 where_clause: #root::core::WhereExpr::Predicate(
6386 #root::core::Filter {
6387 column: #pk_column_lit,
6388 op: #root::core::Op::Eq,
6389 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6390 ::core::clone::Clone::clone(&self.#pk_ident)
6391 ),
6392 }
6393 ),
6394 };
6395 #root::sql::delete_tx(tx, &_query).await
6396 }
6397 }
6398 } else {
6399 quote!()
6400 }
6401 };
6402
6403 // Update emission captures both BEFORE and AFTER state — runs an
6404 // extra SELECT against `_executor` BEFORE the UPDATE, captures
6405 // each tracked field's prior value, then after the UPDATE diffs
6406 // against the in-memory `&self`. `diff_changes` drops unchanged
6407 // columns so the JSON only contains the actual delta.
6408 //
6409 // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6410 // and binds `_audit_before_pairs`; `audit_update_post` runs
6411 // after the UPDATE and emits the PendingEntry.
6412 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6413 audited_fields
6414 {
6415 if tracked.is_empty() {
6416 (quote!(), quote!())
6417 } else {
6418 let select_cols: String = tracked
6419 .iter()
6420 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
6421 .collect::<Vec<_>>()
6422 .join(", ");
6423 let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6424 let select_cols_lit = select_cols;
6425 let pk_column_lit_for_select = pk_column_for_select;
6426 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6427 if fields.pk_is_auto {
6428 quote!(self.#pk_ident.get().copied().unwrap_or_default())
6429 } else {
6430 quote!(::core::clone::Clone::clone(&self.#pk_ident))
6431 }
6432 } else {
6433 quote!(0_i64)
6434 };
6435 let before_pairs = tracked.iter().map(|c| {
6436 let column_lit = c.column.as_str();
6437 let value_ty = &c.value_ty;
6438 quote! {
6439 (
6440 #column_lit,
6441 match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6442 &_audit_before_row, #column_lit,
6443 ) {
6444 ::core::result::Result::Ok(v) => {
6445 #root::__serde_json::to_value(&v)
6446 .unwrap_or(#root::__serde_json::Value::Null)
6447 }
6448 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6449 },
6450 )
6451 }
6452 });
6453 let after_pairs = tracked.iter().map(|c| {
6454 let column_lit = c.column.as_str();
6455 let ident = &c.ident;
6456 quote! {
6457 (
6458 #column_lit,
6459 #root::__serde_json::to_value(&self.#ident)
6460 .unwrap_or(#root::__serde_json::Value::Null),
6461 )
6462 }
6463 });
6464 let pk_str = audit_pk_to_string.clone();
6465 let pre = quote! {
6466 let _audit_select_sql = ::std::format!(
6467 r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
6468 #select_cols_lit,
6469 <Self as #root::core::Model>::SCHEMA.table,
6470 #pk_column_lit_for_select,
6471 );
6472 let _audit_before_pairs:
6473 ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6474 match #root::sql::sqlx::query(&_audit_select_sql)
6475 .bind(#pk_value_for_bind)
6476 .fetch_optional(&mut *_executor)
6477 .await
6478 {
6479 ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
6480 ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
6481 }
6482 _ => ::core::option::Option::None,
6483 };
6484 };
6485 let post = quote! {
6486 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6487 let _audit_after:
6488 ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
6489 ::std::vec![ #( #after_pairs ),* ];
6490 let _audit_entry = #root::audit::PendingEntry {
6491 entity_table: <Self as #root::core::Model>::SCHEMA.table,
6492 entity_pk: #pk_str,
6493 operation: #root::audit::AuditOp::Update,
6494 source: #root::audit::current_source(),
6495 changes: #root::audit::diff_changes(
6496 &_audit_before,
6497 &_audit_after,
6498 ),
6499 };
6500 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6501 }
6502 };
6503 (pre, post)
6504 }
6505 } else {
6506 (quote!(), quote!())
6507 };
6508
6509 // Bulk-insert audit: capture every row's tracked fields after the
6510 // RETURNING populates each PK, then push one batched INSERT INTO
6511 // audit_log via `emit_many`. One round-trip regardless of N rows.
6512 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6513 let row_pk_str = if let Some((pk_ident, _)) = primary_key {
6514 if fields.pk_is_auto {
6515 quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
6516 } else {
6517 quote!(::std::format!("{}", &_row.#pk_ident))
6518 }
6519 } else {
6520 quote!(::std::string::String::new())
6521 };
6522 let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
6523 let column_lit = c.column.as_str();
6524 let ident = &c.ident;
6525 quote! {
6526 (
6527 #column_lit,
6528 #root::__serde_json::to_value(&_row.#ident)
6529 .unwrap_or(#root::__serde_json::Value::Null),
6530 )
6531 }
6532 });
6533 quote! {
6534 let _audit_source = #root::audit::current_source();
6535 let mut _audit_entries:
6536 ::std::vec::Vec<#root::audit::PendingEntry> =
6537 ::std::vec::Vec::with_capacity(rows.len());
6538 for _row in rows.iter() {
6539 _audit_entries.push(#root::audit::PendingEntry {
6540 entity_table: <Self as #root::core::Model>::SCHEMA.table,
6541 entity_pk: #row_pk_str,
6542 operation: #root::audit::AuditOp::Create,
6543 source: _audit_source.clone(),
6544 changes: #root::audit::snapshot_changes(&[
6545 #( #row_pairs ),*
6546 ]),
6547 });
6548 }
6549 #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6550 }
6551 } else {
6552 quote!()
6553 };
6554
6555 let save_method = if fields.pk_is_auto {
6556 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6557 let pk_column_lit = pk_column.as_str();
6558 let assignments = &fields.update_assignments;
6559 let upsert_cols = &fields.upsert_update_columns;
6560 let upsert_pushes = &fields.insert_pushes;
6561 let upsert_returning = &fields.returning_cols;
6562 let upsert_auto_assigns = &fields.auto_assigns;
6563 // Conflict target: prefer the first declared `unique_together`
6564 // when it exists. Plain `Auto<T>` PKs are server-assigned via
6565 // `BIGSERIAL` and never collide on insert, so a PK-only target
6566 // would silently turn `upsert()` into "always-insert" for
6567 // surrogate-PK models with composite UNIQUE constraints — see
6568 // `RolePermission` / `UserRole` / `UserPermission` in the
6569 // tenancy permission engine. When no `unique_together` is
6570 // declared we keep the PK target (the original behaviour).
6571 let upsert_target_columns: Vec<String> = indexes
6572 .iter()
6573 .find(|i| i.unique && !i.columns.is_empty())
6574 .map(|i| i.columns.clone())
6575 .unwrap_or_else(|| vec![pk_column.clone()]);
6576 let upsert_target_lits = upsert_target_columns
6577 .iter()
6578 .map(String::as_str)
6579 .collect::<Vec<_>>();
6580 let conflict_clause = if fields.upsert_update_columns.is_empty() {
6581 quote!(#root::core::ConflictClause::DoNothing)
6582 } else {
6583 quote!(#root::core::ConflictClause::DoUpdate {
6584 target: ::std::vec![ #( #upsert_target_lits ),* ],
6585 update_columns: ::std::vec![ #( #upsert_cols ),* ],
6586 })
6587 };
6588 Some(quote! {
6589 /// Insert this row if its `Auto<T>` primary key is
6590 /// `Unset`, otherwise update the existing row matching the
6591 /// PK. Mirrors Django's `save()` — caller doesn't need to
6592 /// pick `insert` vs the bulk-update path manually.
6593 ///
6594 /// On the insert branch, populates the PK from `RETURNING`
6595 /// (same behavior as `insert`). On the update branch,
6596 /// writes every non-PK column back; if no row matches the
6597 /// PK, returns `Ok(())` silently.
6598 ///
6599 /// Only generated when the primary key is declared as
6600 /// `Auto<T>`. Models with a manually-managed PK must use
6601 /// `insert` or the QuerySet update builder.
6602 ///
6603 /// # Errors
6604 /// Returns [`#root::sql::ExecError`] for SQL-writing
6605 /// or driver failures.
6606 #[cfg(feature = "postgres")]
6607 pub async fn save(
6608 &mut self,
6609 pool: &#root::sql::sqlx::PgPool,
6610 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6611 #pool_to_save_on
6612 }
6613
6614 /// Like [`Self::save`] but accepts any sqlx executor —
6615 /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6616 /// escape hatch for tenant-scoped writes: schema-mode
6617 /// tenants share the registry pool but rely on a per-
6618 /// checkout `SET search_path`, so passing `&PgPool` would
6619 /// silently hit the wrong schema. Acquire a connection
6620 /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6621 ///
6622 /// # Errors
6623 /// As [`Self::save`].
6624 #[cfg(feature = "postgres")]
6625 pub async fn save_on #executor_generics (
6626 &mut self,
6627 #executor_param,
6628 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6629 #executor_where
6630 {
6631 // #1029 — INSERT writes exactly one row → 1; UPDATE returns
6632 // the rows-affected count (0 when the PK no longer exists,
6633 // the Django 6.0 `Model.NotUpdated` signal).
6634 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6635 return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
6636 }
6637 #audit_update_pre
6638 let _query = #root::core::UpdateQuery {
6639 model: <Self as #root::core::Model>::SCHEMA,
6640 set: ::std::vec![ #( #assignments ),* ],
6641 where_clause: #root::core::WhereExpr::Predicate(
6642 #root::core::Filter {
6643 column: #pk_column_lit,
6644 op: #root::core::Op::Eq,
6645 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6646 ::core::clone::Clone::clone(&self.#pk_ident)
6647 ),
6648 }
6649 ),
6650 };
6651 let _affected = #root::sql::__macro_internals::update_on(
6652 #executor_passes_to_data_write,
6653 &_query,
6654 ).await?;
6655 #audit_update_post
6656 ::core::result::Result::Ok(_affected)
6657 }
6658
6659 /// Per-call override for the audit source. Runs
6660 /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6661 /// scope so the resulting audit entry records `source`
6662 /// instead of the task-local default. Useful for seed
6663 /// scripts and one-off CLI tools that don't sit inside an
6664 /// admin handler. The override applies only to this call;
6665 /// no global state changes.
6666 ///
6667 /// # Errors
6668 /// As [`Self::save_on`].
6669 #[cfg(feature = "postgres")]
6670 pub async fn save_on_with #executor_generics (
6671 &mut self,
6672 #executor_param,
6673 source: #root::audit::AuditSource,
6674 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6675 #executor_where
6676 {
6677 #root::audit::with_source(source, self.save_on(_executor)).await
6678 }
6679
6680 /// Insert this row or update it in-place if the primary key already
6681 /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6682 ///
6683 /// With `Auto::Unset` PK the server assigns a new key and no conflict
6684 /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6685 /// inserted if absent or all non-PK columns are overwritten if present.
6686 ///
6687 /// # Errors
6688 /// As [`Self::insert_on`].
6689 #[cfg(feature = "postgres")]
6690 pub async fn upsert(
6691 &mut self,
6692 pool: &#root::sql::sqlx::PgPool,
6693 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6694 #pool_to_upsert_on
6695 }
6696
6697 /// Like [`Self::upsert`] but accepts any sqlx executor.
6698 /// See [`Self::save_on`] for tenancy-scoped rationale.
6699 ///
6700 /// # Errors
6701 /// As [`Self::upsert`].
6702 #[cfg(feature = "postgres")]
6703 pub async fn upsert_on #executor_generics (
6704 &mut self,
6705 #executor_param,
6706 ) -> ::core::result::Result<(), #root::sql::ExecError>
6707 #executor_where
6708 {
6709 let mut _columns: ::std::vec::Vec<&'static str> =
6710 ::std::vec::Vec::new();
6711 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6712 ::std::vec::Vec::new();
6713 #( #upsert_pushes )*
6714 let query = #root::core::InsertQuery {
6715 model: <Self as #root::core::Model>::SCHEMA,
6716 columns: _columns,
6717 values: _values,
6718 returning: ::std::vec![ #( #upsert_returning ),* ],
6719 on_conflict: ::core::option::Option::Some(#conflict_clause),
6720 };
6721 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
6722 #executor_passes_to_data_write,
6723 &query,
6724 ).await?;
6725 let _returning_row = &_returning_row_v;
6726 #( #upsert_auto_assigns )*
6727 ::core::result::Result::Ok(())
6728 }
6729 })
6730 } else {
6731 None
6732 };
6733
6734 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6735 let pk_column_lit = pk_column.as_str();
6736 // Optional `soft_delete_on` / `restore_on` companions when the
6737 // model has a `#[rustango(soft_delete)]` column. They land
6738 // alongside the regular `delete_on` so callers have both
6739 // options — a hard delete (audit-tracked as a real DELETE) and
6740 // a logical delete (audit-tracked as an UPDATE setting the
6741 // deleted_at column to NOW()).
6742 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6743 let col_lit = col;
6744 let sd_field_ident = fields
6745 .soft_delete_field_ident
6746 .clone()
6747 .expect("soft_delete_column without ident");
6748 quote! {
6749 /// Soft-delete this row by setting its
6750 /// `#[rustango(soft_delete)]` column to `NOW()`.
6751 /// Mirrors Django's `SoftDeleteModel.delete()` shape:
6752 /// the row stays in the table; query helpers can
6753 /// filter it out by checking the column for `IS NOT
6754 /// NULL`.
6755 ///
6756 /// # Errors
6757 /// As [`Self::delete`].
6758 pub async fn soft_delete_on #executor_generics (
6759 &self,
6760 #executor_param,
6761 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6762 #executor_where
6763 {
6764 let _query = #root::core::UpdateQuery {
6765 model: <Self as #root::core::Model>::SCHEMA,
6766 set: ::std::vec![
6767 #root::core::Assignment {
6768 column: #col_lit,
6769 value: ::core::convert::Into::<#root::core::Expr>::into(
6770 ::core::convert::Into::<#root::core::SqlValue>::into(
6771 #root::__chrono::Utc::now()
6772 )
6773 ),
6774 },
6775 ],
6776 where_clause: #root::core::WhereExpr::Predicate(
6777 #root::core::Filter {
6778 column: #pk_column_lit,
6779 op: #root::core::Op::Eq,
6780 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6781 ::core::clone::Clone::clone(&self.#pk_ident)
6782 ),
6783 }
6784 ),
6785 };
6786 let _affected = #root::sql::__macro_internals::update_on(
6787 #executor_passes_to_data_write,
6788 &_query,
6789 ).await?;
6790 #audit_softdelete_emit
6791 ::core::result::Result::Ok(_affected)
6792 }
6793
6794 /// Inverse of [`Self::soft_delete_on`] — clears the
6795 /// soft-delete column back to NULL so the row is
6796 /// considered live again.
6797 ///
6798 /// # Errors
6799 /// As [`Self::delete`].
6800 pub async fn restore_on #executor_generics (
6801 &self,
6802 #executor_param,
6803 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6804 #executor_where
6805 {
6806 let _query = #root::core::UpdateQuery {
6807 model: <Self as #root::core::Model>::SCHEMA,
6808 set: ::std::vec![
6809 #root::core::Assignment {
6810 column: #col_lit,
6811 value: ::core::convert::Into::<#root::core::Expr>::into(
6812 #root::core::SqlValue::Null
6813 ),
6814 },
6815 ],
6816 where_clause: #root::core::WhereExpr::Predicate(
6817 #root::core::Filter {
6818 column: #pk_column_lit,
6819 op: #root::core::Op::Eq,
6820 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6821 ::core::clone::Clone::clone(&self.#pk_ident)
6822 ),
6823 }
6824 ),
6825 };
6826 let _affected = #root::sql::__macro_internals::update_on(
6827 #executor_passes_to_data_write,
6828 &_query,
6829 ).await?;
6830 #audit_restore_emit
6831 ::core::result::Result::Ok(_affected)
6832 }
6833
6834 /// Tri-dialect counterpart of [`Self::soft_delete_on`]
6835 /// — takes [`#root::sql::Pool`] and dispatches per
6836 /// backend. Eloquent `Model::delete()` semantics on
6837 /// soft-delete-enabled models (closes #821 partial).
6838 ///
6839 /// Sets the `#[rustango(soft_delete)]` column to
6840 /// `NOW()` on every backend. Query helpers
6841 /// (`QuerySet::active()` / `only_trashed()`,
6842 /// `soft_delete::active_filter` /
6843 /// `compose_with_active`) filter trashed rows out by
6844 /// reading `IS NULL` on the same column.
6845 ///
6846 /// # Errors
6847 /// As [`#root::sql::update_pool`].
6848 pub async fn soft_delete(
6849 &self,
6850 pool: &#root::sql::Pool,
6851 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6852 let _query = #root::core::UpdateQuery {
6853 model: <Self as #root::core::Model>::SCHEMA,
6854 set: ::std::vec![
6855 #root::core::Assignment {
6856 column: #col_lit,
6857 value: ::core::convert::Into::<#root::core::Expr>::into(
6858 ::core::convert::Into::<#root::core::SqlValue>::into(
6859 #root::__chrono::Utc::now()
6860 )
6861 ),
6862 },
6863 ],
6864 where_clause: #root::core::WhereExpr::Predicate(
6865 #root::core::Filter {
6866 column: #pk_column_lit,
6867 op: #root::core::Op::Eq,
6868 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6869 ::core::clone::Clone::clone(&self.#pk_ident)
6870 ),
6871 }
6872 ),
6873 };
6874 #root::sql::update_pool(pool, &_query).await
6875 }
6876
6877 /// Tri-dialect counterpart of [`Self::restore_on`].
6878 /// Clears the `#[rustango(soft_delete)]` column back
6879 /// to `NULL`, marking the row live again. Eloquent
6880 /// `Model::restore()` parity.
6881 ///
6882 /// # Errors
6883 /// As [`#root::sql::update_pool`].
6884 pub async fn restore(
6885 &self,
6886 pool: &#root::sql::Pool,
6887 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6888 let _query = #root::core::UpdateQuery {
6889 model: <Self as #root::core::Model>::SCHEMA,
6890 set: ::std::vec![
6891 #root::core::Assignment {
6892 column: #col_lit,
6893 value: ::core::convert::Into::<#root::core::Expr>::into(
6894 #root::core::SqlValue::Null
6895 ),
6896 },
6897 ],
6898 where_clause: #root::core::WhereExpr::Predicate(
6899 #root::core::Filter {
6900 column: #pk_column_lit,
6901 op: #root::core::Op::Eq,
6902 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6903 ::core::clone::Clone::clone(&self.#pk_ident)
6904 ),
6905 }
6906 ),
6907 };
6908 #root::sql::update_pool(pool, &_query).await
6909 }
6910
6911 /// Hard-delete this row, ignoring the soft-delete
6912 /// column. Eloquent `Model::forceDelete()` parity —
6913 /// the escape hatch when you need to actually purge
6914 /// data (GDPR, fixture cleanup, etc.).
6915 ///
6916 /// Equivalent to [`Self::delete_pool`] (the framework's
6917 /// non-soft delete) — exposed under the Eloquent name
6918 /// for muscle-memory + so soft-delete-enabled models
6919 /// have all three operations (soft / restore / force)
6920 /// in one place.
6921 ///
6922 /// # Errors
6923 /// As [`Self::delete_pool`].
6924 pub async fn force_delete(
6925 &self,
6926 pool: &#root::sql::Pool,
6927 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6928 Self::delete_pool(self, pool).await
6929 }
6930
6931 /// Returns `true` when this row is soft-deleted (its
6932 /// `#[rustango(soft_delete)]` column is currently
6933 /// set — Eloquent `$model->trashed()` parity).
6934 ///
6935 /// Pure in-memory predicate over `&self`; does not
6936 /// hit the database. Useful in admin/template code
6937 /// like `{% if post.trashed() %}…{% endif %}` and in
6938 /// guard clauses on restore/force-delete flows.
6939 pub fn trashed(&self) -> bool {
6940 ::core::option::Option::is_some(&self.#sd_field_ident)
6941 }
6942
6943 /// Fetch every row whose `#[rustango(soft_delete)]`
6944 /// column is `NULL` (a.k.a. the live, non-trashed
6945 /// rows). Eloquent's default `Model::all()` behavior on
6946 /// a soft-delete model (Eloquent auto-scopes trashed
6947 /// rows out; rustango doesn't have auto-scoping yet —
6948 /// see issue #820 — so this is the explicit shortcut).
6949 ///
6950 /// One-liner over `QuerySet::<Self>::default()
6951 /// .active().fetch(pool)`. Closes #821 partial.
6952 ///
6953 /// # Errors
6954 /// As [`#root::sql::FetcherPool::fetch`].
6955 pub async fn active(
6956 pool: &#root::sql::Pool,
6957 ) -> ::core::result::Result<
6958 ::std::vec::Vec<Self>,
6959 #root::sql::ExecError,
6960 > {
6961 use #root::sql::FetcherPool as _;
6962 #root::query::QuerySet::<Self>::default()
6963 .active()
6964 .fetch(pool)
6965 .await
6966 }
6967
6968 /// Fetch ONLY soft-deleted rows. Eloquent
6969 /// `Model::onlyTrashed()->get()` parity — drives the
6970 /// admin "Trash" page, restore flows, GDPR purge
6971 /// scans, etc.
6972 ///
6973 /// One-liner over `QuerySet::<Self>::default()
6974 /// .only_trashed().fetch(pool)`. Closes #821
6975 /// partial.
6976 ///
6977 /// # Errors
6978 /// As [`#root::sql::FetcherPool::fetch`].
6979 pub async fn only_trashed(
6980 pool: &#root::sql::Pool,
6981 ) -> ::core::result::Result<
6982 ::std::vec::Vec<Self>,
6983 #root::sql::ExecError,
6984 > {
6985 use #root::sql::FetcherPool as _;
6986 #root::query::QuerySet::<Self>::default()
6987 .only_trashed()
6988 .fetch(pool)
6989 .await
6990 }
6991
6992 /// Fetch every row, both live and soft-deleted.
6993 /// Eloquent `Model::withTrashed()->get()` parity.
6994 ///
6995 /// Today every queryset already includes trashed rows
6996 /// (rustango has no global-scope tracking yet — issue
6997 /// #820), so this is functionally equivalent to
6998 /// [`Self::all_pool`]. Exposed as a named shortcut so
6999 /// soft-delete-aware code reads `Model::with_trashed_pool`
7000 /// rather than `Model::all_pool` — keeps intent visible
7001 /// in callers and stays correct when auto-scoping lands.
7002 ///
7003 /// Closes #821 partial.
7004 ///
7005 /// # Errors
7006 /// As [`#root::sql::FetcherPool::fetch`].
7007 pub async fn with_trashed(
7008 pool: &#root::sql::Pool,
7009 ) -> ::core::result::Result<
7010 ::std::vec::Vec<Self>,
7011 #root::sql::ExecError,
7012 > {
7013 use #root::sql::FetcherPool as _;
7014 #root::query::QuerySet::<Self>::default()
7015 .with_trashed()
7016 .fetch(pool)
7017 .await
7018 }
7019
7020 }
7021 } else {
7022 quote!()
7023 };
7024 quote! {
7025 /// Delete the row identified by this instance's primary key.
7026 ///
7027 /// Returns the number of rows affected (0 or 1).
7028 ///
7029 /// # Errors
7030 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7031 /// driver failures.
7032 #[cfg(feature = "postgres")]
7033 pub async fn delete(
7034 &self,
7035 pool: &#root::sql::sqlx::PgPool,
7036 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7037 #pool_to_delete_on
7038 }
7039
7040 /// Like [`Self::delete`] but accepts any sqlx executor —
7041 /// for tenant-scoped deletes against an explicitly-acquired
7042 /// connection. See [`Self::save_on`] for the rationale.
7043 ///
7044 /// # Errors
7045 /// As [`Self::delete`].
7046 #[cfg(feature = "postgres")]
7047 pub async fn delete_on #executor_generics (
7048 &self,
7049 #executor_param,
7050 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7051 #executor_where
7052 {
7053 let query = #root::core::DeleteQuery {
7054 model: <Self as #root::core::Model>::SCHEMA,
7055 where_clause: #root::core::WhereExpr::Predicate(
7056 #root::core::Filter {
7057 column: #pk_column_lit,
7058 op: #root::core::Op::Eq,
7059 value: ::core::convert::Into::<#root::core::SqlValue>::into(
7060 ::core::clone::Clone::clone(&self.#pk_ident)
7061 ),
7062 }
7063 ),
7064 };
7065 let _affected = #root::sql::__macro_internals::delete_on(
7066 #executor_passes_to_data_write,
7067 &query,
7068 ).await?;
7069 #audit_delete_emit
7070 ::core::result::Result::Ok(_affected)
7071 }
7072
7073 /// Per-call audit-source override for [`Self::delete_on`].
7074 /// See [`Self::save_on_with`] for shape rationale.
7075 ///
7076 /// # Errors
7077 /// As [`Self::delete_on`].
7078 #[cfg(feature = "postgres")]
7079 pub async fn delete_on_with #executor_generics (
7080 &self,
7081 #executor_param,
7082 source: #root::audit::AuditSource,
7083 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7084 #executor_where
7085 {
7086 #root::audit::with_source(source, self.delete_on(_executor)).await
7087 }
7088 #pool_delete_method
7089 #pool_insert_method
7090 #pool_save_method
7091 #refresh_replicate_methods
7092 #tx_delete_method
7093 #tx_insert_method
7094 #tx_save_method
7095 #soft_delete_methods
7096
7097 /// Returns `true` when `other` represents the same DB
7098 /// row as `self` — i.e. their primary keys compare
7099 /// equal. Eloquent `$model->is($other)` parity.
7100 ///
7101 /// Because both arguments are typed `&Self`, the
7102 /// model/table check is automatic — `Post::is` cannot
7103 /// be invoked against a `Comment` at compile time. Only
7104 /// the PK has to be compared at runtime.
7105 pub fn is(&self, other: &Self) -> bool {
7106 self.#pk_ident == other.#pk_ident
7107 }
7108
7109 /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7110 /// parity.
7111 pub fn is_not(&self, other: &Self) -> bool {
7112 self.#pk_ident != other.#pk_ident
7113 }
7114
7115 /// Returns this row's primary-key value as an
7116 /// [`#root::core::SqlValue`]. Eloquent
7117 /// `$model->getKey()` parity.
7118 ///
7119 /// Useful when you need to thread the PK through a
7120 /// generic `Into<SqlValue>`-bound API without knowing the
7121 /// concrete PK type (`i64` vs `Uuid` vs `String`).
7122 #[must_use]
7123 pub fn get_key(&self) -> #root::core::SqlValue {
7124 ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7125 }
7126
7127 }
7128 });
7129
7130 let insert_method = if fields.has_auto {
7131 let pushes = &fields.insert_pushes;
7132 let returning_cols = &fields.returning_cols;
7133 let auto_assigns = &fields.auto_assigns;
7134 quote! {
7135 /// Insert this row into its table. Skips columns whose
7136 /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7137 /// sequence fills them in, then reads each `Auto` column
7138 /// back via `RETURNING` and stores it on `self`.
7139 ///
7140 /// # Errors
7141 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7142 /// driver failures.
7143 #[cfg(feature = "postgres")]
7144 pub async fn insert(
7145 &mut self,
7146 pool: &#root::sql::sqlx::PgPool,
7147 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7148 #pool_to_insert_on
7149 }
7150
7151 /// Like [`Self::insert`] but accepts any sqlx executor.
7152 /// See [`Self::save_on`] for tenancy-scoped rationale.
7153 ///
7154 /// # Errors
7155 /// As [`Self::insert`].
7156 #[cfg(feature = "postgres")]
7157 pub async fn insert_on #executor_generics (
7158 &mut self,
7159 #executor_param,
7160 ) -> ::core::result::Result<(), #root::sql::ExecError>
7161 #executor_where
7162 {
7163 let mut _columns: ::std::vec::Vec<&'static str> =
7164 ::std::vec::Vec::new();
7165 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7166 ::std::vec::Vec::new();
7167 #( #pushes )*
7168 let query = #root::core::InsertQuery {
7169 model: <Self as #root::core::Model>::SCHEMA,
7170 columns: _columns,
7171 values: _values,
7172 returning: ::std::vec![ #( #returning_cols ),* ],
7173 on_conflict: ::core::option::Option::None,
7174 };
7175 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7176 #executor_passes_to_data_write,
7177 &query,
7178 ).await?;
7179 let _returning_row = &_returning_row_v;
7180 #( #auto_assigns )*
7181 #audit_insert_emit
7182 ::core::result::Result::Ok(())
7183 }
7184
7185 /// Per-call audit-source override for [`Self::insert_on`].
7186 /// See [`Self::save_on_with`] for shape rationale.
7187 ///
7188 /// # Errors
7189 /// As [`Self::insert_on`].
7190 #[cfg(feature = "postgres")]
7191 pub async fn insert_on_with #executor_generics (
7192 &mut self,
7193 #executor_param,
7194 source: #root::audit::AuditSource,
7195 ) -> ::core::result::Result<(), #root::sql::ExecError>
7196 #executor_where
7197 {
7198 #root::audit::with_source(source, self.insert_on(_executor)).await
7199 }
7200 }
7201 } else {
7202 let insert_columns = &fields.insert_columns;
7203 let insert_values = &fields.insert_values;
7204 quote! {
7205 /// Insert this row into its table.
7206 ///
7207 /// # Errors
7208 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7209 /// driver failures.
7210 #[cfg(feature = "postgres")]
7211 pub async fn insert(
7212 &self,
7213 pool: &#root::sql::sqlx::PgPool,
7214 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7215 self.insert_on(pool).await
7216 }
7217
7218 /// Like [`Self::insert`] but accepts any sqlx executor.
7219 /// See [`Self::save_on`] for tenancy-scoped rationale.
7220 ///
7221 /// # Errors
7222 /// As [`Self::insert`].
7223 #[cfg(feature = "postgres")]
7224 pub async fn insert_on<'_c, _E>(
7225 &self,
7226 _executor: _E,
7227 ) -> ::core::result::Result<(), #root::sql::ExecError>
7228 where
7229 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7230 {
7231 let query = #root::core::InsertQuery {
7232 model: <Self as #root::core::Model>::SCHEMA,
7233 columns: ::std::vec![ #( #insert_columns ),* ],
7234 values: ::std::vec![ #( #insert_values ),* ],
7235 returning: ::std::vec::Vec::new(),
7236 on_conflict: ::core::option::Option::None,
7237 };
7238 #root::sql::__macro_internals::insert_on(_executor, &query).await
7239 }
7240 }
7241 };
7242
7243 let bulk_insert_method = if fields.has_auto {
7244 let cols_no_auto = &fields.bulk_columns_no_auto;
7245 let cols_all = &fields.bulk_columns_all;
7246 let pushes_no_auto = &fields.bulk_pushes_no_auto;
7247 let pushes_all = &fields.bulk_pushes_all;
7248 let returning_cols = &fields.returning_cols;
7249 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7250 let uniformity = &fields.bulk_auto_uniformity;
7251 let first_auto_ident = fields
7252 .first_auto_ident
7253 .as_ref()
7254 .expect("has_auto implies first_auto_ident is Some");
7255 quote! {
7256 /// Bulk-insert `rows` in a single round-trip. Every row's
7257 /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7258 /// (sequence fills them in) or uniformly `Auto::Set(_)`
7259 /// (caller-supplied values). Mixed Set/Unset is rejected
7260 /// — call `insert` per row for that case.
7261 ///
7262 /// Empty slice is a no-op. Each row's `Auto` fields are
7263 /// populated from the `RETURNING` clause in input order
7264 /// before this returns.
7265 ///
7266 /// # Errors
7267 /// Returns [`#root::sql::ExecError`] for validation,
7268 /// SQL-writing, mixed-Auto rejection, or driver failures.
7269 #[cfg(feature = "postgres")]
7270 pub async fn bulk_insert(
7271 rows: &mut [Self],
7272 pool: &#root::sql::sqlx::PgPool,
7273 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7274 #pool_to_bulk_insert_on
7275 }
7276
7277 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7278 /// See [`Self::save_on`] for tenancy-scoped rationale.
7279 ///
7280 /// # Errors
7281 /// As [`Self::bulk_insert`].
7282 #[cfg(feature = "postgres")]
7283 pub async fn bulk_insert_on #executor_generics (
7284 rows: &mut [Self],
7285 #executor_param,
7286 ) -> ::core::result::Result<(), #root::sql::ExecError>
7287 #executor_where
7288 {
7289 if rows.is_empty() {
7290 return ::core::result::Result::Ok(());
7291 }
7292 let _first_unset = matches!(
7293 rows[0].#first_auto_ident,
7294 #root::sql::Auto::Unset
7295 );
7296 #( #uniformity )*
7297
7298 let mut _all_rows: ::std::vec::Vec<
7299 ::std::vec::Vec<#root::core::SqlValue>,
7300 > = ::std::vec::Vec::with_capacity(rows.len());
7301 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7302 for _row in rows.iter() {
7303 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7304 ::std::vec::Vec::new();
7305 #( #pushes_no_auto )*
7306 _all_rows.push(_row_vals);
7307 }
7308 ::std::vec![ #( #cols_no_auto ),* ]
7309 } else {
7310 for _row in rows.iter() {
7311 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7312 ::std::vec::Vec::new();
7313 #( #pushes_all )*
7314 _all_rows.push(_row_vals);
7315 }
7316 ::std::vec![ #( #cols_all ),* ]
7317 };
7318
7319 let _query = #root::core::BulkInsertQuery {
7320 model: <Self as #root::core::Model>::SCHEMA,
7321 columns: _columns,
7322 rows: _all_rows,
7323 returning: ::std::vec![ #( #returning_cols ),* ],
7324 on_conflict: ::core::option::Option::None,
7325 };
7326 let _returned = #root::sql::__macro_internals::bulk_insert_on(
7327 #executor_passes_to_data_write,
7328 &_query,
7329 ).await?;
7330 if _returned.len() != rows.len() {
7331 return ::core::result::Result::Err(
7332 #root::sql::ExecError::Sql(
7333 #root::sql::SqlError::BulkInsertReturningMismatch {
7334 expected: rows.len(),
7335 actual: _returned.len(),
7336 }
7337 )
7338 );
7339 }
7340 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7341 #auto_assigns_for_row
7342 }
7343 #audit_bulk_insert_emit
7344 ::core::result::Result::Ok(())
7345 }
7346 }
7347 } else {
7348 let cols_all = &fields.bulk_columns_all;
7349 let pushes_all = &fields.bulk_pushes_all;
7350 quote! {
7351 /// Bulk-insert `rows` in a single round-trip. Every row's
7352 /// fields are written verbatim — there are no `Auto<T>`
7353 /// fields on this model.
7354 ///
7355 /// Empty slice is a no-op.
7356 ///
7357 /// # Errors
7358 /// Returns [`#root::sql::ExecError`] for validation,
7359 /// SQL-writing, or driver failures.
7360 #[cfg(feature = "postgres")]
7361 pub async fn bulk_insert(
7362 rows: &[Self],
7363 pool: &#root::sql::sqlx::PgPool,
7364 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7365 Self::bulk_insert_on(rows, pool).await
7366 }
7367
7368 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7369 /// See [`Self::save_on`] for tenancy-scoped rationale.
7370 ///
7371 /// # Errors
7372 /// As [`Self::bulk_insert`].
7373 #[cfg(feature = "postgres")]
7374 pub async fn bulk_insert_on<'_c, _E>(
7375 rows: &[Self],
7376 _executor: _E,
7377 ) -> ::core::result::Result<(), #root::sql::ExecError>
7378 where
7379 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7380 {
7381 if rows.is_empty() {
7382 return ::core::result::Result::Ok(());
7383 }
7384 let mut _all_rows: ::std::vec::Vec<
7385 ::std::vec::Vec<#root::core::SqlValue>,
7386 > = ::std::vec::Vec::with_capacity(rows.len());
7387 for _row in rows.iter() {
7388 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7389 ::std::vec::Vec::new();
7390 #( #pushes_all )*
7391 _all_rows.push(_row_vals);
7392 }
7393 let _query = #root::core::BulkInsertQuery {
7394 model: <Self as #root::core::Model>::SCHEMA,
7395 columns: ::std::vec![ #( #cols_all ),* ],
7396 rows: _all_rows,
7397 returning: ::std::vec::Vec::new(),
7398 on_conflict: ::core::option::Option::None,
7399 };
7400 let _ = #root::sql::__macro_internals::bulk_insert_on(_executor, &_query).await?;
7401 ::core::result::Result::Ok(())
7402 }
7403 }
7404 };
7405
7406 // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7407 // (no postgres-feature gate); routes through the existing
7408 // `bulk_insert_pool` + per-dialect conflict writer.
7409 //
7410 // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7411 // sequence picks the PK for fresh inserts; the UPDATE branch never
7412 // touches the Auto column.
7413 let bulk_upsert_pool_method = {
7414 // Pick the "no Auto" columns when the model has Auto fields,
7415 // else every column.
7416 let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7417 (
7418 fields.bulk_columns_no_auto.clone(),
7419 fields.bulk_pushes_no_auto.clone(),
7420 )
7421 } else {
7422 (
7423 fields.bulk_columns_all.clone(),
7424 fields.bulk_pushes_all.clone(),
7425 )
7426 };
7427 quote! {
7428 /// Tri-dialect `bulk_create(update_conflicts=True)` — Django's
7429 /// canonical "import a batch idempotently" shape. Issue #267
7430 /// / T1.5.
7431 ///
7432 /// Per-row values are extracted and lowered into a
7433 /// [`#root::core::BulkInsertQuery`] with
7434 /// `on_conflict = DoUpdate { target, update_columns }`. The
7435 /// writer dispatches per-dialect:
7436 /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7437 /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)` (target ignored — MySQL matches every UNIQUE index)
7438 ///
7439 /// `target` names the column(s) whose unique constraint
7440 /// defines the conflict (typically a `unique` or
7441 /// `unique_together` natural-key column, NOT the `Auto<T>`
7442 /// PK). `update_cols` names the columns to overwrite on
7443 /// conflict — every other column is left untouched on the
7444 /// existing row.
7445 ///
7446 /// Auto-PK rows must all have `Auto::Unset` (the sequence
7447 /// picks the PK on insert; the update path never touches
7448 /// the Auto column). Auto-set rows trigger a hard error.
7449 /// Empty slice is a no-op.
7450 ///
7451 /// # Errors
7452 /// Returns [`#root::sql::ExecError`] for validation,
7453 /// SQL-writing, or driver failures.
7454 pub async fn bulk_upsert_pool(
7455 rows: &[Self],
7456 target: &[&'static str],
7457 update_cols: &[&'static str],
7458 pool: &#root::sql::Pool,
7459 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7460 if rows.is_empty() {
7461 return ::core::result::Result::Ok(());
7462 }
7463 let mut _all_rows: ::std::vec::Vec<
7464 ::std::vec::Vec<#root::core::SqlValue>,
7465 > = ::std::vec::Vec::with_capacity(rows.len());
7466 for _row in rows.iter() {
7467 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7468 ::std::vec::Vec::new();
7469 #( #upsert_pushes )*
7470 _all_rows.push(_row_vals);
7471 }
7472 let _query = #root::core::BulkInsertQuery {
7473 model: <Self as #root::core::Model>::SCHEMA,
7474 columns: ::std::vec![ #( #upsert_cols ),* ],
7475 rows: _all_rows,
7476 returning: ::std::vec::Vec::new(),
7477 on_conflict: ::core::option::Option::Some(
7478 #root::core::ConflictClause::DoUpdate {
7479 target: target.to_vec(),
7480 update_columns: update_cols.to_vec(),
7481 }
7482 ),
7483 };
7484 #root::sql::bulk_insert_pool(pool, &_query).await
7485 }
7486
7487 /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7488 /// skip rows that would violate a unique constraint. Issue
7489 /// #267 / T1.5. Same per-dialect dispatch as
7490 /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7491 /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7492 /// <pivot> = <pivot>` (MySQL no-op write).
7493 ///
7494 /// # Errors
7495 /// As [`Self::bulk_upsert_pool`].
7496 pub async fn bulk_insert_or_ignore_pool(
7497 rows: &[Self],
7498 pool: &#root::sql::Pool,
7499 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7500 if rows.is_empty() {
7501 return ::core::result::Result::Ok(());
7502 }
7503 let mut _all_rows: ::std::vec::Vec<
7504 ::std::vec::Vec<#root::core::SqlValue>,
7505 > = ::std::vec::Vec::with_capacity(rows.len());
7506 for _row in rows.iter() {
7507 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7508 ::std::vec::Vec::new();
7509 #( #upsert_pushes )*
7510 _all_rows.push(_row_vals);
7511 }
7512 let _query = #root::core::BulkInsertQuery {
7513 model: <Self as #root::core::Model>::SCHEMA,
7514 columns: ::std::vec![ #( #upsert_cols ),* ],
7515 rows: _all_rows,
7516 returning: ::std::vec::Vec::new(),
7517 on_conflict: ::core::option::Option::Some(
7518 #root::core::ConflictClause::DoNothing
7519 ),
7520 };
7521 #root::sql::bulk_insert_pool(pool, &_query).await
7522 }
7523 }
7524 };
7525
7526 // Ergonomic `Model::bulk_update(objs, fields)` — Django's
7527 // `QuerySet.bulk_update`. The SQL/IR/executor stack
7528 // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7529 // `write_bulk_update_*` writers) already existed; what was missing
7530 // was the per-model constructor that maps `&[Self]` + a runtime
7531 // column list into rows of `[pk, col_vals…]` so callers don't
7532 // hand-build the IR. Emitted only when the model has a primary key
7533 // (the PK is the join key and can't itself be updated).
7534 let bulk_update_method = match &fields.primary_key {
7535 None => quote! {},
7536 Some((pk_ident, pk_col)) => {
7537 // One pair of match arms per non-PK column: a validation arm
7538 // resolving the runtime name to its `&'static str` column,
7539 // and a value arm pushing that field off the row.
7540 let mut col_arms: Vec<TokenStream2> = Vec::new();
7541 let mut val_arms: Vec<TokenStream2> = Vec::new();
7542 for entry in &fields.column_entries {
7543 if &entry.column == pk_col {
7544 continue;
7545 }
7546 let col = &entry.column;
7547 let ident = &entry.ident;
7548 col_arms.push(quote! { #col => #col, });
7549 val_arms.push(quote! {
7550 #col => _row_vals.push(
7551 ::core::convert::Into::<#root::core::SqlValue>::into(
7552 ::core::clone::Clone::clone(&_o.#ident)
7553 )
7554 ),
7555 });
7556 }
7557 quote! {
7558 /// Django's `QuerySet.bulk_update(objs, fields)` — write
7559 /// per-row-different values for the named `fields` across
7560 /// every object in `objs` in a single statement, matched
7561 /// by primary key.
7562 ///
7563 /// `fields` names the **columns** to update. The primary
7564 /// key identifies each row and cannot itself be updated
7565 /// (pass it and you get
7566 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7567 /// Empty `objs` or `fields` is a no-op returning `0`.
7568 /// Objects whose PK matches no row are simply not updated.
7569 /// Returns the number of rows affected.
7570 ///
7571 /// Tri-dialect: lowers to one
7572 /// [`#root::core::BulkUpdateQuery`] and dispatches
7573 /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7574 /// a CTE + correlated subquery on SQLite, an inner
7575 /// `JOIN (VALUES …)` on MySQL.
7576 ///
7577 /// # Errors
7578 /// [`#root::core::QueryError::UnknownField`] for a name
7579 /// that isn't a column on this model,
7580 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7581 /// `fields` names the PK, or [`#root::sql::ExecError`] for
7582 /// SQL-writing / driver failures.
7583 pub async fn bulk_update(
7584 objs: &[Self],
7585 fields: &[&str],
7586 pool: &#root::sql::Pool,
7587 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7588 if objs.is_empty() || fields.is_empty() {
7589 return ::core::result::Result::Ok(0);
7590 }
7591 let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7592 let mut _update_columns: ::std::vec::Vec<&'static str> =
7593 ::std::vec::Vec::with_capacity(fields.len());
7594 for &_f in fields {
7595 let _col: &'static str = match _f {
7596 #pk_col => {
7597 return ::core::result::Result::Err(
7598 ::core::convert::Into::into(
7599 #root::core::QueryError::BulkUpdatePrimaryKey {
7600 model: _model_name,
7601 field: ::std::string::ToString::to_string(_f),
7602 }
7603 )
7604 );
7605 }
7606 #( #col_arms )*
7607 _ => {
7608 return ::core::result::Result::Err(
7609 ::core::convert::Into::into(
7610 #root::core::QueryError::UnknownField {
7611 model: _model_name,
7612 field: ::std::string::ToString::to_string(_f),
7613 }
7614 )
7615 );
7616 }
7617 };
7618 _update_columns.push(_col);
7619 }
7620 let mut _rows: ::std::vec::Vec<
7621 ::std::vec::Vec<#root::core::SqlValue>,
7622 > = ::std::vec::Vec::with_capacity(objs.len());
7623 for _o in objs.iter() {
7624 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7625 ::std::vec::Vec::with_capacity(fields.len() + 1);
7626 // PK first — the writers expect `[pk, …update cols]`.
7627 _row_vals.push(
7628 ::core::convert::Into::<#root::core::SqlValue>::into(
7629 ::core::clone::Clone::clone(&_o.#pk_ident)
7630 )
7631 );
7632 for &_f in fields {
7633 match _f {
7634 #( #val_arms )*
7635 // Unreachable: every name was validated
7636 // against the same arm set above.
7637 _ => {}
7638 }
7639 }
7640 _rows.push(_row_vals);
7641 }
7642 let _query = #root::core::BulkUpdateQuery {
7643 model: <Self as #root::core::Model>::SCHEMA,
7644 update_columns: _update_columns,
7645 rows: _rows,
7646 };
7647 #root::sql::bulk_update_pool(pool, &_query).await
7648 }
7649 }
7650 }
7651 };
7652
7653 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7654 quote! {
7655 /// Hidden runtime accessor for the primary-key value as a
7656 /// [`SqlValue`]. Used by reverse-relation helpers
7657 /// (`<parent>::<child>_set`) emitted from sibling models'
7658 /// FK fields. Not part of the public API.
7659 #[doc(hidden)]
7660 pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7661 ::core::convert::Into::<#root::core::SqlValue>::into(
7662 ::core::clone::Clone::clone(&self.#pk_ident)
7663 )
7664 }
7665 }
7666 });
7667
7668 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7669 quote! {
7670 impl #root::sql::HasPkValue for #struct_name {
7671 fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7672 ::core::convert::Into::<#root::core::SqlValue>::into(
7673 ::core::clone::Clone::clone(&self.#pk_ident)
7674 )
7675 }
7676 }
7677 }
7678 });
7679
7680 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7681
7682 // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7683 // dispatch to the right per-backend body without the macro emitting
7684 // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7685 // so audited models with non-Auto PKs (which still go through
7686 // `insert_one_with_audit` → `apply_auto_pk`) link.
7687 let assign_auto_pk_pool_impl = {
7688 let auto_assigns = &fields.auto_assigns;
7689 // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7690 // so the body is structurally identical to `auto_assigns` —
7691 // only the helper name swaps from `try_get_returning` to
7692 // `try_get_returning_sqlite` so the closure typechecks against
7693 // a `SqliteRow` instead of a `PgRow`.
7694 let auto_assigns_sqlite: Vec<TokenStream2> = fields
7695 .auto_field_idents
7696 .iter()
7697 .map(|(ident, column)| {
7698 quote! {
7699 self.#ident = #root::sql::try_get_returning_sqlite(
7700 _returning_row, #column
7701 )?;
7702 }
7703 })
7704 .collect();
7705 // #1028 — decode each `generated_as` column from the same
7706 // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7707 // own type drives the decode (no `Auto<T>` wrapper).
7708 let generated_assigns: Vec<TokenStream2> = fields
7709 .generated_field_idents
7710 .iter()
7711 .map(|(ident, column)| {
7712 quote! {
7713 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7714 }
7715 })
7716 .collect();
7717 let generated_assigns_sqlite: Vec<TokenStream2> = fields
7718 .generated_field_idents
7719 .iter()
7720 .map(|(ident, column)| {
7721 quote! {
7722 self.#ident = #root::sql::try_get_returning_sqlite(
7723 _returning_row, #column
7724 )?;
7725 }
7726 })
7727 .collect();
7728 let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
7729 // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7730 // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7731 // Auto<Uuid>/etc. fail to link against MySQL (intended —
7732 // those models can't use AUTO_INCREMENT). The trait is only
7733 // touched on the MySQL arm at runtime, so PG-only consumers
7734 // never see the bound failure.
7735 //
7736 // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7737 // Auto<i64> PK + auto_now_add timestamp) errored hard at
7738 // runtime with "multi-column RETURNING". MySQL has no
7739 // multi-column RETURNING semantic and a follow-up SELECT
7740 // would need cross-trait plumbing. Pragmatic shape: succeed
7741 // with the FIRST Auto field populated from LAST_INSERT_ID();
7742 // any other Auto fields stay `Auto::Unset`. Callers that
7743 // need the DB-defaulted timestamp / UUID can re-fetch the
7744 // row by PK after `save_pool`. Fixes the cookbook chapter
7745 // 12 dialect divergence.
7746 let value_ty = fields
7747 .first_auto_value_ty
7748 .as_ref()
7749 .expect("first_auto_value_ty set whenever first_auto_ident is");
7750 quote! {
7751 let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7752 ::rustango_from_mysql_auto_id(_id)?;
7753 self.#first = #root::sql::Auto::Set(_converted);
7754 ::core::result::Result::Ok(())
7755 }
7756 } else {
7757 quote! {
7758 let _ = _id;
7759 ::core::result::Result::Ok(())
7760 }
7761 };
7762 quote! {
7763 impl #root::sql::AssignAutoPkPool for #struct_name {
7764 fn __rustango_assign_from_pg_row(
7765 &mut self,
7766 _returning_row: &#root::sql::PgReturningRow,
7767 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7768 #( #auto_assigns )*
7769 #( #generated_assigns )*
7770 ::core::result::Result::Ok(())
7771 }
7772 fn __rustango_assign_from_mysql_id(
7773 &mut self,
7774 _id: i64,
7775 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7776 #mysql_body
7777 }
7778 fn __rustango_assign_from_sqlite_row(
7779 &mut self,
7780 _returning_row: &#root::sql::SqliteReturningRow,
7781 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7782 #( #auto_assigns_sqlite )*
7783 #( #generated_assigns_sqlite )*
7784 ::core::result::Result::Ok(())
7785 }
7786 }
7787 }
7788 };
7789
7790 let from_aliased_row_inits = &fields.from_aliased_row_inits;
7791 let aliased_row_helper = quote! {
7792 /// Decode a row's aliased target columns (produced by
7793 /// `select_related`'s LEFT JOIN) into a fresh instance of
7794 /// this model. Reads each column via
7795 /// `format!("{prefix}__{col}")`, matching the alias the
7796 /// SELECT writer emitted. Slice 9.0d.
7797 #[doc(hidden)]
7798 #[cfg(feature = "postgres")]
7799 pub fn __rustango_from_aliased_row(
7800 row: &#root::sql::sqlx::postgres::PgRow,
7801 prefix: &str,
7802 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7803 ::core::result::Result::Ok(Self {
7804 #( #from_aliased_row_inits ),*
7805 })
7806 }
7807 };
7808 // v0.23.0-batch8 — MySQL counterpart, gated through the
7809 // cfg-aware macro_rules so PG-only builds expand to nothing.
7810 let aliased_row_helper_my = quote! {
7811 #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7812 #( #from_aliased_row_inits ),*
7813 });
7814 };
7815
7816 // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7817 // pattern + cfg gate on the `sqlite` feature.
7818 let aliased_row_helper_sqlite = quote! {
7819 #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7820 #( #from_aliased_row_inits ),*
7821 });
7822 };
7823
7824 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7825 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7826 let load_related_impl_sqlite =
7827 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7828
7829 // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7830 // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7831 // default `Self::objects()`. Each accessor returns a fresh
7832 // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7833 // methods the user defined.
7834 let extra_manager_fns: Vec<TokenStream2> = manager_fns
7835 .iter()
7836 .map(|fn_ident| {
7837 let model_name_str = struct_name.to_string();
7838 let fn_name_str = fn_ident.to_string();
7839 let doc = format!(
7840 "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7841 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7842 equivalent to `Self::objects()`. Chains with any \
7843 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7844 extension methods."
7845 );
7846 quote! {
7847 #[doc = #doc]
7848 #[must_use]
7849 pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7850 #root::query::QuerySet::new()
7851 }
7852 }
7853 })
7854 .collect();
7855
7856 quote! {
7857 impl #struct_name {
7858 /// Start a new `QuerySet` over this model. Django shape.
7859 #[must_use]
7860 pub fn objects() -> #root::query::QuerySet<#struct_name> {
7861 #root::query::QuerySet::new()
7862 }
7863
7864 /// Eloquent-shape alias of [`Self::objects`]. Returns
7865 /// a fresh `QuerySet<Self>` ready for `.filter()` /
7866 /// `.where_()` / etc. Matches Laravel muscle-memory:
7867 ///
7868 /// ```ignore
7869 /// // Eloquent: Post::query()->where('published', true)
7870 /// // Django: Post.objects.filter(published=True)
7871 /// // rustango: Post::query().filter("published", true)
7872 /// // or: Post::objects().filter("published", true)
7873 /// ```
7874 ///
7875 /// Both names point at the same underlying constructor;
7876 /// neither is preferred.
7877 #[must_use]
7878 pub fn query() -> #root::query::QuerySet<#struct_name> {
7879 #root::query::QuerySet::new()
7880 }
7881
7882 #( #extra_manager_fns )*
7883
7884 #insert_method
7885
7886 #bulk_insert_method
7887
7888 #bulk_upsert_pool_method
7889
7890 #bulk_update_method
7891
7892 #save_method
7893
7894 #pk_methods
7895
7896 #pk_value_helper
7897
7898 #aliased_row_helper
7899
7900 #column_consts
7901 }
7902
7903 #aliased_row_helper_my
7904
7905 #aliased_row_helper_sqlite
7906
7907 #load_related_impl
7908
7909 #load_related_impl_my
7910
7911 #load_related_impl_sqlite
7912
7913 #has_pk_value_impl
7914
7915 #fk_pk_access_impl
7916
7917 #assign_auto_pk_pool_impl
7918 }
7919}
7920
7921/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
7922/// `auto_assigns` but reading from `_returning_row` and writing to
7923/// `_row_mut` instead of `self`.
7924fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
7925 let root = rustango_root();
7926 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
7927 let col_lit = column.as_str();
7928 quote! {
7929 _row_mut.#ident = #root::sql::sqlx::Row::try_get(
7930 _returning_row,
7931 #col_lit,
7932 )?;
7933 }
7934 });
7935 quote! { #( #lines )* }
7936}
7937
7938/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
7939fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
7940 let lines = entries.iter().map(|e| {
7941 let ident = &e.ident;
7942 let col_ty = column_type_ident(ident);
7943 quote! {
7944 #[allow(non_upper_case_globals)]
7945 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
7946 }
7947 });
7948 quote! { #(#lines)* }
7949}
7950
7951/// Emit a hidden per-model module carrying one zero-sized type per field,
7952/// each with a `Column` impl pointing back at the model.
7953fn column_module_tokens(
7954 module_ident: &syn::Ident,
7955 struct_name: &syn::Ident,
7956 entries: &[ColumnEntry],
7957) -> TokenStream2 {
7958 let root = rustango_root();
7959 let items = entries.iter().map(|e| {
7960 let col_ty = column_type_ident(&e.ident);
7961 let value_ty = &e.value_ty;
7962 let name = &e.name;
7963 let column = &e.column;
7964 let field_type_tokens = &e.field_type_tokens;
7965 quote! {
7966 #[derive(::core::clone::Clone, ::core::marker::Copy)]
7967 pub struct #col_ty;
7968
7969 impl #root::core::Column for #col_ty {
7970 type Model = super::#struct_name;
7971 type Value = #value_ty;
7972 const NAME: &'static str = #name;
7973 const COLUMN: &'static str = #column;
7974 const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
7975 }
7976 }
7977 });
7978 quote! {
7979 #[doc(hidden)]
7980 #[allow(non_camel_case_types, non_snake_case)]
7981 pub mod #module_ident {
7982 // Re-import the parent scope so field types referencing
7983 // sibling models (e.g. `ForeignKey<Author>`) resolve
7984 // inside this submodule. Without this we'd hit
7985 // `proc_macro_derive_resolution_fallback` warnings.
7986 #[allow(unused_imports)]
7987 use super::*;
7988 #(#items)*
7989 }
7990 }
7991}
7992
7993fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
7994 syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
7995}
7996
7997fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
7998 syn::Ident::new(
7999 &format!("__rustango_cols_{struct_name}"),
8000 struct_name.span(),
8001 )
8002}
8003
8004fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
8005 let root = rustango_root();
8006 // The Postgres impl is always emitted — every rustango build pulls in
8007 // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8008 // routed through `#root::__impl_my_from_row!`, a cfg-gated
8009 // macro_rules whose body collapses to nothing when rustango is built
8010 // without the `mysql` feature. No user-facing feature shim required.
8011 //
8012 // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8013 // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8014 // back into a comma-separated list inside square brackets. Since each
8015 // entry is already in `field: expr` shape, the existing tokens slot in.
8016 quote! {
8017 #[cfg(feature = "postgres")]
8018 impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8019 for #struct_name
8020 {
8021 fn from_row(
8022 row: &'r #root::sql::sqlx::postgres::PgRow,
8023 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8024 ::core::result::Result::Ok(Self {
8025 #( #from_row_inits ),*
8026 })
8027 }
8028 }
8029
8030 #root::__impl_my_from_row!(#struct_name, |row| {
8031 #( #from_row_inits ),*
8032 });
8033
8034 #root::__impl_sqlite_from_row!(#struct_name, |row| {
8035 #( #from_row_inits ),*
8036 });
8037 }
8038}
8039
8040struct ContainerAttrs {
8041 table: Option<String>,
8042 display: Option<(String, proc_macro2::Span)>,
8043 /// Explicit Django-style app label from `#[rustango(app = "blog")]`.
8044 /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8045 /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8046 /// at runtime — this attribute is the override for cases where
8047 /// the inference is wrong (e.g. a model that conceptually belongs
8048 /// to one app but is physically in another module).
8049 app: Option<String>,
8050 /// Django ModelAdmin-shape per-model knobs from
8051 /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8052 /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8053 /// admin code falls back to `AdminConfig::DEFAULT`.
8054 admin: Option<AdminAttrs>,
8055 /// Per-model audit configuration from `#[rustango(audit(...))]`.
8056 /// `None` when the model isn't audited — write paths emit no
8057 /// audit entries. When present, single-row writes capture
8058 /// before/after for the listed fields and bulk writes batch
8059 /// snapshots into one INSERT into `rustango_audit_log`.
8060 audit: Option<AuditAttrs>,
8061 /// `true` when `#[rustango(permissions)]` is present. Signals that
8062 /// `auto_create_permissions` should seed the four CRUD codenames for
8063 /// this model.
8064 permissions: bool,
8065 /// Many-to-many relations declared via
8066 /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8067 /// src = "post_id", dst = "tag_id"))]`.
8068 m2m: Vec<M2MAttr>,
8069 /// Polymorphic M2M relations declared via
8070 /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8071 /// pk_column = "taggable_id", ct_column = "taggable_type",
8072 /// related_column = "tag_id"))]` (issue #818).
8073 generic_m2m: Vec<GenericM2MAttr>,
8074 /// Composite indexes declared via
8075 /// `#[rustango(index("col1, col2"))]` or
8076 /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8077 /// Single-column indexes from `#[rustango(index)]` on fields are
8078 /// accumulated here during field collection.
8079 indexes: Vec<IndexAttr>,
8080 /// Table-level CHECK constraints declared via
8081 /// `#[rustango(check(name = "…", expr = "…"))]`.
8082 checks: Vec<CheckAttr>,
8083 /// Table-level PG `EXCLUDE` constraints declared via
8084 /// `#[rustango(exclude(name = "…", using = "gist", elements =
8085 /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8086 /// migration writer renders them on Postgres and skips with a
8087 /// warning on MySQL/SQLite. Issue #319.
8088 excludes: Vec<ExcludeAttr>,
8089 /// Composite (multi-column) FKs declared via
8090 /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8091 /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8092 composite_fks: Vec<CompositeFkAttr>,
8093 /// Generic ("any model") FKs declared via
8094 /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8095 /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8096 generic_fks: Vec<GenericFkAttr>,
8097 /// Where this model lives in a tenancy deployment, declared via
8098 /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8099 /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8100 /// to partition diff output between registry-scoped and
8101 /// tenant-scoped migration files.
8102 scope: Option<String>,
8103 /// Custom-Manager extension-trait name from
8104 /// `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8105 /// When set, the macro emits an empty `pub trait <name>: Sized {}`
8106 /// adjacent to the model so users can write
8107 /// `impl FooManagerExt for QuerySet<Foo> { fn published(self) -> Self ... }`
8108 /// and discover the convention from the model definition.
8109 manager_ext: Option<syn::Ident>,
8110 /// Extra QuerySet accessor names from
8111 /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8112 /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8113 /// the default `Self::objects()`. Multiple attributes allowed.
8114 manager_fns: Vec<syn::Ident>,
8115 /// Default ordering declared via `#[rustango(default_order =
8116 /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8117 /// `(column_name, desc_flag, span_for_error_reporting)` — the
8118 /// `-` prefix means descending; the `+` prefix or no prefix means
8119 /// ascending.
8120 default_order: Vec<(String, bool, proc_macro2::Span)>,
8121 /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8122 /// Routes the emitted schema's `is_view = true` so the migration
8123 /// snapshot skips this model (its underlying SQL view is operator-
8124 /// managed, not rustango-managed).
8125 is_view: bool,
8126 /// Django-shape `Meta.managed` from `#[rustango(managed = false)]`.
8127 /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8128 /// the migration snapshot skips this model so `makemigrations` /
8129 /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8130 /// TABLE` against it (operator-managed schema).
8131 managed: bool,
8132 /// Django-shape `Meta.base_manager_name` from
8133 /// `#[rustango(base_manager_name = "...")]`. Threaded into
8134 /// `ModelSchema::base_manager_name`. Declarative-only today.
8135 base_manager_name: Option<String>,
8136 /// Django-shape `Meta.order_with_respect_to = "parent_fk"` from
8137 /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8138 /// field this model's instances are ordered relative to.
8139 /// Declarative-only today; threaded onto
8140 /// `ModelSchema::order_with_respect_to`.
8141 order_with_respect_to: Option<String>,
8142 /// Django-shape `Meta.proxy = True` from `#[rustango(proxy)]` /
8143 /// `#[rustango(proxy = true)]`. Marks the model as a proxy that
8144 /// shares its DB table with another struct. Threaded into
8145 /// `ModelSchema::proxy` so future codegen can skip table-owning
8146 /// behavior for proxies.
8147 proxy: bool,
8148 /// Django-shape `Meta.required_db_features` from
8149 /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8150 /// Each comma-separated capability token surfaces on
8151 /// `ModelSchema::required_db_features` so `manage check --deploy`
8152 /// can warn when the active dialect lacks one.
8153 required_db_features: Vec<String>,
8154 /// Django-shape `Meta.required_db_vendor` from
8155 /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8156 /// Normalized to the dialect name `manage check --deploy`
8157 /// compares against `Settings.database.backend`. Aliases
8158 /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8159 /// stored under the canonical name.
8160 required_db_vendor: Option<String>,
8161 /// Django-shape `Meta.default_related_name` from
8162 /// `#[rustango(default_related_name = "...")]`. Threaded into
8163 /// `ModelSchema::default_related_name`. Reverse-relation accessor
8164 /// name to use when an FK / M2M field doesn't override it.
8165 /// Today rustango doesn't auto-emit reverse managers; the
8166 /// metadata is the foundation for that work.
8167 default_related_name: Option<String>,
8168 /// Django-shape `Meta.db_table_comment` (4.2+) from
8169 /// `#[rustango(db_table_comment = "...")]`. Threaded into
8170 /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8171 /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8172 /// inline `COMMENT='...'`, SQLite: no-op).
8173 db_table_comment: Option<String>,
8174 /// Django-shape `Meta.get_latest_by` from
8175 /// `#[rustango(get_latest_by = "created_at")]` /
8176 /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8177 /// `(column, descending)` where `descending = true` when the
8178 /// attribute value starts with `-`. Threaded into
8179 /// `ModelSchema::get_latest_by`.
8180 get_latest_by: Option<(String, bool)>,
8181 /// Django-shape `Meta.permissions = [(codename, name), ...]`
8182 /// from `#[rustango(extra_permissions = "approve:Can approve,
8183 /// archive:Can archive")]`. Comma-separated `codename:label`
8184 /// pairs. Threaded into `ModelSchema::extra_permissions`.
8185 extra_permissions: Vec<(String, String)>,
8186 /// Django-shape `Meta.default_permissions` — which CRUD codenames
8187 /// (`"add"` / `"change"` / `"delete"` / `"view"`) the framework
8188 /// auto-creates. Empty `Vec` (default) means **all four** — matches
8189 /// Django's behavior when the operator omits the option. Set via
8190 /// `#[rustango(default_permissions = "view,change")]` to opt out.
8191 /// Validated at parse time; unknown actions fail with a span-pointing
8192 /// error.
8193 default_permissions: Vec<String>,
8194 /// `#[rustango(verbose_name = "blog post")]` — Django-shape
8195 /// human-readable singular label for the model. Threaded into
8196 /// `ModelSchema::verbose_name` so admin section headers /
8197 /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8198 /// over the Rust struct identifier.
8199 verbose_name: Option<String>,
8200 /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8201 /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8202 /// When unset, `display_label_plural()` auto-suffixes `s`.
8203 verbose_name_plural: Option<String>,
8204 /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8205 /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8206 /// a name (used by `QuerySet::without_global_scope`) with a
8207 /// `fn() -> WhereExpr` path that the macro emits into
8208 /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8209 global_scopes: Vec<GlobalScopeAttr>,
8210 /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8211 /// `#[rustango(through(name, far, far_fk_column, intermediate,
8212 /// intermediate_fk_column, intermediate_pk_column))]` — issue
8213 /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8214 /// entry emits an inherent `<name>_through(&self)` accessor that
8215 /// returns a `QuerySet<Far>` filtered via a correlated subquery
8216 /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8217 /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8218 through_relations: Vec<ThroughAttr>,
8219 /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8220 /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8221 /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8222 /// Each entry emits two associated functions on the parent —
8223 /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8224 /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8225 /// over a correlated subquery against the child table. Users
8226 /// drop the result into `QuerySet::where_raw(...)`.
8227 reverse_has_relations: Vec<ReverseHasAttr>,
8228 /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8229 /// issue #830. Each emits a `Model::generic_reverse_relations()`
8230 /// entry so the relation-existence family resolves polymorphic
8231 /// children by name.
8232 generic_has_relations: Vec<GenericHasAttr>,
8233}
8234
8235/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8236/// declaration. Each entry becomes one `core::GlobalScope` in the
8237/// emitted schema literal; `apply` resolves at macro-expand time
8238/// against the consumer's scope so the function must be in scope at
8239/// the use site. Issue #820.
8240struct GlobalScopeAttr {
8241 name: String,
8242 apply: syn::Path,
8243}
8244
8245/// Parsed `#[rustango(through(...))]` declaration. Issue
8246/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8247/// `hasManyThrough` / `hasOneThrough` parity.
8248///
8249/// `Country hasManyThrough Post via User` declares as:
8250///
8251/// ```ignore
8252/// #[rustango(through(
8253/// name = "posts",
8254/// far = "Post",
8255/// far_fk_column = "author_id",
8256/// intermediate = "User",
8257/// intermediate_fk_column = "country_id",
8258/// ))]
8259/// struct Country { ... }
8260/// ```
8261///
8262/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8263/// which returns a queryset filtered via a correlated subquery —
8264/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8265/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8266/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8267/// because the subquery lives inside a `WhereExpr::InSubquery` node
8268/// and the outer queryset's pending list stays empty.
8269///
8270/// All four required arguments use **SQL column / table names**
8271/// (not Rust field names) to sidestep the multi-hop-filter substrate
8272/// gap. Once that substrate lands, a higher-level Rust-field-name
8273/// shorthand can be added without breaking this surface.
8274struct ThroughAttr {
8275 /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8276 name: String,
8277 /// Far model type identifier. `far = "Post"` → returns
8278 /// `QuerySet<Post>`. Resolved verbatim against the scope where
8279 /// the derive expands.
8280 far: syn::Ident,
8281 /// SQL column on the far model's table that references the
8282 /// intermediate model's primary key. For `Post`'s
8283 /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8284 /// default `<field>_id` convention) or whatever the user
8285 /// declared via `#[rustango(db_column = "...")]`.
8286 far_fk_column: String,
8287 /// Intermediate model type identifier. Needed to look up its
8288 /// `SCHEMA` so the subquery's `FROM` clause points at the
8289 /// intermediate table. `intermediate = "User"`.
8290 intermediate: syn::Ident,
8291 /// SQL column on the intermediate's table that references the
8292 /// source (this) model's primary key. For `User`'s
8293 /// `country: ForeignKey<Country>` the column is `"country_id"`.
8294 intermediate_fk_column: String,
8295 /// SQL primary-key column on the intermediate's table — the
8296 /// column the subquery projects. Optional; defaults to `"id"`
8297 /// (rustango's default PK column name). Override when the
8298 /// intermediate declares a custom PK column.
8299 intermediate_pk_column: String,
8300}
8301
8302/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8303/// child_fk_column = "..."))]` declaration. Issue
8304/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8305/// `whereHas` / `whereDoesntHave` parity.
8306///
8307/// `Post hasMany Comment` declares as:
8308///
8309/// ```ignore
8310/// #[rustango(reverse_has(
8311/// name = "comments",
8312/// child = "Comment",
8313/// child_fk_column = "post_id",
8314/// ))]
8315/// struct Post { ... }
8316/// ```
8317///
8318/// The macro emits two associated functions on `Post`:
8319///
8320/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8321/// `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8322/// <outer>.<self_pk_column>)`.
8323/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8324/// but `NOT EXISTS`, the `whereDoesntHave` analog.
8325///
8326/// User code:
8327///
8328/// ```ignore
8329/// // Posts with at least one comment:
8330/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8331/// // Posts with no comments:
8332/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8333/// ```
8334///
8335/// As with #817, all column / table identifiers are **SQL names**
8336/// (not Rust field names) so the substrate is independent of the
8337/// outstanding multi-hop filter resolver gap. The emitted
8338/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8339/// SQL-emit time via the writer's scope stack.
8340struct ReverseHasAttr {
8341 /// Accessor name. `name = "comments"` → emits
8342 /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8343 name: String,
8344 /// Child model type identifier. `child = "Comment"` — needed to
8345 /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8346 /// points at the child table.
8347 child: syn::Ident,
8348 /// SQL column on the child's table that references this model's
8349 /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8350 /// column is `"post_id"`.
8351 child_fk_column: String,
8352 /// SQL primary-key column on **this** model's table — the column
8353 /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8354 /// (rustango's default PK column name).
8355 self_pk_column: String,
8356}
8357
8358/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8359/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8360/// relation-existence family (issue #830). The child is a polymorphic,
8361/// content-type-discriminated model; emits a `Model::
8362/// generic_reverse_relations()` entry the queryset resolves by name.
8363struct GenericHasAttr {
8364 /// Accessor name, e.g. `name = "tags"`.
8365 name: String,
8366 /// Child model type identifier (`child = "Tag"`) — looked up for its
8367 /// `SCHEMA` so the subquery's `FROM` points at the child table.
8368 child: syn::Ident,
8369 /// Column on the child table holding the parent's content-type id.
8370 /// Optional; defaults to `"content_type_id"`.
8371 ct_column: String,
8372 /// Column on the child table holding the parent's PK value.
8373 /// Optional; defaults to `"object_pk"`.
8374 pk_column: String,
8375 /// SQL primary-key column on **this** (parent) model's table.
8376 /// Optional; defaults to `"id"`.
8377 self_pk_column: String,
8378}
8379
8380/// Parsed form of one index declaration (field-level or container-level).
8381struct IndexAttr {
8382 /// Index name; auto-derived when `None` at parse time.
8383 name: Option<String>,
8384 /// Column names in the index.
8385 columns: Vec<String>,
8386 /// `true` for `CREATE UNIQUE INDEX`.
8387 unique: bool,
8388 /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8389 /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8390 /// `"btree"` when the attribute is absent — the DDL writer omits
8391 /// the `USING` clause and the backend uses its own default
8392 /// (btree on every supported dialect).
8393 method: String,
8394 /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8395 /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8396 /// condition = "...", name = "..."))]`. `None` for plain indexes.
8397 where_clause: Option<String>,
8398 /// Django `Index(fields=..., include=[...])` covering-index
8399 /// columns (PG 11+ `INCLUDE (...)` clause). Empty `Vec` (the
8400 /// default) means "no covering columns".
8401 include: Vec<String>,
8402}
8403
8404/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8405struct CheckAttr {
8406 name: String,
8407 expr: String,
8408}
8409
8410/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8411/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8412/// PG-only — surfaced on every backend in the macro emit; the migration
8413/// writer skips the op on MySQL/SQLite. Issue #319.
8414struct ExcludeAttr {
8415 /// Constraint name (free-form Rust identifier).
8416 name: String,
8417 /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8418 using: String,
8419 /// Comma-separated `(column, operator)` pairs, in declaration
8420 /// order. Parsed from `"col WITH op, col WITH op"`.
8421 elements: Vec<(String, String)>,
8422 /// Optional `WHERE` predicate (raw SQL).
8423 where_clause: Option<String>,
8424}
8425
8426/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8427/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8428/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8429/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8430/// the model, not the field.
8431struct CompositeFkAttr {
8432 /// Logical relation name (free-form Rust identifier).
8433 name: String,
8434 /// SQL table name of the target.
8435 to: String,
8436 /// Source-side column names, in declaration order.
8437 from: Vec<String>,
8438 /// Target-side column names, same length / order as `from`.
8439 on: Vec<String>,
8440}
8441
8442/// Parsed form of one `#[rustango(generic_fk(name = "target",
8443/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8444/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8445/// generic ("any model") FKs live on the model, not the field.
8446struct GenericFkAttr {
8447 /// Logical relation name (free-form Rust identifier).
8448 name: String,
8449 /// Source-side column carrying the `content_type_id` value.
8450 ct_column: String,
8451 /// Source-side column carrying the target row's primary key.
8452 pk_column: String,
8453}
8454
8455/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8456struct M2MAttr {
8457 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8458 name: String,
8459 /// Target table (e.g. `"app_tags"`).
8460 to: String,
8461 /// Junction table (e.g. `"post_tags"`).
8462 through: String,
8463 /// Source FK column in the junction table (e.g. `"post_id"`).
8464 src: String,
8465 /// Destination FK column in the junction table (e.g. `"tag_id"`).
8466 dst: String,
8467 /// Whether the migration writer should auto-create the junction
8468 /// table. Default `true`. Set `auto_create = false` (#324) when
8469 /// the operator declares the through table as its own
8470 /// `#[derive(Model)]` struct with extra columns.
8471 auto_create: bool,
8472}
8473
8474/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8475/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8476/// junction carries a ContentType discriminator so unrelated models
8477/// share one pivot + related set.
8478struct GenericM2MAttr {
8479 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8480 name: String,
8481 /// Polymorphic junction table (e.g. `"taggables"`).
8482 through: String,
8483 /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8484 pk_column: String,
8485 /// Junction column holding the owning model's `content_type_id`
8486 /// discriminator (e.g. `"taggable_type"`).
8487 ct_column: String,
8488 /// Junction column holding the related model PK (e.g. `"tag_id"`).
8489 related_column: String,
8490}
8491
8492/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8493/// "user"))]`. `track` is a comma-separated list of field names whose
8494/// before/after values land in the JSONB `changes` column. `source`
8495/// is informational only — it pins a default source when the model
8496/// is written outside any `audit::with_source(...)` scope (rare).
8497#[derive(Default)]
8498struct AuditAttrs {
8499 /// Field names to capture in the `changes` JSONB. Validated
8500 /// against declared scalar fields at compile time. Empty means
8501 /// "track every scalar field" — Django's audit-everything default.
8502 track: Option<(Vec<String>, proc_macro2::Span)>,
8503}
8504
8505/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8506/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8507/// comma-separated strings; we validate each ident against the model's
8508/// declared fields at compile time.
8509#[derive(Default)]
8510struct AdminAttrs {
8511 list_display: Option<(Vec<String>, proc_macro2::Span)>,
8512 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8513 list_per_page: Option<usize>,
8514 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8515 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8516 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8517 /// Bulk action names. No field-validation against model fields —
8518 /// these are action handlers, not column references.
8519 actions: Option<(Vec<String>, proc_macro2::Span)>,
8520 /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8521 /// sections, comma-separated fields per section, optional
8522 /// `Title:` prefix. Empty title omits the `<legend>`.
8523 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8524 /// `admin(list_display_links = "title")` — Django-shape. Names
8525 /// from `list_display` whose cells should link to detail/edit.
8526 /// Issue #350.
8527 list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8528 /// `admin(search_help_text = "...")` — Django-shape. Short
8529 /// caption rendered beside the admin list view's search box.
8530 /// Issue #353.
8531 search_help_text: Option<String>,
8532 /// `admin(actions_on_top = false)` — Django-shape. Hides the
8533 /// action-bar above the table. Default `true`. Issue #354.
8534 actions_on_top: Option<bool>,
8535 /// `admin(actions_on_bottom = true)` — Django-shape. Renders an
8536 /// additional action-bar below the table. Default `false`.
8537 /// Issue #354.
8538 actions_on_bottom: Option<bool>,
8539 /// `admin(date_hierarchy = "created_at")` — Django-shape. Name of
8540 /// a date / datetime field whose values render as a clickable
8541 /// year / month / day drill-down strip above the list table.
8542 /// Empty / unset disables the strip. Issue #355.
8543 date_hierarchy: Option<String>,
8544 /// `admin(prepopulated_fields = "slug:title")` — Django-shape.
8545 /// Each entry is `target:source[+source2]`; multiple entries are
8546 /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8547 /// The admin change-form emits JS that slugifies the source values
8548 /// into the target field on every keystroke. Issue #356.
8549 prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8550 /// `admin(raw_id_fields = "parent, owner")` — Django-shape. Names
8551 /// of FK fields whose change-form widget renders a lookup link
8552 /// next to the input. Issue #357.
8553 raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8554 /// `admin(autocomplete_fields = "author_id")` — Django-shape.
8555 /// Names of FK fields whose change-form widget renders an
8556 /// Ajax-driven typeahead populated from a `__autocomplete`
8557 /// endpoint on the target model. Issue #358.
8558 autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8559 /// `admin(list_select_related = "all" | "none" | "author, …")`
8560 /// — Django-shape. Tunes the admin list view's FK auto-JOIN
8561 /// policy. Default `"all"` matches rustango's join-everything
8562 /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8563 list_select_related: Option<String>,
8564 /// `admin(formfield_overrides = "field:widget, field2:widget2")` —
8565 /// Django-shape. Each entry is `field_name:widget_name`; multiple
8566 /// entries comma-separated. Empty / unset → no overrides. The
8567 /// list of widget names supported is documented on
8568 /// `AdminConfig::formfield_overrides`. Issue #359.
8569 formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8570}
8571
8572fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8573 let mut out = ContainerAttrs {
8574 table: None,
8575 display: None,
8576 app: None,
8577 admin: None,
8578 audit: None,
8579 // Default `permissions = true` so every `#[derive(Model)]`
8580 // gets the four CRUD codenames seeded by `auto_create_permissions`
8581 // and is visible to non-superusers in the tenant admin without
8582 // manual per-model annotation. Models that intentionally don't
8583 // want permission rows (registry-internal types, framework
8584 // tables operators shouldn't manage directly) opt out via
8585 // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8586 // out-of-the-box admin invisibility regression (#62).
8587 permissions: true,
8588 m2m: Vec::new(),
8589 generic_m2m: Vec::new(),
8590 indexes: Vec::new(),
8591 checks: Vec::new(),
8592 excludes: Vec::new(),
8593 composite_fks: Vec::new(),
8594 generic_fks: Vec::new(),
8595 scope: None,
8596 manager_ext: None,
8597 manager_fns: Vec::new(),
8598 default_order: Vec::new(),
8599 is_view: false,
8600 managed: true,
8601 verbose_name: None,
8602 verbose_name_plural: None,
8603 base_manager_name: None,
8604 order_with_respect_to: None,
8605 proxy: false,
8606 required_db_features: Vec::new(),
8607 required_db_vendor: None,
8608 default_related_name: None,
8609 db_table_comment: None,
8610 get_latest_by: None,
8611 extra_permissions: Vec::new(),
8612 default_permissions: Vec::new(),
8613 global_scopes: Vec::new(),
8614 through_relations: Vec::new(),
8615 reverse_has_relations: Vec::new(),
8616 generic_has_relations: Vec::new(),
8617 };
8618 for attr in &input.attrs {
8619 if !attr.path().is_ident("rustango") {
8620 continue;
8621 }
8622 attr.parse_nested_meta(|meta| {
8623 if meta.path.is_ident("table") {
8624 let s: LitStr = meta.value()?.parse()?;
8625 let name = s.value();
8626 // v0.27.3 (#65) — macro-time guard against table names
8627 // that compile but break SQL downstream. Hyphens are
8628 // the common footgun: PostgreSQL accepts a quoted
8629 // `"intermediate-region"` in CREATE TABLE, but the
8630 // FK / index name derivation in `migrate::ddl`
8631 // emits `intermediate-region_field_fkey` unquoted,
8632 // which then fails the SQL parser. Same shape rule
8633 // as Postgres regular identifiers so the safe path
8634 // is the only path.
8635 validate_table_name(&name, s.span())?;
8636 out.table = Some(name);
8637 return Ok(());
8638 }
8639 if meta.path.is_ident("display") {
8640 let s: LitStr = meta.value()?.parse()?;
8641 out.display = Some((s.value(), s.span()));
8642 return Ok(());
8643 }
8644 if meta.path.is_ident("app") {
8645 let s: LitStr = meta.value()?.parse()?;
8646 out.app = Some(s.value());
8647 return Ok(());
8648 }
8649 if meta.path.is_ident("scope") {
8650 let s: LitStr = meta.value()?.parse()?;
8651 let val = s.value();
8652 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8653 return Err(meta.error(format!(
8654 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8655 )));
8656 }
8657 out.scope = Some(val);
8658 return Ok(());
8659 }
8660 if meta.path.is_ident("admin") {
8661 let mut admin = AdminAttrs::default();
8662 meta.parse_nested_meta(|inner| {
8663 if inner.path.is_ident("list_display") {
8664 let s: LitStr = inner.value()?.parse()?;
8665 admin.list_display =
8666 Some((split_field_list(&s.value()), s.span()));
8667 return Ok(());
8668 }
8669 if inner.path.is_ident("search_fields") {
8670 let s: LitStr = inner.value()?.parse()?;
8671 admin.search_fields =
8672 Some((split_field_list(&s.value()), s.span()));
8673 return Ok(());
8674 }
8675 if inner.path.is_ident("readonly_fields") {
8676 let s: LitStr = inner.value()?.parse()?;
8677 admin.readonly_fields =
8678 Some((split_field_list(&s.value()), s.span()));
8679 return Ok(());
8680 }
8681 if inner.path.is_ident("list_per_page") {
8682 let lit: syn::LitInt = inner.value()?.parse()?;
8683 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8684 return Ok(());
8685 }
8686 if inner.path.is_ident("ordering") {
8687 let s: LitStr = inner.value()?.parse()?;
8688 admin.ordering = Some((
8689 parse_ordering_list(&s.value()),
8690 s.span(),
8691 ));
8692 return Ok(());
8693 }
8694 if inner.path.is_ident("list_filter") {
8695 let s: LitStr = inner.value()?.parse()?;
8696 admin.list_filter =
8697 Some((split_field_list(&s.value()), s.span()));
8698 return Ok(());
8699 }
8700 if inner.path.is_ident("actions") {
8701 let s: LitStr = inner.value()?.parse()?;
8702 admin.actions =
8703 Some((split_field_list(&s.value()), s.span()));
8704 return Ok(());
8705 }
8706 if inner.path.is_ident("fieldsets") {
8707 let s: LitStr = inner.value()?.parse()?;
8708 admin.fieldsets =
8709 Some((parse_fieldset_list(&s.value()), s.span()));
8710 return Ok(());
8711 }
8712 if inner.path.is_ident("list_display_links") {
8713 let s: LitStr = inner.value()?.parse()?;
8714 admin.list_display_links =
8715 Some((split_field_list(&s.value()), s.span()));
8716 return Ok(());
8717 }
8718 if inner.path.is_ident("search_help_text") {
8719 let s: LitStr = inner.value()?.parse()?;
8720 admin.search_help_text = Some(s.value());
8721 return Ok(());
8722 }
8723 if inner.path.is_ident("actions_on_top") {
8724 let lit: syn::LitBool = inner.value()?.parse()?;
8725 admin.actions_on_top = Some(lit.value);
8726 return Ok(());
8727 }
8728 if inner.path.is_ident("actions_on_bottom") {
8729 let lit: syn::LitBool = inner.value()?.parse()?;
8730 admin.actions_on_bottom = Some(lit.value);
8731 return Ok(());
8732 }
8733 if inner.path.is_ident("date_hierarchy") {
8734 let s: LitStr = inner.value()?.parse()?;
8735 admin.date_hierarchy = Some(s.value());
8736 return Ok(());
8737 }
8738 if inner.path.is_ident("prepopulated_fields") {
8739 let s: LitStr = inner.value()?.parse()?;
8740 admin.prepopulated_fields =
8741 Some((parse_prepopulated_list(&s.value()), s.span()));
8742 return Ok(());
8743 }
8744 if inner.path.is_ident("raw_id_fields") {
8745 let s: LitStr = inner.value()?.parse()?;
8746 admin.raw_id_fields =
8747 Some((split_field_list(&s.value()), s.span()));
8748 return Ok(());
8749 }
8750 if inner.path.is_ident("autocomplete_fields") {
8751 let s: LitStr = inner.value()?.parse()?;
8752 admin.autocomplete_fields =
8753 Some((split_field_list(&s.value()), s.span()));
8754 return Ok(());
8755 }
8756 if inner.path.is_ident("list_select_related") {
8757 let s: LitStr = inner.value()?.parse()?;
8758 admin.list_select_related = Some(s.value());
8759 return Ok(());
8760 }
8761 if inner.path.is_ident("formfield_overrides") {
8762 let s: LitStr = inner.value()?.parse()?;
8763 admin.formfield_overrides =
8764 Some((parse_formfield_overrides(&s.value()), s.span()));
8765 return Ok(());
8766 }
8767 Err(inner.error(
8768 "unknown admin attribute (supported: \
8769 `list_display`, `list_display_links`, \
8770 `search_fields`, `search_help_text`, \
8771 `readonly_fields`, \
8772 `list_filter`, `list_per_page`, `ordering`, `actions`, \
8773 `actions_on_top`, `actions_on_bottom`, \
8774 `date_hierarchy`, \
8775 `prepopulated_fields`, \
8776 `raw_id_fields`, \
8777 `autocomplete_fields`, \
8778 `list_select_related`, \
8779 `formfield_overrides`, \
8780 `fieldsets`)",
8781 ))
8782 })?;
8783 out.admin = Some(admin);
8784 return Ok(());
8785 }
8786 if meta.path.is_ident("manager") {
8787 // `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8788 // Stretch `from_queryset = "..."` (Django Manager.from_queryset
8789 // shape) is left as a follow-up — the issue's primary
8790 // acceptance is the `ext = ...` trait emission.
8791 meta.parse_nested_meta(|inner| {
8792 if inner.path.is_ident("ext") {
8793 let s: LitStr = inner.value()?.parse()?;
8794 let name = s.value();
8795 if name.is_empty() {
8796 return Err(inner.error("manager(ext = \"...\") cannot be empty"));
8797 }
8798 out.manager_ext =
8799 Some(syn::Ident::new(&name, s.span()));
8800 return Ok(());
8801 }
8802 Err(inner.error(
8803 "unknown manager attribute (supported: `ext = \"TraitName\"`)",
8804 ))
8805 })?;
8806 return Ok(());
8807 }
8808 if meta.path.is_ident("manager_fn") {
8809 // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8810 // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8811 // next to the default `Self::objects()`. Multiple
8812 // attributes accumulate.
8813 let s: LitStr = meta.value()?.parse()?;
8814 let name = s.value();
8815 if name.is_empty() {
8816 return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8817 }
8818 if name == "objects" {
8819 return Err(meta.error(
8820 "`manager_fn = \"objects\"` collides with the default \
8821 accessor — pick a different name",
8822 ));
8823 }
8824 let ident = syn::Ident::new(&name, s.span());
8825 if out.manager_fns.iter().any(|prev| *prev == ident) {
8826 return Err(meta.error(format!(
8827 "duplicate `manager_fn = \"{name}\"`"
8828 )));
8829 }
8830 out.manager_fns.push(ident);
8831 return Ok(());
8832 }
8833 if meta.path.is_ident("default_order") {
8834 // `#[rustango(default_order = "-created_at, status")]`
8835 // — issue #291 / T2.5. Comma-separated list; `-prefix`
8836 // means descending, `+prefix` or bare name means ascending.
8837 // Per-query opt-in via `QuerySet::with_default_order()`.
8838 let s: LitStr = meta.value()?.parse()?;
8839 let raw = s.value();
8840 let span = s.span();
8841 let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
8842 Vec::new();
8843 for entry in raw.split(',') {
8844 let trimmed = entry.trim();
8845 if trimmed.is_empty() {
8846 return Err(syn::Error::new(
8847 span,
8848 "`default_order = \"...\"` has an empty entry — \
8849 check for a stray comma",
8850 ));
8851 }
8852 let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
8853 (true, rest.trim().to_owned())
8854 } else if let Some(rest) = trimmed.strip_prefix('+') {
8855 (false, rest.trim().to_owned())
8856 } else {
8857 (false, trimmed.to_owned())
8858 };
8859 if name.is_empty() {
8860 return Err(syn::Error::new(
8861 span,
8862 "`default_order` entry has no column name after the prefix",
8863 ));
8864 }
8865 if parsed.iter().any(|(n, _, _)| *n == name) {
8866 return Err(syn::Error::new(
8867 span,
8868 format!("duplicate column `{name}` in `default_order`"),
8869 ));
8870 }
8871 parsed.push((name, desc, span));
8872 }
8873 if parsed.is_empty() {
8874 return Err(syn::Error::new(
8875 span,
8876 "`default_order = \"...\"` cannot be empty",
8877 ));
8878 }
8879 out.default_order = parsed;
8880 return Ok(());
8881 }
8882 if meta.path.is_ident("global_scope") {
8883 // `#[rustango(global_scope(name = "active", apply =
8884 // path::to::fn))]` — issue #820. The apply function
8885 // path resolves at macro-expand time in the consumer's
8886 // scope; the macro re-emits it verbatim into the
8887 // `ModelSchema::global_scopes` slice literal.
8888 let span = meta.path.span();
8889 let mut scope_name: Option<String> = None;
8890 let mut apply_path: Option<syn::Path> = None;
8891 meta.parse_nested_meta(|inner| {
8892 if inner.path.is_ident("name") {
8893 let s: LitStr = inner.value()?.parse()?;
8894 let raw = s.value();
8895 if raw.trim().is_empty() {
8896 return Err(syn::Error::new(
8897 s.span(),
8898 "`global_scope(name = \"...\")` must not be empty",
8899 ));
8900 }
8901 scope_name = Some(raw);
8902 return Ok(());
8903 }
8904 if inner.path.is_ident("apply") {
8905 let p: syn::Path = inner.value()?.parse()?;
8906 apply_path = Some(p);
8907 return Ok(());
8908 }
8909 Err(inner.error(
8910 "unknown `global_scope` attribute (supported: \
8911 `name`, `apply`)",
8912 ))
8913 })?;
8914 let Some(name) = scope_name else {
8915 return Err(syn::Error::new(
8916 span,
8917 "`global_scope` requires `name = \"...\"`",
8918 ));
8919 };
8920 let Some(apply) = apply_path else {
8921 return Err(syn::Error::new(
8922 span,
8923 "`global_scope` requires `apply = fn_path`",
8924 ));
8925 };
8926 if out.global_scopes.iter().any(|s| s.name == name) {
8927 return Err(syn::Error::new(
8928 span,
8929 format!(
8930 "duplicate global scope name `{name}` — \
8931 pick a unique identifier so \
8932 `QuerySet::without_global_scope(\"{name}\")` \
8933 is unambiguous"
8934 ),
8935 ));
8936 }
8937 out.global_scopes.push(GlobalScopeAttr { name, apply });
8938 return Ok(());
8939 }
8940 if meta.path.is_ident("through") {
8941 // `#[rustango(through(name = "posts", far = "Post",
8942 // far_fk_column = "author_id", intermediate = "User",
8943 // intermediate_fk_column = "country_id"
8944 // [, intermediate_pk_column = "id"]))]` — issue #817.
8945 let span = meta.path.span();
8946 let mut accessor_name: Option<String> = None;
8947 let mut far_ident: Option<syn::Ident> = None;
8948 let mut far_fk_column: Option<String> = None;
8949 let mut intermediate_ident: Option<syn::Ident> = None;
8950 let mut intermediate_fk_column: Option<String> = None;
8951 let mut intermediate_pk_column: Option<String> = None;
8952 fn parse_nonempty_string(
8953 inner: &syn::meta::ParseNestedMeta<'_>,
8954 field: &str,
8955 ) -> syn::Result<String> {
8956 let s: LitStr = inner.value()?.parse()?;
8957 let raw = s.value();
8958 let trimmed = raw.trim();
8959 if trimmed.is_empty() {
8960 return Err(syn::Error::new(
8961 s.span(),
8962 format!("`through({field} = \"...\")` must not be empty"),
8963 ));
8964 }
8965 Ok(trimmed.to_owned())
8966 }
8967 meta.parse_nested_meta(|inner| {
8968 if inner.path.is_ident("name") {
8969 accessor_name = Some(parse_nonempty_string(&inner, "name")?);
8970 return Ok(());
8971 }
8972 if inner.path.is_ident("far") {
8973 // Far model name accepted as a string literal
8974 // so the attribute fits inside the existing
8975 // `parse_nested_meta` shape; parsed back into a
8976 // `syn::Ident` so the emitted accessor can
8977 // reference the type directly.
8978 let s: LitStr = inner.value()?.parse()?;
8979 let raw = s.value();
8980 let trimmed = raw.trim();
8981 if trimmed.is_empty() {
8982 return Err(syn::Error::new(
8983 s.span(),
8984 "`through(far = \"...\")` must not be empty",
8985 ));
8986 }
8987 far_ident = Some(syn::Ident::new(trimmed, s.span()));
8988 return Ok(());
8989 }
8990 if inner.path.is_ident("far_fk_column") {
8991 far_fk_column =
8992 Some(parse_nonempty_string(&inner, "far_fk_column")?);
8993 return Ok(());
8994 }
8995 if inner.path.is_ident("intermediate") {
8996 let s: LitStr = inner.value()?.parse()?;
8997 let raw = s.value();
8998 let trimmed = raw.trim();
8999 if trimmed.is_empty() {
9000 return Err(syn::Error::new(
9001 s.span(),
9002 "`through(intermediate = \"...\")` must not be empty",
9003 ));
9004 }
9005 intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9006 return Ok(());
9007 }
9008 if inner.path.is_ident("intermediate_fk_column") {
9009 intermediate_fk_column =
9010 Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9011 return Ok(());
9012 }
9013 if inner.path.is_ident("intermediate_pk_column") {
9014 intermediate_pk_column =
9015 Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9016 return Ok(());
9017 }
9018 Err(inner.error(
9019 "unknown `through` attribute (supported: \
9020 `name`, `far`, `far_fk_column`, \
9021 `intermediate`, `intermediate_fk_column`, \
9022 `intermediate_pk_column`)",
9023 ))
9024 })?;
9025 let Some(name) = accessor_name else {
9026 return Err(syn::Error::new(
9027 span,
9028 "`through` requires `name = \"...\"`",
9029 ));
9030 };
9031 let Some(far) = far_ident else {
9032 return Err(syn::Error::new(
9033 span,
9034 "`through` requires `far = \"FarModelType\"`",
9035 ));
9036 };
9037 let Some(far_fk_column) = far_fk_column else {
9038 return Err(syn::Error::new(
9039 span,
9040 "`through` requires `far_fk_column = \"<column>\"`",
9041 ));
9042 };
9043 let Some(intermediate) = intermediate_ident else {
9044 return Err(syn::Error::new(
9045 span,
9046 "`through` requires `intermediate = \"IntermediateModelType\"`",
9047 ));
9048 };
9049 let Some(intermediate_fk_column) = intermediate_fk_column else {
9050 return Err(syn::Error::new(
9051 span,
9052 "`through` requires `intermediate_fk_column = \"<column>\"`",
9053 ));
9054 };
9055 let intermediate_pk_column =
9056 intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9057 if out.through_relations.iter().any(|t| t.name == name) {
9058 return Err(syn::Error::new(
9059 span,
9060 format!(
9061 "duplicate `through(name = \"{name}\")` — \
9062 pick a unique accessor name"
9063 ),
9064 ));
9065 }
9066 out.through_relations.push(ThroughAttr {
9067 name,
9068 far,
9069 far_fk_column,
9070 intermediate,
9071 intermediate_fk_column,
9072 intermediate_pk_column,
9073 });
9074 return Ok(());
9075 }
9076 if meta.path.is_ident("reverse_has") {
9077 // `#[rustango(reverse_has(name = "comments",
9078 // child = "Comment", child_fk_column = "post_id"
9079 // [, self_pk_column = "id"]))]` — issue #830.
9080 let span = meta.path.span();
9081 let mut accessor_name: Option<String> = None;
9082 let mut child_ident: Option<syn::Ident> = None;
9083 let mut child_fk_column: Option<String> = None;
9084 let mut self_pk_column: Option<String> = None;
9085 meta.parse_nested_meta(|inner| {
9086 if inner.path.is_ident("name") {
9087 let s: LitStr = inner.value()?.parse()?;
9088 let raw = s.value();
9089 if raw.trim().is_empty() {
9090 return Err(syn::Error::new(
9091 s.span(),
9092 "`reverse_has(name = \"...\")` must not be empty",
9093 ));
9094 }
9095 accessor_name = Some(raw);
9096 return Ok(());
9097 }
9098 if inner.path.is_ident("child") {
9099 let s: LitStr = inner.value()?.parse()?;
9100 let raw = s.value();
9101 let trimmed = raw.trim();
9102 if trimmed.is_empty() {
9103 return Err(syn::Error::new(
9104 s.span(),
9105 "`reverse_has(child = \"...\")` must not be empty",
9106 ));
9107 }
9108 child_ident = Some(syn::Ident::new(trimmed, s.span()));
9109 return Ok(());
9110 }
9111 if inner.path.is_ident("child_fk_column") {
9112 let s: LitStr = inner.value()?.parse()?;
9113 let raw = s.value();
9114 let trimmed = raw.trim();
9115 if trimmed.is_empty() {
9116 return Err(syn::Error::new(
9117 s.span(),
9118 "`reverse_has(child_fk_column = \"...\")` must not be empty",
9119 ));
9120 }
9121 child_fk_column = Some(trimmed.to_owned());
9122 return Ok(());
9123 }
9124 if inner.path.is_ident("self_pk_column") {
9125 let s: LitStr = inner.value()?.parse()?;
9126 let raw = s.value();
9127 let trimmed = raw.trim();
9128 if trimmed.is_empty() {
9129 return Err(syn::Error::new(
9130 s.span(),
9131 "`reverse_has(self_pk_column = \"...\")` must not be empty",
9132 ));
9133 }
9134 self_pk_column = Some(trimmed.to_owned());
9135 return Ok(());
9136 }
9137 Err(inner.error(
9138 "unknown `reverse_has` attribute (supported: \
9139 `name`, `child`, `child_fk_column`, \
9140 `self_pk_column`)",
9141 ))
9142 })?;
9143 let Some(name) = accessor_name else {
9144 return Err(syn::Error::new(
9145 span,
9146 "`reverse_has` requires `name = \"...\"`",
9147 ));
9148 };
9149 let Some(child) = child_ident else {
9150 return Err(syn::Error::new(
9151 span,
9152 "`reverse_has` requires `child = \"ChildModelType\"`",
9153 ));
9154 };
9155 let Some(child_fk_column) = child_fk_column else {
9156 return Err(syn::Error::new(
9157 span,
9158 "`reverse_has` requires `child_fk_column = \"<column>\"`",
9159 ));
9160 };
9161 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9162 if out.reverse_has_relations.iter().any(|r| r.name == name) {
9163 return Err(syn::Error::new(
9164 span,
9165 format!(
9166 "duplicate `reverse_has(name = \"{name}\")` — \
9167 pick a unique accessor name"
9168 ),
9169 ));
9170 }
9171 out.reverse_has_relations.push(ReverseHasAttr {
9172 name,
9173 child,
9174 child_fk_column,
9175 self_pk_column,
9176 });
9177 return Ok(());
9178 }
9179 if meta.path.is_ident("generic_has") {
9180 // `#[rustango(generic_has(name = "tags",
9181 // child = "Tag", ct_column = "content_type_id",
9182 // pk_column = "object_pk" [, self_pk_column = "id"]))]`
9183 // — issue #830, the reverse generic-FK (GFK) arm of the
9184 // relation-existence family.
9185 let span = meta.path.span();
9186 let mut accessor_name: Option<String> = None;
9187 let mut child_ident: Option<syn::Ident> = None;
9188 let mut ct_column: Option<String> = None;
9189 let mut pk_column: Option<String> = None;
9190 let mut self_pk_column: Option<String> = None;
9191 meta.parse_nested_meta(|inner| {
9192 if inner.path.is_ident("name") {
9193 let s: LitStr = inner.value()?.parse()?;
9194 let raw = s.value();
9195 if raw.trim().is_empty() {
9196 return Err(syn::Error::new(
9197 s.span(),
9198 "`generic_has(name = \"...\")` must not be empty",
9199 ));
9200 }
9201 accessor_name = Some(raw);
9202 return Ok(());
9203 }
9204 if inner.path.is_ident("child") {
9205 let s: LitStr = inner.value()?.parse()?;
9206 let trimmed = s.value().trim().to_owned();
9207 if trimmed.is_empty() {
9208 return Err(syn::Error::new(
9209 s.span(),
9210 "`generic_has(child = \"...\")` must not be empty",
9211 ));
9212 }
9213 child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9214 return Ok(());
9215 }
9216 if inner.path.is_ident("ct_column") {
9217 let s: LitStr = inner.value()?.parse()?;
9218 let trimmed = s.value().trim().to_owned();
9219 if trimmed.is_empty() {
9220 return Err(syn::Error::new(
9221 s.span(),
9222 "`generic_has(ct_column = \"...\")` must not be empty",
9223 ));
9224 }
9225 ct_column = Some(trimmed);
9226 return Ok(());
9227 }
9228 if inner.path.is_ident("pk_column") {
9229 let s: LitStr = inner.value()?.parse()?;
9230 let trimmed = s.value().trim().to_owned();
9231 if trimmed.is_empty() {
9232 return Err(syn::Error::new(
9233 s.span(),
9234 "`generic_has(pk_column = \"...\")` must not be empty",
9235 ));
9236 }
9237 pk_column = Some(trimmed);
9238 return Ok(());
9239 }
9240 if inner.path.is_ident("self_pk_column") {
9241 let s: LitStr = inner.value()?.parse()?;
9242 let trimmed = s.value().trim().to_owned();
9243 if trimmed.is_empty() {
9244 return Err(syn::Error::new(
9245 s.span(),
9246 "`generic_has(self_pk_column = \"...\")` must not be empty",
9247 ));
9248 }
9249 self_pk_column = Some(trimmed);
9250 return Ok(());
9251 }
9252 Err(inner.error(
9253 "unknown `generic_has` attribute (supported: \
9254 `name`, `child`, `ct_column`, `pk_column`, \
9255 `self_pk_column`)",
9256 ))
9257 })?;
9258 let Some(name) = accessor_name else {
9259 return Err(syn::Error::new(
9260 span,
9261 "`generic_has` requires `name = \"...\"`",
9262 ));
9263 };
9264 let Some(child) = child_ident else {
9265 return Err(syn::Error::new(
9266 span,
9267 "`generic_has` requires `child = \"ChildModelType\"`",
9268 ));
9269 };
9270 let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9271 let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9272 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9273 if out.generic_has_relations.iter().any(|r| r.name == name) {
9274 return Err(syn::Error::new(
9275 span,
9276 format!(
9277 "duplicate `generic_has(name = \"{name}\")` — \
9278 pick a unique accessor name"
9279 ),
9280 ));
9281 }
9282 out.generic_has_relations.push(GenericHasAttr {
9283 name,
9284 child,
9285 ct_column,
9286 pk_column,
9287 self_pk_column,
9288 });
9289 return Ok(());
9290 }
9291 if meta.path.is_ident("audit") {
9292 let mut audit = AuditAttrs::default();
9293 meta.parse_nested_meta(|inner| {
9294 if inner.path.is_ident("track") {
9295 let s: LitStr = inner.value()?.parse()?;
9296 audit.track =
9297 Some((split_field_list(&s.value()), s.span()));
9298 return Ok(());
9299 }
9300 Err(inner.error(
9301 "unknown audit attribute (supported: `track`)",
9302 ))
9303 })?;
9304 out.audit = Some(audit);
9305 return Ok(());
9306 }
9307 if meta.path.is_ident("permissions") {
9308 // Two forms accepted:
9309 // #[rustango(permissions)] — flag form, true
9310 // #[rustango(permissions = false)] — explicit opt-out
9311 // #[rustango(permissions = true)] — explicit opt-in
9312 if let Ok(v) = meta.value() {
9313 let lit: syn::LitBool = v.parse()?;
9314 out.permissions = lit.value;
9315 } else {
9316 out.permissions = true;
9317 }
9318 return Ok(());
9319 }
9320 if meta.path.is_ident("view") {
9321 // Issue #293 / T2.10. Two forms accepted, matching
9322 // the `permissions` flag pattern:
9323 // #[rustango(view)] — flag form, true
9324 // #[rustango(view = false)] — explicit opt-out
9325 // #[rustango(view = true)] — explicit opt-in
9326 if let Ok(v) = meta.value() {
9327 let lit: syn::LitBool = v.parse()?;
9328 out.is_view = lit.value;
9329 } else {
9330 out.is_view = true;
9331 }
9332 return Ok(());
9333 }
9334 if meta.path.is_ident("managed") {
9335 // Django-shape Meta.managed. Issue #321.
9336 // #[rustango(managed = false)] — operator-managed table
9337 // #[rustango(managed = true)] — rustango-managed (the default)
9338 // Bare-flag form is intentionally not accepted: writing
9339 // `#[rustango(managed)]` reads as "yes please manage it"
9340 // which is already the default. The opt-out is the only
9341 // useful state, so it must be explicit.
9342 let v = meta.value()?;
9343 let lit: syn::LitBool = v.parse()?;
9344 out.managed = lit.value;
9345 return Ok(());
9346 }
9347 if meta.path.is_ident("verbose_name") {
9348 let s: LitStr = meta.value()?.parse()?;
9349 out.verbose_name = Some(s.value());
9350 return Ok(());
9351 }
9352 if meta.path.is_ident("verbose_name_plural") {
9353 let s: LitStr = meta.value()?.parse()?;
9354 out.verbose_name_plural = Some(s.value());
9355 return Ok(());
9356 }
9357 if meta.path.is_ident("db_table_comment") {
9358 // Django-shape `Meta.db_table_comment` (4.2+) — free-form
9359 // table-level comment attached to the DB catalog.
9360 let s: LitStr = meta.value()?.parse()?;
9361 out.db_table_comment = Some(s.value());
9362 return Ok(());
9363 }
9364 if meta.path.is_ident("proxy") {
9365 // Django-shape `Meta.proxy = True` — declarative flag
9366 // marking the struct as a proxy of another model that
9367 // shares its DB table. Stored on `ModelSchema::proxy`
9368 // so future codegen can skip `CreateTable` emission
9369 // for proxies (parent owns the table) and route
9370 // per-instance method resolution to the proxy class.
9371 //
9372 // Accepts `proxy` (bare → true) and `proxy = true/false`.
9373 let value = if meta.input.peek(syn::Token![=]) {
9374 meta.value()?.parse::<syn::LitBool>()?.value
9375 } else {
9376 true
9377 };
9378 out.proxy = value;
9379 return Ok(());
9380 }
9381 if meta.path.is_ident("order_with_respect_to") {
9382 // Django-shape `Meta.order_with_respect_to = "parent_fk"` —
9383 // the model's instances are intrinsically ordered
9384 // relative to their parent FK. Django auto-generates
9385 // a `_order` integer column + admin reordering UI.
9386 //
9387 // rustango stores the FK field name on
9388 // `ModelSchema::order_with_respect_to`. Declarative-only
9389 // today: the migration writer + admin surfaces still
9390 // treat every model identically. Future codegen will
9391 // key off the metadata to auto-emit the `_order`
9392 // column and reorder helpers.
9393 //
9394 // Validated as a Rust-shape identifier so the macro
9395 // can reject typos at derive time.
9396 let s: LitStr = meta.value()?.parse()?;
9397 let raw = s.value();
9398 if raw.is_empty() {
9399 return Err(syn::Error::new(
9400 s.span(),
9401 "`order_with_respect_to` must be a non-empty FK field name",
9402 ));
9403 }
9404 let valid = raw
9405 .chars()
9406 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9407 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9408 if !valid {
9409 return Err(syn::Error::new(
9410 s.span(),
9411 format!(
9412 "`order_with_respect_to` must be a valid Rust \
9413 identifier (letters / digits / underscores, \
9414 not starting with a digit); got `{raw}`"
9415 ),
9416 ));
9417 }
9418 out.order_with_respect_to = Some(raw);
9419 return Ok(());
9420 }
9421 if meta.path.is_ident("required_db_features") {
9422 // Django-shape `Meta.required_db_features` — capability
9423 // tokens the model needs (e.g. `"json_extract"`,
9424 // `"window_functions"`, `"row_security"`). Comma-separated.
9425 // `manage check --deploy` walks every model and warns
9426 // when the active backend doesn't advertise the
9427 // capability.
9428 //
9429 // rustango ships a small registry of capability tokens
9430 // each dialect supports — see `Dialect::supports`.
9431 // Unknown tokens still parse (they end up on the
9432 // schema and show up in the warning) so projects can
9433 // declare aspirational capabilities and the check
9434 // verb will keep nagging until the dialect implements
9435 // them.
9436 let s: LitStr = meta.value()?.parse()?;
9437 let raw = s.value();
9438 let features: Vec<String> = raw
9439 .split(',')
9440 .map(str::trim)
9441 .filter(|s| !s.is_empty())
9442 .map(str::to_owned)
9443 .collect();
9444 if features.is_empty() {
9445 return Err(syn::Error::new(
9446 s.span(),
9447 "`required_db_features` must list at least one \
9448 comma-separated capability token",
9449 ));
9450 }
9451 out.required_db_features = features;
9452 return Ok(());
9453 }
9454 if meta.path.is_ident("required_db_vendor") {
9455 // Django-shape `Meta.required_db_vendor` — the model
9456 // is only meant to run against the named DB backend.
9457 // `manage check --deploy` flags a mismatch so
9458 // ops catches "I forgot to switch DATABASE_URL" at
9459 // deploy time rather than runtime.
9460 //
9461 // Django spells it as a free-form string; rustango
9462 // restricts to the three backends it ships dialects
9463 // for so the check verb can compare reliably.
9464 let s: LitStr = meta.value()?.parse()?;
9465 let raw = s.value().to_ascii_lowercase();
9466 match raw.as_str() {
9467 "postgresql" | "postgres" | "pg" => {
9468 out.required_db_vendor = Some("postgres".to_owned());
9469 }
9470 "mysql" | "mariadb" => {
9471 out.required_db_vendor = Some("mysql".to_owned());
9472 }
9473 "sqlite" | "sqlite3" => {
9474 out.required_db_vendor = Some("sqlite".to_owned());
9475 }
9476 _ => {
9477 return Err(syn::Error::new(
9478 s.span(),
9479 format!(
9480 "unknown required_db_vendor `{raw}` — \
9481 expected `postgres` (aliases: `postgresql`, `pg`), \
9482 `mysql` (alias: `mariadb`), or `sqlite` \
9483 (alias: `sqlite3`)"
9484 ),
9485 ));
9486 }
9487 }
9488 return Ok(());
9489 }
9490 if meta.path.is_ident("base_manager_name") {
9491 // Django-shape `Meta.base_manager_name` — name of the
9492 // Manager subclass that `<instance>.<relation>_set`
9493 // uses when resolving reverse-relation managers.
9494 // Distinct from `default_manager_name` (what
9495 // `Model.objects` returns at the class level).
9496 // Stored on `ModelSchema::base_manager_name`.
9497 //
9498 // Validated as a Rust identifier so it stays safe to
9499 // re-emit as code in future reverse-manager codegen.
9500 let s: LitStr = meta.value()?.parse()?;
9501 let raw = s.value();
9502 if raw.is_empty() {
9503 return Err(syn::Error::new(
9504 s.span(),
9505 "`base_manager_name` must be a non-empty string",
9506 ));
9507 }
9508 let valid = raw
9509 .chars()
9510 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9511 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9512 if !valid {
9513 return Err(syn::Error::new(
9514 s.span(),
9515 format!(
9516 "`base_manager_name` must be a valid Rust \
9517 identifier (letters / digits / underscores, \
9518 not starting with a digit); got `{raw}`"
9519 ),
9520 ));
9521 }
9522 out.base_manager_name = Some(raw);
9523 return Ok(());
9524 }
9525 if meta.path.is_ident("default_related_name") {
9526 // Django-shape `Meta.default_related_name` — the name
9527 // reverse-relation accessors use when callers don't
9528 // override `related_name=...` on the FK / M2M field.
9529 // Stored on `ModelSchema::default_related_name` so
9530 // future reverse-manager codegen / DRF schema emit /
9531 // admin templates can pick the right accessor name
9532 // (today rustango doesn't auto-emit reverse managers;
9533 // the metadata is the foundation for that work).
9534 //
9535 // Django requires snake_case + no `+` suffix; we
9536 // enforce non-empty + ASCII identifier-shape so the
9537 // string is safe to use as a Rust ident later.
9538 let s: LitStr = meta.value()?.parse()?;
9539 let raw = s.value();
9540 if raw.is_empty() {
9541 return Err(syn::Error::new(
9542 s.span(),
9543 "`default_related_name` must be a non-empty string",
9544 ));
9545 }
9546 let valid = raw
9547 .chars()
9548 .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9549 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9550 if !valid {
9551 return Err(syn::Error::new(
9552 s.span(),
9553 format!(
9554 "`default_related_name` must be snake_case ASCII \
9555 (lowercase letters / digits / underscores, not \
9556 starting with a digit); got `{raw}`"
9557 ),
9558 ));
9559 }
9560 out.default_related_name = Some(raw);
9561 return Ok(());
9562 }
9563 if meta.path.is_ident("extra_permissions") {
9564 // Django-shape `Meta.permissions = [(codename, name), ...]`.
9565 // Comma-separated `codename:label` pairs.
9566 let s: LitStr = meta.value()?.parse()?;
9567 let raw = s.value();
9568 let mut pairs = Vec::new();
9569 for entry in raw.split(',') {
9570 let entry = entry.trim();
9571 if entry.is_empty() {
9572 continue;
9573 }
9574 let (codename, label) = match entry.split_once(':') {
9575 Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9576 None => (entry.to_owned(), entry.to_owned()),
9577 };
9578 if codename.is_empty() {
9579 return Err(meta.error(
9580 "`extra_permissions` entries must be `codename:label` pairs",
9581 ));
9582 }
9583 pairs.push((codename, label));
9584 }
9585 if pairs.is_empty() {
9586 return Err(meta
9587 .error("`extra_permissions = \"…\"` must list at least one pair"));
9588 }
9589 out.extra_permissions = pairs;
9590 return Ok(());
9591 }
9592 if meta.path.is_ident("default_permissions") {
9593 // Django-shape `Meta.default_permissions = ('view',
9594 // 'change')`. Comma-separated subset of the CRUD
9595 // action set. Empty means all four (the framework
9596 // default — matches Django when the option is
9597 // omitted).
9598 let s: LitStr = meta.value()?.parse()?;
9599 let raw = s.value();
9600 let mut actions: Vec<String> = Vec::new();
9601 for entry in raw.split(',') {
9602 let action = entry.trim().to_ascii_lowercase();
9603 if action.is_empty() {
9604 continue;
9605 }
9606 match action.as_str() {
9607 "add" | "change" | "delete" | "view" => {}
9608 other => {
9609 return Err(syn::Error::new(
9610 s.span(),
9611 format!(
9612 "unknown default_permissions action `{other}` — \
9613 expected one of `add`, `change`, `delete`, `view`"
9614 ),
9615 ));
9616 }
9617 }
9618 if !actions.contains(&action) {
9619 actions.push(action);
9620 }
9621 }
9622 if actions.is_empty() {
9623 return Err(syn::Error::new(
9624 s.span(),
9625 "`default_permissions = \"…\"` must list at least one action; \
9626 use `permissions = false` on the container if you want NO \
9627 permissions seeded for this model.",
9628 ));
9629 }
9630 out.default_permissions = actions;
9631 return Ok(());
9632 }
9633 if meta.path.is_ident("get_latest_by") {
9634 // Django-shape `Meta.get_latest_by`. The `-` prefix
9635 // selects descending order (Django muscle memory).
9636 let s: LitStr = meta.value()?.parse()?;
9637 let raw = s.value();
9638 let trimmed = raw.trim();
9639 if trimmed.is_empty() {
9640 return Err(meta.error("`get_latest_by` must name a column"));
9641 }
9642 let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9643 (stripped.to_owned(), true)
9644 } else if let Some(stripped) = trimmed.strip_prefix('+') {
9645 (stripped.to_owned(), false)
9646 } else {
9647 (trimmed.to_owned(), false)
9648 };
9649 if col.is_empty() {
9650 return Err(meta.error("`get_latest_by` must name a column"));
9651 }
9652 out.get_latest_by = Some((col, desc));
9653 return Ok(());
9654 }
9655 if meta.path.is_ident("unique_together") {
9656 // Django-shape composite UNIQUE index. Two syntaxes:
9657 //
9658 // #[rustango(unique_together = "org_id, user_id")] — auto-derived name
9659 // #[rustango(unique_together(columns = "org_id, user_id", name = "x"))] — explicit name
9660 //
9661 // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9662 // (col1, col2)`, where <name> defaults to
9663 // `<table>_<col1>_<col2>_uq` when not supplied.
9664 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9665 out.indexes.push(IndexAttr {
9666 name,
9667 columns,
9668 unique: true,
9669 method: "btree".to_owned(),
9670 where_clause: None,
9671 include: Vec::new(),
9672 });
9673 return Ok(());
9674 }
9675 if meta.path.is_ident("index_together") {
9676 // Django-shape composite (non-unique) index. Two syntaxes
9677 // mirroring `unique_together`.
9678 //
9679 // #[rustango(index_together = "created_at, status")]
9680 // #[rustango(index_together(columns = "created_at, status", name = "x"))]
9681 let (columns, name) = parse_together_attr(&meta, "index_together")?;
9682 out.indexes.push(IndexAttr {
9683 name,
9684 columns,
9685 unique: false,
9686 method: "btree".to_owned(),
9687 where_clause: None,
9688 include: Vec::new(),
9689 });
9690 return Ok(());
9691 }
9692 if meta.path.is_ident("unique_when") {
9693 // Django 4.0+ `UniqueConstraint(condition=Q(...))` —
9694 // partial unique index. Issue #265 / T1.3.
9695 //
9696 // #[rustango(unique_when(
9697 // columns = "email",
9698 // condition = "deleted_at IS NULL",
9699 // name = "unique_active_email"
9700 // ))]
9701 //
9702 // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9703 // on PG / SQLite (both ship partial indexes natively).
9704 // MySQL falls back to a plain UNIQUE index — the
9705 // condition is lost; document the limitation in the
9706 // generated migration.
9707 let mut columns: Option<Vec<String>> = None;
9708 let mut condition: Option<String> = None;
9709 let mut name: Option<String> = None;
9710 let mut include: Vec<String> = Vec::new();
9711 meta.parse_nested_meta(|inner| {
9712 if inner.path.is_ident("columns") {
9713 let s: LitStr = inner.value()?.parse()?;
9714 columns = Some(split_field_list(&s.value()));
9715 return Ok(());
9716 }
9717 if inner.path.is_ident("condition") {
9718 let s: LitStr = inner.value()?.parse()?;
9719 condition = Some(s.value());
9720 return Ok(());
9721 }
9722 if inner.path.is_ident("name") {
9723 let s: LitStr = inner.value()?.parse()?;
9724 name = Some(s.value());
9725 return Ok(());
9726 }
9727 if inner.path.is_ident("include") {
9728 // Django `UniqueConstraint(include=[...])` — PG
9729 // 11+ covering-index columns. Non-key columns
9730 // travel with the index leaf for index-only
9731 // scans. Dropped on MySQL/SQLite by the writer.
9732 let s: LitStr = inner.value()?.parse()?;
9733 include = split_field_list(&s.value());
9734 return Ok(());
9735 }
9736 Err(inner.error(
9737 "unknown unique_when attribute (supported: \
9738 `columns = \"...\"`, `condition = \"...\"`, \
9739 `name = \"...\"`, `include = \"...\"`)",
9740 ))
9741 })?;
9742 let columns = columns.ok_or_else(|| {
9743 meta.error("`unique_when(...)` requires `columns = \"...\"`")
9744 })?;
9745 let condition = condition.ok_or_else(|| {
9746 meta.error("`unique_when(...)` requires `condition = \"...\"`")
9747 })?;
9748 if columns.is_empty() {
9749 return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9750 }
9751 out.indexes.push(IndexAttr {
9752 name,
9753 columns,
9754 unique: true,
9755 method: "btree".to_owned(),
9756 where_clause: Some(condition),
9757 include,
9758 });
9759 return Ok(());
9760 }
9761 if meta.path.is_ident("index_when") {
9762 // Django `Index(fields=..., condition=Q(...))` parity —
9763 // non-unique partial index. Sibling of `unique_when`
9764 // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9765 //
9766 // #[rustango(index_when(
9767 // columns = "status, created_at",
9768 // condition = "deleted_at IS NULL",
9769 // name = "active_status_created_idx"
9770 // ))]
9771 //
9772 // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9773 // on PG / SQLite (both ship partial indexes natively).
9774 // MySQL has no native partial-index support — the writer
9775 // emits a plain CREATE INDEX and the condition is lost;
9776 // operators wanting that selectivity on MySQL should
9777 // declare a covering index plus an application-level
9778 // filter.
9779 let mut columns: Option<Vec<String>> = None;
9780 let mut condition: Option<String> = None;
9781 let mut name: Option<String> = None;
9782 let mut method: String = "btree".to_owned();
9783 let mut include: Vec<String> = Vec::new();
9784 meta.parse_nested_meta(|inner| {
9785 if inner.path.is_ident("columns") {
9786 let s: LitStr = inner.value()?.parse()?;
9787 columns = Some(split_field_list(&s.value()));
9788 return Ok(());
9789 }
9790 if inner.path.is_ident("condition") {
9791 let s: LitStr = inner.value()?.parse()?;
9792 condition = Some(s.value());
9793 return Ok(());
9794 }
9795 if inner.path.is_ident("name") {
9796 let s: LitStr = inner.value()?.parse()?;
9797 name = Some(s.value());
9798 return Ok(());
9799 }
9800 if inner.path.is_ident("method") {
9801 let s: LitStr = inner.value()?.parse()?;
9802 method = s.value();
9803 return Ok(());
9804 }
9805 if inner.path.is_ident("include") {
9806 // Django `Index(include=[...])` — PG 11+
9807 // covering-index columns; non-key columns
9808 // travel with the index leaf. Dropped on
9809 // MySQL/SQLite.
9810 let s: LitStr = inner.value()?.parse()?;
9811 include = split_field_list(&s.value());
9812 return Ok(());
9813 }
9814 Err(inner.error(
9815 "unknown index_when attribute (supported: \
9816 `columns = \"...\"`, `condition = \"...\"`, \
9817 `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9818 `include = \"...\"`)",
9819 ))
9820 })?;
9821 let columns = columns
9822 .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9823 let condition = condition.ok_or_else(|| {
9824 meta.error("`index_when(...)` requires `condition = \"...\"`")
9825 })?;
9826 if columns.is_empty() {
9827 return Err(meta.error("`index_when(columns = \"\")` is empty"));
9828 }
9829 out.indexes.push(IndexAttr {
9830 name,
9831 columns,
9832 unique: false,
9833 method,
9834 where_clause: Some(condition),
9835 include,
9836 });
9837 return Ok(());
9838 }
9839 if meta.path.is_ident("index") {
9840 // Container-level composite index. Two syntaxes:
9841 // #[rustango(index = "col1, col2")] — bare, non-unique btree
9842 // #[rustango(index("col1, col2"))] — call form (same result)
9843 // #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
9844 // The call form takes the column list as a leading string
9845 // literal, then optional `unique` / `name = "..."` /
9846 // `method = "..."` flags (a leading literal can't compose
9847 // under `parse_nested_meta`, so the paren body is parsed by
9848 // hand). `unique_together` / `index_together` remain the
9849 // Django-shape aliases for the same feature.
9850 let cols_lit: LitStr;
9851 let mut unique = false;
9852 let mut name: Option<String> = None;
9853 let mut method = "btree".to_owned();
9854 if meta.input.peek(syn::token::Paren) {
9855 let content;
9856 syn::parenthesized!(content in meta.input);
9857 cols_lit = content.parse()?;
9858 while content.peek(syn::Token![,]) {
9859 content.parse::<syn::Token![,]>()?;
9860 if content.is_empty() {
9861 break;
9862 }
9863 let flag: syn::Ident = content.parse()?;
9864 if flag == "unique" {
9865 unique = true;
9866 } else if flag == "name" {
9867 content.parse::<syn::Token![=]>()?;
9868 let s: LitStr = content.parse()?;
9869 name = Some(s.value());
9870 } else if flag == "method" {
9871 content.parse::<syn::Token![=]>()?;
9872 let s: LitStr = content.parse()?;
9873 let v = s.value();
9874 match v.as_str() {
9875 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
9876 | "bloom" => method = v,
9877 other => {
9878 return Err(syn::Error::new(
9879 s.span(),
9880 format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
9881 ));
9882 }
9883 }
9884 } else {
9885 return Err(syn::Error::new(
9886 flag.span(),
9887 "unknown index flag (supported: `unique`, `name`, `method`)",
9888 ));
9889 }
9890 }
9891 } else {
9892 cols_lit = meta.value()?.parse()?;
9893 }
9894 let columns = split_field_list(&cols_lit.value());
9895 out.indexes.push(IndexAttr {
9896 name,
9897 columns,
9898 unique,
9899 method,
9900 where_clause: None,
9901 include: Vec::new(),
9902 });
9903 return Ok(());
9904 }
9905 if meta.path.is_ident("check") {
9906 // #[rustango(check(name = "…", expr = "…"))]
9907 let mut name: Option<String> = None;
9908 let mut expr: Option<String> = None;
9909 meta.parse_nested_meta(|inner| {
9910 if inner.path.is_ident("name") {
9911 let s: LitStr = inner.value()?.parse()?;
9912 name = Some(s.value());
9913 return Ok(());
9914 }
9915 if inner.path.is_ident("expr") {
9916 let s: LitStr = inner.value()?.parse()?;
9917 expr = Some(s.value());
9918 return Ok(());
9919 }
9920 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
9921 })?;
9922 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
9923 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
9924 out.checks.push(CheckAttr { name, expr });
9925 return Ok(());
9926 }
9927 if meta.path.is_ident("exclude") {
9928 // #[rustango(exclude(name = "…", using = "gist",
9929 // elements = "col WITH op, col WITH op",
9930 // where = "…"))]
9931 let mut name: Option<String> = None;
9932 let mut using: Option<String> = None;
9933 let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
9934 let mut where_clause: Option<String> = None;
9935 meta.parse_nested_meta(|inner| {
9936 if inner.path.is_ident("name") {
9937 let s: LitStr = inner.value()?.parse()?;
9938 name = Some(s.value());
9939 return Ok(());
9940 }
9941 if inner.path.is_ident("using") {
9942 let s: LitStr = inner.value()?.parse()?;
9943 using = Some(s.value());
9944 return Ok(());
9945 }
9946 if inner.path.is_ident("elements") {
9947 let s: LitStr = inner.value()?.parse()?;
9948 elements_raw = Some((s.value(), s.span()));
9949 return Ok(());
9950 }
9951 if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
9952 let s: LitStr = inner.value()?.parse()?;
9953 where_clause = Some(s.value());
9954 return Ok(());
9955 }
9956 Err(inner.error(
9957 "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
9958 ))
9959 })?;
9960 let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
9961 let using = using.unwrap_or_else(|| "gist".to_owned());
9962 let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
9963 meta.error(
9964 "exclude requires `elements = \"col WITH op, col WITH op\"`",
9965 )
9966 })?;
9967 // Parse `col WITH op` pairs separated by commas.
9968 let mut elements: Vec<(String, String)> = Vec::new();
9969 for pair in elements_str.split(',') {
9970 let pair = pair.trim();
9971 if pair.is_empty() {
9972 continue;
9973 }
9974 let mut split = pair.splitn(2, |c: char| c.is_whitespace());
9975 let col = split.next().unwrap_or("").trim();
9976 let rest = split.next().unwrap_or("").trim();
9977 // `WITH op` — case-insensitive on `WITH`, then op.
9978 let rest_lc = rest.to_ascii_lowercase();
9979 let op = rest_lc
9980 .strip_prefix("with")
9981 .map(|r| r.trim_start())
9982 .filter(|r| !r.is_empty())
9983 .map(|_| {
9984 // Pull the original-case op from `rest` after the
9985 // `WITH ` token (5 chars).
9986 rest[4..].trim_start().to_owned()
9987 });
9988 let Some(op) = op else {
9989 return Err(syn::Error::new(
9990 elements_span,
9991 format!(
9992 "exclude elements: `{pair}` must be `<col> WITH <op>` \
9993 (e.g. `room_id WITH =` or `during WITH &&`)"
9994 ),
9995 ));
9996 };
9997 if col.is_empty() || op.is_empty() {
9998 return Err(syn::Error::new(
9999 elements_span,
10000 format!(
10001 "exclude elements: `{pair}` must be `<col> WITH <op>` \
10002 (both sides non-empty)"
10003 ),
10004 ));
10005 }
10006 elements.push((col.to_owned(), op));
10007 }
10008 if elements.is_empty() {
10009 return Err(syn::Error::new(
10010 elements_span,
10011 "exclude requires at least one `col WITH op` element",
10012 ));
10013 }
10014 out.excludes.push(ExcludeAttr {
10015 name,
10016 using,
10017 elements,
10018 where_clause,
10019 });
10020 return Ok(());
10021 }
10022 if meta.path.is_ident("generic_fk") {
10023 let mut gfk = GenericFkAttr {
10024 name: String::new(),
10025 ct_column: String::new(),
10026 pk_column: String::new(),
10027 };
10028 meta.parse_nested_meta(|inner| {
10029 if inner.path.is_ident("name") {
10030 let s: LitStr = inner.value()?.parse()?;
10031 gfk.name = s.value();
10032 return Ok(());
10033 }
10034 if inner.path.is_ident("ct_column") {
10035 let s: LitStr = inner.value()?.parse()?;
10036 gfk.ct_column = s.value();
10037 return Ok(());
10038 }
10039 if inner.path.is_ident("pk_column") {
10040 let s: LitStr = inner.value()?.parse()?;
10041 gfk.pk_column = s.value();
10042 return Ok(());
10043 }
10044 Err(inner.error(
10045 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10046 ))
10047 })?;
10048 if gfk.name.is_empty() {
10049 return Err(meta.error("generic_fk requires `name = \"...\"`"));
10050 }
10051 if gfk.ct_column.is_empty() {
10052 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10053 }
10054 if gfk.pk_column.is_empty() {
10055 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10056 }
10057 out.generic_fks.push(gfk);
10058 return Ok(());
10059 }
10060 if meta.path.is_ident("fk_composite") {
10061 let mut fk = CompositeFkAttr {
10062 name: String::new(),
10063 to: String::new(),
10064 from: Vec::new(),
10065 on: Vec::new(),
10066 };
10067 meta.parse_nested_meta(|inner| {
10068 if inner.path.is_ident("name") {
10069 let s: LitStr = inner.value()?.parse()?;
10070 fk.name = s.value();
10071 return Ok(());
10072 }
10073 if inner.path.is_ident("to") {
10074 let s: LitStr = inner.value()?.parse()?;
10075 fk.to = s.value();
10076 return Ok(());
10077 }
10078 // `on = ("col1", "col2", ...)` — parse a parenthesised
10079 // comma-list of string literals.
10080 if inner.path.is_ident("on") || inner.path.is_ident("from") {
10081 let value = inner.value()?;
10082 let content;
10083 syn::parenthesized!(content in value);
10084 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10085 content.parse_terminated(
10086 |p| p.parse::<syn::LitStr>(),
10087 syn::Token![,],
10088 )?;
10089 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10090 if inner.path.is_ident("on") {
10091 fk.on = cols;
10092 } else {
10093 fk.from = cols;
10094 }
10095 return Ok(());
10096 }
10097 Err(inner.error(
10098 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10099 ))
10100 })?;
10101 if fk.name.is_empty() {
10102 return Err(meta.error("fk_composite requires `name = \"...\"`"));
10103 }
10104 if fk.to.is_empty() {
10105 return Err(meta.error("fk_composite requires `to = \"...\"`"));
10106 }
10107 if fk.from.is_empty() || fk.on.is_empty() {
10108 return Err(meta.error(
10109 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10110 ));
10111 }
10112 if fk.from.len() != fk.on.len() {
10113 return Err(meta.error(format!(
10114 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10115 fk.from.len(),
10116 fk.on.len(),
10117 )));
10118 }
10119 out.composite_fks.push(fk);
10120 return Ok(());
10121 }
10122 if meta.path.is_ident("m2m") {
10123 let mut m2m = M2MAttr {
10124 name: String::new(),
10125 to: String::new(),
10126 through: String::new(),
10127 src: String::new(),
10128 dst: String::new(),
10129 auto_create: true,
10130 };
10131 meta.parse_nested_meta(|inner| {
10132 if inner.path.is_ident("name") {
10133 let s: LitStr = inner.value()?.parse()?;
10134 m2m.name = s.value();
10135 return Ok(());
10136 }
10137 if inner.path.is_ident("to") {
10138 let s: LitStr = inner.value()?.parse()?;
10139 m2m.to = s.value();
10140 return Ok(());
10141 }
10142 if inner.path.is_ident("through") {
10143 let s: LitStr = inner.value()?.parse()?;
10144 m2m.through = s.value();
10145 return Ok(());
10146 }
10147 if inner.path.is_ident("src") {
10148 let s: LitStr = inner.value()?.parse()?;
10149 m2m.src = s.value();
10150 return Ok(());
10151 }
10152 if inner.path.is_ident("dst") {
10153 let s: LitStr = inner.value()?.parse()?;
10154 m2m.dst = s.value();
10155 return Ok(());
10156 }
10157 if inner.path.is_ident("auto_create") {
10158 let lit: syn::LitBool = inner.value()?.parse()?;
10159 m2m.auto_create = lit.value;
10160 return Ok(());
10161 }
10162 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10163 })?;
10164 if m2m.name.is_empty() {
10165 return Err(meta.error("m2m requires `name = \"...\"`"));
10166 }
10167 if m2m.to.is_empty() {
10168 return Err(meta.error("m2m requires `to = \"...\"`"));
10169 }
10170 if m2m.through.is_empty() {
10171 return Err(meta.error("m2m requires `through = \"...\"`"));
10172 }
10173 if m2m.src.is_empty() {
10174 return Err(meta.error("m2m requires `src = \"...\"`"));
10175 }
10176 if m2m.dst.is_empty() {
10177 return Err(meta.error("m2m requires `dst = \"...\"`"));
10178 }
10179 out.m2m.push(m2m);
10180 return Ok(());
10181 }
10182 if meta.path.is_ident("generic_m2m") {
10183 let mut gm = GenericM2MAttr {
10184 name: String::new(),
10185 through: String::new(),
10186 pk_column: String::new(),
10187 ct_column: String::new(),
10188 related_column: String::new(),
10189 };
10190 meta.parse_nested_meta(|inner| {
10191 let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10192 let s: LitStr = inner.value()?.parse()?;
10193 Ok(s.value())
10194 };
10195 if inner.path.is_ident("name") {
10196 gm.name = field(&inner)?;
10197 return Ok(());
10198 }
10199 if inner.path.is_ident("through") {
10200 gm.through = field(&inner)?;
10201 return Ok(());
10202 }
10203 if inner.path.is_ident("pk_column") {
10204 gm.pk_column = field(&inner)?;
10205 return Ok(());
10206 }
10207 if inner.path.is_ident("ct_column") {
10208 gm.ct_column = field(&inner)?;
10209 return Ok(());
10210 }
10211 if inner.path.is_ident("related_column") {
10212 gm.related_column = field(&inner)?;
10213 return Ok(());
10214 }
10215 Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10216 })?;
10217 for (val, label) in [
10218 (&gm.name, "name"),
10219 (&gm.through, "through"),
10220 (&gm.pk_column, "pk_column"),
10221 (&gm.ct_column, "ct_column"),
10222 (&gm.related_column, "related_column"),
10223 ] {
10224 if val.is_empty() {
10225 return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10226 }
10227 }
10228 out.generic_m2m.push(gm);
10229 return Ok(());
10230 }
10231 Err(meta.error("unknown rustango container attribute"))
10232 })?;
10233 }
10234 Ok(out)
10235}
10236
10237/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10238/// owned field names, trimming whitespace and skipping empty entries.
10239/// Field-name validation against the model is done by the caller.
10240fn split_field_list(raw: &str) -> Vec<String> {
10241 raw.split(',')
10242 .map(str::trim)
10243 .filter(|s| !s.is_empty())
10244 .map(str::to_owned)
10245 .collect()
10246}
10247
10248/// Shared parser for `unique_together` and `index_together` container
10249/// attrs. Accepts both shapes:
10250///
10251/// * `attr = "col1, col2"` — auto-derived index name.
10252/// * `attr(columns = "col1, col2", name = "...")` — explicit name.
10253///
10254/// Returns `(columns, name)`.
10255fn parse_together_attr(
10256 meta: &syn::meta::ParseNestedMeta<'_>,
10257 attr: &str,
10258) -> syn::Result<(Vec<String>, Option<String>)> {
10259 // Disambiguate by whether the next token is `=` (key-value) or
10260 // `(` (parenthesized).
10261 if meta.input.peek(syn::Token![=]) {
10262 let cols_lit: LitStr = meta.value()?.parse()?;
10263 let columns = split_field_list(&cols_lit.value());
10264 check_together_columns(meta, attr, &columns)?;
10265 return Ok((columns, None));
10266 }
10267 let mut columns: Option<Vec<String>> = None;
10268 let mut name: Option<String> = None;
10269 meta.parse_nested_meta(|inner| {
10270 if inner.path.is_ident("columns") {
10271 let s: LitStr = inner.value()?.parse()?;
10272 columns = Some(split_field_list(&s.value()));
10273 return Ok(());
10274 }
10275 if inner.path.is_ident("name") {
10276 let s: LitStr = inner.value()?.parse()?;
10277 name = Some(s.value());
10278 return Ok(());
10279 }
10280 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10281 })?;
10282 let columns = columns.ok_or_else(|| {
10283 meta.error(format!(
10284 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10285 ))
10286 })?;
10287 check_together_columns(meta, attr, &columns)?;
10288 Ok((columns, name))
10289}
10290
10291fn check_together_columns(
10292 meta: &syn::meta::ParseNestedMeta<'_>,
10293 attr: &str,
10294 columns: &[String],
10295) -> syn::Result<()> {
10296 if columns.len() < 2 {
10297 let single = if attr == "unique_together" {
10298 "#[rustango(unique)] on the field"
10299 } else {
10300 "#[rustango(index)] on the field"
10301 };
10302 return Err(meta.error(format!(
10303 "{attr} expects two or more columns; for a single-column equivalent use {single}",
10304 )));
10305 }
10306 Ok(())
10307}
10308
10309/// Parse the fieldsets DSL: pipe-separated sections, optional
10310/// `"Title:"` prefix on each, comma-separated field names after.
10311/// Examples:
10312/// * `"name, office"` → one untitled section with two fields
10313/// * `"Identity: name, office | Metadata: created_at"` → two titled
10314/// sections
10315///
10316/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10317fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10318 raw.split('|')
10319 .map(str::trim)
10320 .filter(|s| !s.is_empty())
10321 .map(|section| {
10322 // Split off an optional `Title:` prefix (first colon).
10323 let (title, rest) = match section.split_once(':') {
10324 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10325 _ => (String::new(), section),
10326 };
10327 let fields = split_field_list(rest);
10328 (title, fields)
10329 })
10330 .collect()
10331}
10332
10333/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10334/// comma-separated entry maps a target field to one or more source
10335/// fields joined with `+`. Whitespace around tokens is trimmed.
10336/// Entries missing `:` or with empty target/source lists are dropped.
10337fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10338 raw.split(',')
10339 .map(str::trim)
10340 .filter(|s| !s.is_empty())
10341 .filter_map(|entry| {
10342 let (target, sources_raw) = entry.split_once(':')?;
10343 let target = target.trim().to_owned();
10344 if target.is_empty() {
10345 return None;
10346 }
10347 let sources: Vec<String> = sources_raw
10348 .split('+')
10349 .map(|s| s.trim().to_owned())
10350 .filter(|s| !s.is_empty())
10351 .collect();
10352 if sources.is_empty() {
10353 return None;
10354 }
10355 Some((target, sources))
10356 })
10357 .collect()
10358}
10359
10360/// Parse Django-shape `formfield_overrides` — `"field:widget,field2:widget2"`
10361/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10362/// and empty halves drop silently — the macro layer only enforces shape,
10363/// not field-name vs. widget-name validity (those checks happen at
10364/// `AdminConfig` consumption time). Issue #359.
10365fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10366 raw.split(',')
10367 .map(str::trim)
10368 .filter(|s| !s.is_empty())
10369 .filter_map(|entry| {
10370 let (field, widget) = entry.split_once(':')?;
10371 let field = field.trim().to_owned();
10372 let widget = widget.trim().to_owned();
10373 if field.is_empty() || widget.is_empty() {
10374 return None;
10375 }
10376 Some((field, widget))
10377 })
10378 .collect()
10379}
10380
10381/// Parse Django-shape ordering — `"name"` is ASC, `"-name"` is DESC.
10382/// Returns `(field_name, desc)` pairs in the same order as the input.
10383fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10384 raw.split(',')
10385 .map(str::trim)
10386 .filter(|s| !s.is_empty())
10387 .map(|spec| {
10388 spec.strip_prefix('-')
10389 .map_or((spec.to_owned(), false), |rest| {
10390 (rest.trim().to_owned(), true)
10391 })
10392 })
10393 .collect()
10394}
10395
10396struct FieldAttrs {
10397 column: Option<String>,
10398 primary_key: bool,
10399 fk: Option<String>,
10400 o2o: Option<String>,
10401 on: Option<String>,
10402 /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10403 /// "set_default" | "no_action")]` — Django-shape
10404 /// `ForeignKey(on_delete=…)`. Only meaningful when `fk` / `o2o` is
10405 /// also set; the macro errors at compile time if applied to a
10406 /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10407 /// DDL writer renders `ON DELETE <action>` after the constraint
10408 /// clause when this is `Some`; `None` falls back to the database
10409 /// default (NO ACTION on every backend rustango supports).
10410 on_delete: Option<String>,
10411 /// `#[rustango(related_name = "...")]` — Django-shape per-FK
10412 /// reverse-accessor override. When set, the derive emits
10413 /// `Parent::<related_name>[_pool]` instead of the container-level
10414 /// `default_related_name` or the `<child_snake>_set[_pool]`
10415 /// fallback. Only meaningful when `fk` / `o2o` is also set;
10416 /// silently ignored on non-FK fields. Follow-up to #816.
10417 related_name: Option<String>,
10418 max_length: Option<u32>,
10419 /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10420 /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10421 /// unconstrained `vector` column.
10422 vector_dims: Option<u32>,
10423 /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10424 /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10425 /// unconstrained `geometry(Point)` column (SRID 0).
10426 geometry_srid: Option<u32>,
10427 min: Option<i64>,
10428 max: Option<i64>,
10429 default: Option<String>,
10430 /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10431 /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10432 /// "gen_random_uuid()"`. The Rust field type must be
10433 /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10434 /// INSERTs so the DB DEFAULT fires.
10435 auto_uuid: bool,
10436 /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10437 /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10438 /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10439 /// the field is `Auto::Unset`, then bound as a normal parameter
10440 /// rather than relying on a per-dialect DB function. Issue #823
10441 /// (Eloquent `HasUuids`).
10442 ///
10443 /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10444 /// Composes with every backend (PG / MySQL / SQLite) — no
10445 /// `gen_random_uuid()` requirement on the database.
10446 default_uuid_v7: bool,
10447 /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10448 /// Server-set on insert, immutable from app code afterwards.
10449 /// Implies `auto + default = "now()"`. Field type must be
10450 /// `DateTime<Utc>`.
10451 auto_now_add: bool,
10452 /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10453 /// every insert AND every update. Implies `auto + default =
10454 /// "now()"`; the macro additionally rewrites `update_on` /
10455 /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10456 /// field value.
10457 auto_now: bool,
10458 /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10459 /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10460 /// `soft_delete_on(executor)` and `restore_on(executor)`
10461 /// methods on the model.
10462 soft_delete: bool,
10463 /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10464 /// the column in the generated DDL.
10465 unique: bool,
10466 /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10467 /// generates a `CREATE INDEX` for this column. `unique` here means
10468 /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10469 index: bool,
10470 index_unique: bool,
10471 index_name: Option<String>,
10472 /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10473 /// `"btree"`. Issue #34.
10474 index_method: String,
10475 /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10476 /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10477 /// the macro skips this column from every INSERT and UPDATE
10478 /// path, so the database always recomputes the value from
10479 /// `EXPR`. Backlog item #35.
10480 generated_as: Option<String>,
10481 /// `#[rustango(help_text = "…")]` — Django-shape help text
10482 /// rendered below the admin form's input. Threaded into
10483 /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10484 /// layers can read it.
10485 help_text: Option<String>,
10486 /// `#[rustango(choices = "value:Label, value:Label")]` — Django-shape
10487 /// enumerated allowed values. Threaded into `FieldSchema::choices`
10488 /// as a `&'static [(&'static str, &'static str)]` slice. When
10489 /// present, the admin form renders a `<select>` instead of `<input>`
10490 /// and the validator rejects values not in the list. Only meaningful
10491 /// for `FieldType::String`; the macro errors at compile time if
10492 /// applied to a non-string field.
10493 choices: Option<Vec<(String, String)>>,
10494 /// `#[rustango(db_comment = "…")]` — Django-shape DB-side column
10495 /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10496 /// the comment in CREATE TABLE; Postgres emits a separate
10497 /// `COMMENT ON COLUMN` statement after the table is created;
10498 /// SQLite silently drops the value (no native column comments).
10499 db_comment: Option<String>,
10500 /// `#[rustango(verbose_name = "…")]` — Django-shape human-readable
10501 /// label for the field. Threaded into `FieldSchema::verbose_name`
10502 /// so admin column headers, form labels, and other display
10503 /// surfaces can prefer the friendly caption over the Rust
10504 /// identifier. `None` means renderers fall back to the field name.
10505 verbose_name: Option<String>,
10506 /// `#[rustango(editable = false)]` — Django-shape opt-out from
10507 /// auto-generated form rendering. Defaults to `true` so existing
10508 /// fields keep their current admin / form behavior; setting
10509 /// `false` removes the field from the admin change-form entirely
10510 /// (the value is still visible on detail / list views, just not
10511 /// editable).
10512 editable: bool,
10513 /// `#[rustango(blank)]` / `#[rustango(blank = true)]` — Django-shape
10514 /// "form may submit empty even when DB is NOT NULL". Threaded into
10515 /// `FieldSchema::blank`. Defaults to `false`.
10516 blank: bool,
10517 /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10518 /// Django-shape `CITextField`. Threaded into
10519 /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10520 /// fields; the macro errors at derive time if applied elsewhere.
10521 case_insensitive: bool,
10522 /// `#[rustango(validators = "email,url")]` — Django-shape
10523 /// model-level validator chain. Comma-separated names that
10524 /// dispatch to the `validators::*` family in `validate_value`.
10525 /// Empty by default; fires on every typed INSERT/UPDATE.
10526 validators: Vec<String>,
10527}
10528
10529fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10530 let mut out = FieldAttrs {
10531 column: None,
10532 primary_key: false,
10533 fk: None,
10534 o2o: None,
10535 on: None,
10536 on_delete: None,
10537 related_name: None,
10538 max_length: None,
10539 vector_dims: None,
10540 geometry_srid: None,
10541 min: None,
10542 max: None,
10543 default: None,
10544 auto_uuid: false,
10545 default_uuid_v7: false,
10546 auto_now_add: false,
10547 auto_now: false,
10548 soft_delete: false,
10549 unique: false,
10550 index: false,
10551 index_unique: false,
10552 index_name: None,
10553 index_method: "btree".to_owned(),
10554 generated_as: None,
10555 help_text: None,
10556 choices: None,
10557 db_comment: None,
10558 verbose_name: None,
10559 editable: true,
10560 blank: false,
10561 case_insensitive: false,
10562 validators: Vec::new(),
10563 };
10564 for attr in &field.attrs {
10565 if !attr.path().is_ident("rustango") {
10566 continue;
10567 }
10568 attr.parse_nested_meta(|meta| {
10569 if meta.path.is_ident("column") {
10570 let s: LitStr = meta.value()?.parse()?;
10571 let name = s.value();
10572 validate_sql_identifier(&name, "column", s.span())?;
10573 out.column = Some(name);
10574 return Ok(());
10575 }
10576 if meta.path.is_ident("primary_key") {
10577 out.primary_key = true;
10578 return Ok(());
10579 }
10580 if meta.path.is_ident("fk") {
10581 let s: LitStr = meta.value()?.parse()?;
10582 out.fk = Some(s.value());
10583 return Ok(());
10584 }
10585 if meta.path.is_ident("o2o") {
10586 let s: LitStr = meta.value()?.parse()?;
10587 out.o2o = Some(s.value());
10588 return Ok(());
10589 }
10590 if meta.path.is_ident("on") {
10591 let s: LitStr = meta.value()?.parse()?;
10592 out.on = Some(s.value());
10593 return Ok(());
10594 }
10595 if meta.path.is_ident("on_delete") {
10596 let s: LitStr = meta.value()?.parse()?;
10597 let raw = s.value();
10598 let normalized = raw.trim().to_ascii_lowercase();
10599 // Validate at parse time so the user gets a clear span
10600 // rather than a downstream compile error in the emit.
10601 match normalized.as_str() {
10602 "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10603 _ => {
10604 return Err(syn::Error::new(
10605 s.span(),
10606 format!(
10607 "unknown on_delete action `{raw}`; expected one of \
10608 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10609 ),
10610 ));
10611 }
10612 }
10613 out.on_delete = Some(normalized);
10614 return Ok(());
10615 }
10616 if meta.path.is_ident("related_name") {
10617 let s: LitStr = meta.value()?.parse()?;
10618 let raw = s.value();
10619 if raw.trim().is_empty() {
10620 return Err(syn::Error::new(
10621 s.span(),
10622 "`related_name` must be a non-empty identifier",
10623 ));
10624 }
10625 // Validate as a Rust-ident shape — the value becomes
10626 // a method name on the parent type. Same rule as
10627 // `default_related_name` so the two surfaces match.
10628 if !raw
10629 .chars()
10630 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10631 || raw.starts_with(char::is_numeric)
10632 {
10633 return Err(syn::Error::new(
10634 s.span(),
10635 "`related_name` must be snake_case ASCII (lowercase letters, \
10636 digits, underscores; no leading digit)",
10637 ));
10638 }
10639 out.related_name = Some(raw);
10640 return Ok(());
10641 }
10642 if meta.path.is_ident("max_length") {
10643 let lit: syn::LitInt = meta.value()?.parse()?;
10644 out.max_length = Some(lit.base10_parse::<u32>()?);
10645 return Ok(());
10646 }
10647 // `#[rustango(vector(dims = N))]` — pgvector column
10648 // dimension (#824). Nested-meta form so it reads like the
10649 // other typed-column attrs.
10650 if meta.path.is_ident("vector") {
10651 meta.parse_nested_meta(|inner| {
10652 if inner.path.is_ident("dims") {
10653 let lit: syn::LitInt = inner.value()?.parse()?;
10654 out.vector_dims = Some(lit.base10_parse::<u32>()?);
10655 return Ok(());
10656 }
10657 Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10658 })?;
10659 return Ok(());
10660 }
10661 // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10662 // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10663 if meta.path.is_ident("geometry") {
10664 meta.parse_nested_meta(|inner| {
10665 if inner.path.is_ident("srid") {
10666 let lit: syn::LitInt = inner.value()?.parse()?;
10667 out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10668 return Ok(());
10669 }
10670 Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10671 })?;
10672 return Ok(());
10673 }
10674 if meta.path.is_ident("min") {
10675 out.min = Some(parse_signed_i64(&meta)?);
10676 return Ok(());
10677 }
10678 if meta.path.is_ident("max") {
10679 out.max = Some(parse_signed_i64(&meta)?);
10680 return Ok(());
10681 }
10682 if meta.path.is_ident("default") {
10683 let s: LitStr = meta.value()?.parse()?;
10684 out.default = Some(s.value());
10685 return Ok(());
10686 }
10687 if meta.path.is_ident("generated_as") {
10688 let s: LitStr = meta.value()?.parse()?;
10689 out.generated_as = Some(s.value());
10690 return Ok(());
10691 }
10692 if meta.path.is_ident("help_text") {
10693 let s: LitStr = meta.value()?.parse()?;
10694 out.help_text = Some(s.value());
10695 return Ok(());
10696 }
10697 if meta.path.is_ident("choices") {
10698 let s: LitStr = meta.value()?.parse()?;
10699 let raw = s.value();
10700 let mut pairs: Vec<(String, String)> = Vec::new();
10701 for chunk in raw.split(',') {
10702 let chunk = chunk.trim();
10703 if chunk.is_empty() {
10704 continue;
10705 }
10706 let (value, label) = match chunk.split_once(':') {
10707 Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10708 None => (chunk.to_owned(), chunk.to_owned()),
10709 };
10710 if value.is_empty() {
10711 return Err(syn::Error::new(
10712 s.span(),
10713 "`choices` entry has empty value before `:`",
10714 ));
10715 }
10716 pairs.push((value, label));
10717 }
10718 if pairs.is_empty() {
10719 return Err(syn::Error::new(
10720 s.span(),
10721 "`choices = \"…\"` must contain at least one value",
10722 ));
10723 }
10724 out.choices = Some(pairs);
10725 return Ok(());
10726 }
10727 if meta.path.is_ident("db_comment") {
10728 let s: LitStr = meta.value()?.parse()?;
10729 out.db_comment = Some(s.value());
10730 return Ok(());
10731 }
10732 if meta.path.is_ident("verbose_name") {
10733 let s: LitStr = meta.value()?.parse()?;
10734 out.verbose_name = Some(s.value());
10735 return Ok(());
10736 }
10737 if meta.path.is_ident("editable") {
10738 // Two forms accepted:
10739 // #[rustango(editable = false)] / true — explicit
10740 // #[rustango(editable)] — flag form (= true, the
10741 // default, so harmless; included for symmetry)
10742 if let Ok(v) = meta.value() {
10743 let lit: syn::LitBool = v.parse()?;
10744 out.editable = lit.value;
10745 } else {
10746 out.editable = true;
10747 }
10748 return Ok(());
10749 }
10750 if meta.path.is_ident("blank") {
10751 // Two forms accepted:
10752 // #[rustango(blank)] — flag form, true
10753 // #[rustango(blank = false)] / true — explicit
10754 if let Ok(v) = meta.value() {
10755 let lit: syn::LitBool = v.parse()?;
10756 out.blank = lit.value;
10757 } else {
10758 out.blank = true;
10759 }
10760 return Ok(());
10761 }
10762 if meta.path.is_ident("citext") {
10763 // Django-parity CITextField (#344). Two forms:
10764 // #[rustango(citext)] — flag form, true
10765 // #[rustango(citext = true)] — explicit
10766 // #[rustango(citext = false)] — explicit opt-out
10767 // String-only validation lives in the field-type
10768 // emitter (the FieldType discriminant is computed in
10769 // `detect_type`); the macro records the flag and the
10770 // DDL writer emits dialect-specific COLLATE / CITEXT.
10771 if let Ok(v) = meta.value() {
10772 let lit: syn::LitBool = v.parse()?;
10773 out.case_insensitive = lit.value;
10774 } else {
10775 out.case_insensitive = true;
10776 }
10777 return Ok(());
10778 }
10779 if meta.path.is_ident("validators") {
10780 let s: LitStr = meta.value()?.parse()?;
10781 let raw = s.value();
10782 out.validators = raw
10783 .split(',')
10784 .map(str::trim)
10785 .filter(|s| !s.is_empty())
10786 .map(str::to_owned)
10787 .collect();
10788 if out.validators.is_empty() {
10789 return Err(syn::Error::new(
10790 s.span(),
10791 "`validators = \"…\"` must list at least one name",
10792 ));
10793 }
10794 return Ok(());
10795 }
10796 if meta.path.is_ident("auto_uuid") {
10797 out.auto_uuid = true;
10798 // Implied: PK + auto + DEFAULT gen_random_uuid().
10799 // Each is also explicitly settable; the explicit
10800 // value wins if conflicting.
10801 out.primary_key = true;
10802 if out.default.is_none() {
10803 out.default = Some("gen_random_uuid()".into());
10804 }
10805 return Ok(());
10806 }
10807 if meta.path.is_ident("default_uuid_v7") {
10808 // Backend-neutral counterpart of `auto_uuid` — issue #823.
10809 // No SQL DEFAULT (the macro fills the value Rust-side
10810 // before binding); just mark the field as PK + Auto
10811 // so the insert path is the `Auto::Unset → generate`
10812 // branch.
10813 out.default_uuid_v7 = true;
10814 out.primary_key = true;
10815 return Ok(());
10816 }
10817 if meta.path.is_ident("auto_now_add") {
10818 out.auto_now_add = true;
10819 if out.default.is_none() {
10820 out.default = Some("now()".into());
10821 }
10822 return Ok(());
10823 }
10824 if meta.path.is_ident("auto_now") {
10825 out.auto_now = true;
10826 if out.default.is_none() {
10827 out.default = Some("now()".into());
10828 }
10829 return Ok(());
10830 }
10831 if meta.path.is_ident("soft_delete") {
10832 out.soft_delete = true;
10833 return Ok(());
10834 }
10835 if meta.path.is_ident("unique") {
10836 out.unique = true;
10837 return Ok(());
10838 }
10839 if meta.path.is_ident("index") {
10840 out.index = true;
10841 // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
10842 if meta.input.peek(syn::token::Paren) {
10843 meta.parse_nested_meta(|inner| {
10844 if inner.path.is_ident("unique") {
10845 out.index_unique = true;
10846 return Ok(());
10847 }
10848 if inner.path.is_ident("name") {
10849 let s: LitStr = inner.value()?.parse()?;
10850 out.index_name = Some(s.value());
10851 return Ok(());
10852 }
10853 if inner.path.is_ident("method") {
10854 let s: LitStr = inner.value()?.parse()?;
10855 let v = s.value();
10856 match v.as_str() {
10857 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
10858 out.index_method = v;
10859 }
10860 other => {
10861 return Err(inner.error(format!(
10862 "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
10863 )));
10864 }
10865 }
10866 return Ok(());
10867 }
10868 Err(inner.error(
10869 "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
10870 ))
10871 })?;
10872 }
10873 return Ok(());
10874 }
10875 Err(meta.error("unknown rustango field attribute"))
10876 })?;
10877 }
10878 Ok(out)
10879}
10880
10881/// Parse a signed integer literal, accepting optional leading `-`.
10882fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
10883 let expr: syn::Expr = meta.value()?.parse()?;
10884 match expr {
10885 syn::Expr::Lit(syn::ExprLit {
10886 lit: syn::Lit::Int(lit),
10887 ..
10888 }) => lit.base10_parse::<i64>(),
10889 syn::Expr::Unary(syn::ExprUnary {
10890 op: syn::UnOp::Neg(_),
10891 expr,
10892 ..
10893 }) => {
10894 if let syn::Expr::Lit(syn::ExprLit {
10895 lit: syn::Lit::Int(lit),
10896 ..
10897 }) = *expr
10898 {
10899 let v: i64 = lit.base10_parse()?;
10900 Ok(-v)
10901 } else {
10902 Err(syn::Error::new_spanned(expr, "expected integer literal"))
10903 }
10904 }
10905 other => Err(syn::Error::new_spanned(
10906 other,
10907 "expected integer literal (signed)",
10908 )),
10909 }
10910}
10911
10912struct FieldInfo<'a> {
10913 ident: &'a syn::Ident,
10914 column: String,
10915 primary_key: bool,
10916 /// `true` when the Rust type was `Auto<T>` — the INSERT path will
10917 /// skip this column when `Auto::Unset` and emit it under
10918 /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
10919 auto: bool,
10920 /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
10921 /// the `Column::Value` associated type for typed-column tokens.
10922 value_ty: &'a Type,
10923 /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
10924 field_type_tokens: TokenStream2,
10925 schema: TokenStream2,
10926 from_row_init: TokenStream2,
10927 /// Variant of [`Self::from_row_init`] that reads the column via
10928 /// `format!("{prefix}__{col}")` so a model can be decoded out of
10929 /// the aliased columns of a JOINed row. Drives slice 9.0d's
10930 /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
10931 /// helper that `select_related` calls when stitching loaded FKs.
10932 from_aliased_row_init: TokenStream2,
10933 /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
10934 /// relation helper emit (`Author::<child>_set`) needs to know `T`
10935 /// to point the generated method at the right child model.
10936 fk_inner: Option<Type>,
10937 /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
10938 /// `kind` (since ForeignKey detection sets `kind` to K's
10939 /// underlying type) but stored separately for clarity at the
10940 /// `FkRelation` construction site, which only sees the FK's
10941 /// surface fields.
10942 fk_pk_kind: DetectedKind,
10943 /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
10944 /// the bare `ForeignKey<T, K>`. Routes the load_related and
10945 /// fk_pk_access emitters to wrap assignments / accessors in
10946 /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
10947 /// FK column compiles end-to-end. The DDL writer reads this off
10948 /// the field schema (`nullable` flag); the macro just needs to
10949 /// keep the Rust-side codegen consistent.
10950 nullable: bool,
10951 /// `true` when this column was marked `#[rustango(auto_now)]` —
10952 /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
10953 /// column instead of the user-supplied value, so `updated_at`
10954 /// always reflects the latest write without the caller having
10955 /// to remember to set it.
10956 auto_now: bool,
10957 /// `true` when this column was marked `#[rustango(auto_now_add)]`
10958 /// — the column is server-set on INSERT (DB DEFAULT) and
10959 /// **immutable** afterwards. `update_on` / `save_on` skip the
10960 /// column entirely so a stale `created_at` value in memory never
10961 /// rewrites the persisted timestamp.
10962 auto_now_add: bool,
10963 /// `true` when this column was marked `#[rustango(soft_delete)]`.
10964 /// Triggers emission of `soft_delete_on(executor)` and
10965 /// `restore_on(executor)` on the model's inherent impl. There is
10966 /// at most one such column per model — emission asserts this.
10967 soft_delete: bool,
10968 /// `Some` when this column was marked
10969 /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
10970 /// every INSERT and UPDATE path; the database recomputes the
10971 /// value from `EXPR`. Backlog item #35.
10972 generated_as: Option<String>,
10973 /// `true` when this column was marked
10974 /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
10975 /// emit an `insert_push` that auto-fills an `Auto::Unset` value
10976 /// with `Uuid::now_v7()` before binding, so the PK is generated
10977 /// Rust-side and the column is always present in the INSERT
10978 /// statement (no DB DEFAULT requirement). Issue #823.
10979 default_uuid_v7: bool,
10980 /// `Some` when this FK field carried `#[rustango(related_name =
10981 /// "...")]`. Threaded into [`FkRelation::related_name`] and
10982 /// consumed by [`reverse_helper_tokens`] to override the default
10983 /// `<child_snake>_set` accessor name. Follow-up to #816.
10984 related_name: Option<String>,
10985}
10986
10987/// Reject table names that won't survive SQL identifier
10988/// derivation downstream. Postgres' regular-identifier rule
10989/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
10990/// through the framework's unquoted FK / index / constraint name
10991/// emission without surprises. We also disallow leading-digit and
10992/// the empty string for clarity.
10993///
10994/// Reserved-word collisions (`select`, `from`, …) aren't flagged
10995/// here — those produce a runtime error from the SQL parser,
10996/// which is loud enough; statically enumerating reserved words
10997/// across the three supported dialects is more friction than help.
10998///
10999/// Backlog item #65.
11000fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11001 validate_sql_identifier(name, "table", span)
11002}
11003
11004/// Reject SQL identifiers that compile but break downstream SQL
11005/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11006/// `kind` is "table" / "column" — used for the error message so users
11007/// see which attribute caused the failure.
11008fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11009 if name.is_empty() {
11010 return Err(syn::Error::new(
11011 span,
11012 format!("`{kind} = \"\"` is not a valid SQL identifier"),
11013 ));
11014 }
11015 let mut chars = name.chars();
11016 let first = chars.next().unwrap();
11017 if !(first.is_ascii_alphabetic() || first == '_') {
11018 return Err(syn::Error::new(
11019 span,
11020 format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11021 ));
11022 }
11023 for c in chars {
11024 if !(c.is_ascii_alphanumeric() || c == '_') {
11025 return Err(syn::Error::new(
11026 span,
11027 format!(
11028 "{kind} name `{name}` contains invalid character {c:?} — \
11029 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11030 Hyphens in particular break FK / index name derivation \
11031 downstream; use underscores instead (e.g. `{}`)",
11032 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11033 ),
11034 ));
11035 }
11036 }
11037 Ok(())
11038}
11039
11040fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11041 let root = rustango_root();
11042 let attrs = parse_field_attrs(field)?;
11043 let ident = field
11044 .ident
11045 .as_ref()
11046 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11047 let name = ident.to_string();
11048 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11049 let primary_key = attrs.primary_key;
11050 let DetectedType {
11051 kind,
11052 nullable,
11053 auto: detected_auto,
11054 fk_inner,
11055 } = detect_type(&field.ty)?;
11056 check_bound_compatibility(field, &attrs, kind)?;
11057 let auto = detected_auto;
11058 // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11059 // INSERT path: the user must wrap the field in `Auto<T>`, which
11060 // marks the column as DB-default-supplied. The mixin attrs then
11061 // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11062 // for `auto_now`, force the value on UPDATE too.
11063 if attrs.auto_uuid {
11064 if kind != DetectedKind::Uuid {
11065 return Err(syn::Error::new_spanned(
11066 field,
11067 "`#[rustango(auto_uuid)]` requires the field type to be \
11068 `Auto<uuid::Uuid>`",
11069 ));
11070 }
11071 if !detected_auto {
11072 return Err(syn::Error::new_spanned(
11073 field,
11074 "`#[rustango(auto_uuid)]` requires the field type to be \
11075 wrapped in `Auto<...>` so the macro skips the column on \
11076 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11077 ));
11078 }
11079 }
11080 if attrs.default_uuid_v7 {
11081 if kind != DetectedKind::Uuid {
11082 return Err(syn::Error::new_spanned(
11083 field,
11084 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11085 `Auto<uuid::Uuid>`",
11086 ));
11087 }
11088 if !detected_auto {
11089 return Err(syn::Error::new_spanned(
11090 field,
11091 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11092 wrapped in `Auto<...>` so the macro can detect the \
11093 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11094 ));
11095 }
11096 if attrs.auto_uuid {
11097 return Err(syn::Error::new_spanned(
11098 field,
11099 "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11100 `#[rustango(auto_uuid)]` — the former generates the UUID \
11101 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11102 Pick one.",
11103 ));
11104 }
11105 }
11106 if attrs.auto_now_add || attrs.auto_now {
11107 if kind != DetectedKind::DateTime {
11108 return Err(syn::Error::new_spanned(
11109 field,
11110 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11111 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11112 ));
11113 }
11114 if !detected_auto {
11115 return Err(syn::Error::new_spanned(
11116 field,
11117 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11118 the field type to be wrapped in `Auto<...>` so the macro skips \
11119 the column on INSERT and the DB DEFAULT (`now()`) fires",
11120 ));
11121 }
11122 }
11123 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11124 return Err(syn::Error::new_spanned(
11125 field,
11126 "`#[rustango(soft_delete)]` requires the field type to be \
11127 `Option<chrono::DateTime<chrono::Utc>>`",
11128 ));
11129 }
11130 let is_mixin_auto =
11131 attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11132 if detected_auto && !primary_key && !is_mixin_auto {
11133 return Err(syn::Error::new_spanned(
11134 field,
11135 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11136 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11137 `auto_now`",
11138 ));
11139 }
11140 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11141 return Err(syn::Error::new_spanned(
11142 field,
11143 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11144 SERIAL / BIGSERIAL already supplies a default sequence.",
11145 ));
11146 }
11147 if fk_inner.is_some() && primary_key {
11148 return Err(syn::Error::new_spanned(
11149 field,
11150 "`ForeignKey<T>` is not allowed on a primary-key field — \
11151 a row's PK is its own identity, not a reference to a parent.",
11152 ));
11153 }
11154 if attrs.generated_as.is_some() {
11155 if primary_key {
11156 return Err(syn::Error::new_spanned(
11157 field,
11158 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11159 primary-key field — a PK must be writable so the row \
11160 has an identity at INSERT time.",
11161 ));
11162 }
11163 if attrs.default.is_some() {
11164 return Err(syn::Error::new_spanned(
11165 field,
11166 "`#[rustango(generated_as = \"…\")]` cannot combine with \
11167 `default = \"…\"` — Postgres rejects DEFAULT on \
11168 generated columns. The expression IS the default.",
11169 ));
11170 }
11171 if detected_auto {
11172 return Err(syn::Error::new_spanned(
11173 field,
11174 "`#[rustango(generated_as = \"…\")]` is not allowed on \
11175 an `Auto<T>` field — generated columns are computed \
11176 by the DB, not server-assigned via a sequence. Use a \
11177 plain Rust type (e.g. `f64`).",
11178 ));
11179 }
11180 if fk_inner.is_some() {
11181 return Err(syn::Error::new_spanned(
11182 field,
11183 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11184 ForeignKey field.",
11185 ));
11186 }
11187 }
11188 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11189 let column_lit = column.as_str();
11190 // pgvector (#824): the `vector(dims = N)` attribute supplies the
11191 // dimension that the bare `Vector` Rust type can't carry, so emit
11192 // `FieldType::Vector(N)` here rather than the `variant_tokens`
11193 // fallback of `Vector(0)`.
11194 let field_type_tokens = if kind == DetectedKind::Vector {
11195 let root = rustango_root();
11196 let dims = attrs.vector_dims.unwrap_or(0);
11197 quote!(#root::core::FieldType::Vector(#dims))
11198 } else if kind == DetectedKind::Geometry {
11199 // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11200 // SRID that the bare `Point` Rust type can't carry, so emit
11201 // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11202 let root = rustango_root();
11203 let srid = attrs.geometry_srid.unwrap_or(0);
11204 quote!(#root::core::FieldType::Geometry(#srid))
11205 } else {
11206 kind.variant_tokens()
11207 };
11208 let max_length = optional_u32(attrs.max_length);
11209 let min = optional_i64(attrs.min);
11210 let max = optional_i64(attrs.max);
11211 let default = optional_str(attrs.default.as_deref());
11212
11213 let unique = attrs.unique;
11214 let generated_as = optional_str(attrs.generated_as.as_deref());
11215 let help_text = optional_str(attrs.help_text.as_deref());
11216 let choices = optional_choices(attrs.choices.as_deref());
11217 let db_comment = optional_str(attrs.db_comment.as_deref());
11218 let verbose_name = optional_str(attrs.verbose_name.as_deref());
11219 let editable = attrs.editable;
11220 let blank = attrs.blank;
11221 let case_insensitive = attrs.case_insensitive;
11222 let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11223 if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() {
11224 return Err(syn::Error::new_spanned(
11225 field,
11226 "`#[rustango(on_delete = \"…\")]` requires either `fk = \"<table>\"` \
11227 or `o2o = \"<table>\"` on the same field — it has no meaning on a \
11228 non-FK column.",
11229 ));
11230 }
11231 let fk_on_delete = match attrs.on_delete.as_deref() {
11232 None => quote!(::core::option::Option::None),
11233 Some(action) => {
11234 let variant = match action {
11235 "cascade" => quote!(Cascade),
11236 "restrict" => quote!(Restrict),
11237 "set_null" => quote!(SetNull),
11238 "set_default" => quote!(SetDefault),
11239 "no_action" => quote!(NoAction),
11240 // parse_field_attrs already validated this — guard against future drift.
11241 other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11242 };
11243 quote!(::core::option::Option::Some(
11244 #root::core::OnDeleteAction::#variant
11245 ))
11246 }
11247 };
11248 let schema = quote! {
11249 #root::core::FieldSchema {
11250 name: #name,
11251 column: #column_lit,
11252 ty: #field_type_tokens,
11253 nullable: #nullable,
11254 primary_key: #primary_key,
11255 relation: #relation,
11256 max_length: #max_length,
11257 min: #min,
11258 max: #max,
11259 default: #default,
11260 auto: #auto,
11261 unique: #unique,
11262 generated_as: #generated_as,
11263 help_text: #help_text,
11264 choices: #choices,
11265 db_comment: #db_comment,
11266 verbose_name: #verbose_name,
11267 editable: #editable,
11268 blank: #blank,
11269 case_insensitive: #case_insensitive,
11270 fk_on_delete: #fk_on_delete,
11271 validators: &[ #(#validators_lits),* ],
11272 }
11273 };
11274
11275 let from_row_init = quote! {
11276 #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11277 };
11278 let from_aliased_row_init = quote! {
11279 #ident: #root::sql::sqlx::Row::try_get(
11280 row,
11281 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11282 )?
11283 };
11284
11285 Ok(FieldInfo {
11286 ident,
11287 column,
11288 primary_key,
11289 auto,
11290 value_ty: &field.ty,
11291 field_type_tokens,
11292 schema,
11293 from_row_init,
11294 from_aliased_row_init,
11295 fk_inner: fk_inner.cloned(),
11296 fk_pk_kind: kind,
11297 nullable,
11298 auto_now: attrs.auto_now,
11299 auto_now_add: attrs.auto_now_add,
11300 soft_delete: attrs.soft_delete,
11301 generated_as: attrs.generated_as.clone(),
11302 default_uuid_v7: attrs.default_uuid_v7,
11303 related_name: attrs.related_name.clone(),
11304 })
11305}
11306
11307fn check_bound_compatibility(
11308 field: &syn::Field,
11309 attrs: &FieldAttrs,
11310 kind: DetectedKind,
11311) -> syn::Result<()> {
11312 if attrs.max_length.is_some() && kind != DetectedKind::String {
11313 return Err(syn::Error::new_spanned(
11314 field,
11315 "`max_length` is only valid on `String` fields (or `Option<String>`)",
11316 ));
11317 }
11318 if attrs.choices.is_some() && kind != DetectedKind::String {
11319 return Err(syn::Error::new_spanned(
11320 field,
11321 "`choices` is only valid on `String` fields (or `Option<String>`) — \
11322 integer-valued enumerations should be modeled with a Rust enum and \
11323 custom (de)serializer for now",
11324 ));
11325 }
11326 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11327 return Err(syn::Error::new_spanned(
11328 field,
11329 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11330 ));
11331 }
11332 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11333 if min > max {
11334 return Err(syn::Error::new_spanned(
11335 field,
11336 format!("`min` ({min}) is greater than `max` ({max})"),
11337 ));
11338 }
11339 }
11340 Ok(())
11341}
11342
11343fn optional_u32(value: Option<u32>) -> TokenStream2 {
11344 if let Some(v) = value {
11345 quote!(::core::option::Option::Some(#v))
11346 } else {
11347 quote!(::core::option::Option::None)
11348 }
11349}
11350
11351fn optional_i64(value: Option<i64>) -> TokenStream2 {
11352 if let Some(v) = value {
11353 quote!(::core::option::Option::Some(#v))
11354 } else {
11355 quote!(::core::option::Option::None)
11356 }
11357}
11358
11359fn optional_str(value: Option<&str>) -> TokenStream2 {
11360 if let Some(v) = value {
11361 quote!(::core::option::Option::Some(#v))
11362 } else {
11363 quote!(::core::option::Option::None)
11364 }
11365}
11366
11367fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11368 let Some(pairs) = pairs else {
11369 return quote!(::core::option::Option::None);
11370 };
11371 let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11372 quote!(::core::option::Option::Some(&[#(#entries),*]))
11373}
11374
11375fn relation_tokens(
11376 field: &syn::Field,
11377 attrs: &FieldAttrs,
11378 fk_inner: Option<&syn::Type>,
11379 table: &str,
11380) -> syn::Result<TokenStream2> {
11381 let root = rustango_root();
11382 if let Some(inner) = fk_inner {
11383 if attrs.fk.is_some() || attrs.o2o.is_some() {
11384 return Err(syn::Error::new_spanned(
11385 field,
11386 "`ForeignKey<T>` already declares the FK target via the type parameter — \
11387 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11388 ));
11389 }
11390 let on = attrs.on.as_deref().unwrap_or("id");
11391 return Ok(quote! {
11392 ::core::option::Option::Some(#root::core::Relation::Fk {
11393 to: <#inner as #root::core::Model>::SCHEMA.table,
11394 on: #on,
11395 })
11396 });
11397 }
11398 match (&attrs.fk, &attrs.o2o) {
11399 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11400 field,
11401 "`fk` and `o2o` are mutually exclusive",
11402 )),
11403 (Some(to), None) => {
11404 let on = attrs.on.as_deref().unwrap_or("id");
11405 // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11406 // the model's own table. Threaded as a literal string at
11407 // macro-expansion time to sidestep the const-eval cycle
11408 // that `Self::SCHEMA.table` would create when referenced
11409 // inside Self::SCHEMA's own initializer.
11410 let resolved = if to == "self" { table } else { to };
11411 Ok(quote! {
11412 ::core::option::Option::Some(#root::core::Relation::Fk { to: #resolved, on: #on })
11413 })
11414 }
11415 (None, Some(to)) => {
11416 let on = attrs.on.as_deref().unwrap_or("id");
11417 let resolved = if to == "self" { table } else { to };
11418 Ok(quote! {
11419 ::core::option::Option::Some(#root::core::Relation::O2O { to: #resolved, on: #on })
11420 })
11421 }
11422 (None, None) => {
11423 if attrs.on.is_some() {
11424 return Err(syn::Error::new_spanned(
11425 field,
11426 "`on` requires `fk` or `o2o`",
11427 ));
11428 }
11429 Ok(quote!(::core::option::Option::None))
11430 }
11431 }
11432}
11433
11434/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11435/// about kinds without depending on `rustango-core` (which would require a
11436/// proc-macro/normal split it doesn't have today).
11437#[derive(Clone, Copy, PartialEq, Eq)]
11438enum DetectedKind {
11439 I16,
11440 I32,
11441 I64,
11442 F32,
11443 F64,
11444 Bool,
11445 String,
11446 DateTime,
11447 Date,
11448 Time,
11449 Uuid,
11450 Json,
11451 Decimal,
11452 Binary,
11453 /// `Array<String>` → PG `text[]` (#341).
11454 ArrayText,
11455 /// `Array<i32>` → PG `integer[]` (#341).
11456 ArrayInt,
11457 /// `Array<i64>` → PG `bigint[]` (#341).
11458 ArrayBigInt,
11459 /// `Range<i32>` → PG `int4range` (#343).
11460 RangeInt,
11461 /// `Range<i64>` → PG `int8range` (#343).
11462 RangeBigInt,
11463 /// `Range<Decimal>` → PG `numrange` (#343).
11464 RangeNumeric,
11465 /// `Range<NaiveDate>` → PG `daterange` (#343).
11466 RangeDate,
11467 /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11468 RangeDateTime,
11469 /// `HStore` → PG `hstore` (#342).
11470 HStore,
11471 /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11472 /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11473 /// in at the `FieldType` emission site (not carried on this enum).
11474 Vector,
11475 /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11476 /// from the `#[rustango(geometry(srid = N))]` field attribute,
11477 /// threaded in at the `FieldType` emission site.
11478 Geometry,
11479}
11480
11481impl DetectedKind {
11482 fn variant_tokens(self) -> TokenStream2 {
11483 let root = rustango_root();
11484 match self {
11485 Self::I16 => quote!(#root::core::FieldType::I16),
11486 Self::I32 => quote!(#root::core::FieldType::I32),
11487 Self::I64 => quote!(#root::core::FieldType::I64),
11488 Self::F32 => quote!(#root::core::FieldType::F32),
11489 Self::F64 => quote!(#root::core::FieldType::F64),
11490 Self::Bool => quote!(#root::core::FieldType::Bool),
11491 Self::String => quote!(#root::core::FieldType::String),
11492 Self::DateTime => quote!(#root::core::FieldType::DateTime),
11493 Self::Date => quote!(#root::core::FieldType::Date),
11494 Self::Time => quote!(#root::core::FieldType::Time),
11495 Self::Uuid => quote!(#root::core::FieldType::Uuid),
11496 Self::Json => quote!(#root::core::FieldType::Json),
11497 Self::Decimal => quote!(#root::core::FieldType::Decimal),
11498 Self::Binary => quote!(#root::core::FieldType::Binary),
11499 Self::ArrayText => {
11500 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11501 }
11502 Self::ArrayInt => {
11503 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11504 }
11505 Self::ArrayBigInt => {
11506 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11507 }
11508 Self::RangeInt => {
11509 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11510 }
11511 Self::RangeBigInt => {
11512 quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11513 }
11514 Self::RangeNumeric => {
11515 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11516 }
11517 Self::RangeDate => {
11518 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11519 }
11520 Self::RangeDateTime => {
11521 quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11522 }
11523 Self::HStore => quote!(#root::core::FieldType::HStore),
11524 // Dimension comes from the `vector(dims = N)` attribute,
11525 // applied at the emission site; `0` here is just a fallback.
11526 Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11527 // SRID comes from the `geometry(srid = N)` attribute, applied
11528 // at the emission site; `0` here is just a fallback.
11529 Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11530 }
11531 }
11532
11533 fn is_integer(self) -> bool {
11534 matches!(self, Self::I16 | Self::I32 | Self::I64)
11535 }
11536
11537 /// `(SqlValue::<Variant>, default expr)` for emitting the
11538 /// `match SqlValue { … }` arm in `LoadRelated::__rustango_load_related`
11539 /// for a `ForeignKey<T, K>` FK whose K maps to `self`. The default
11540 /// fires only when the parent's `__rustango_pk_value` returns a
11541 /// different variant than expected, which is a compile-time bug —
11542 /// but we still need a value-typed fallback to keep the match
11543 /// total.
11544 fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
11545 let root = rustango_root();
11546 match self {
11547 Self::I16 => (quote!(I16), quote!(0i16)),
11548 Self::I32 => (quote!(I32), quote!(0i32)),
11549 Self::I64 => (quote!(I64), quote!(0i64)),
11550 Self::F32 => (quote!(F32), quote!(0f32)),
11551 Self::F64 => (quote!(F64), quote!(0f64)),
11552 Self::Bool => (quote!(Bool), quote!(false)),
11553 Self::String => (quote!(String), quote!(::std::string::String::new())),
11554 Self::DateTime => (
11555 quote!(DateTime),
11556 quote!(<#root::__chrono::DateTime<#root::__chrono::Utc> as ::std::default::Default>::default()),
11557 ),
11558 Self::Date => (
11559 quote!(Date),
11560 quote!(<#root::__chrono::NaiveDate as ::std::default::Default>::default()),
11561 ),
11562 Self::Time => (
11563 quote!(Time),
11564 quote!(<#root::__chrono::NaiveTime as ::std::default::Default>::default()),
11565 ),
11566 Self::Uuid => (quote!(Uuid), quote!(#root::__uuid::Uuid::nil())),
11567 Self::Json => (quote!(Json), quote!(#root::__serde_json::Value::Null)),
11568 Self::Decimal => (
11569 quote!(Decimal),
11570 quote!(<#root::__rust_decimal::Decimal as ::std::default::Default>::default()),
11571 ),
11572 Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
11573 // Arrays (#341) can never be a foreign-key primary key, so
11574 // this arm is never reached at runtime — but the match must
11575 // stay total. A bare empty `SqlValue::Array` is the dummy.
11576 Self::ArrayText | Self::ArrayInt | Self::ArrayBigInt => {
11577 (quote!(Array), quote!(::std::vec::Vec::new()))
11578 }
11579 // Ranges (#343) likewise can't be a FK PK — never reached.
11580 Self::RangeInt
11581 | Self::RangeBigInt
11582 | Self::RangeNumeric
11583 | Self::RangeDate
11584 | Self::RangeDateTime => (quote!(RangeLiteral), quote!(::std::string::String::new())),
11585 // HStore (#342) can't be a FK PK — never reached.
11586 Self::HStore => (quote!(HStore), quote!(::std::vec::Vec::new())),
11587 // Vector (#824) can't be a FK PK — never reached.
11588 Self::Vector => (quote!(Vector), quote!(::std::vec::Vec::new())),
11589 // Geometry (#443) can't be a FK PK — never reached (arm is
11590 // exhaustiveness-only; never interpolated into emitted code).
11591 Self::Geometry => (quote!(Geometry), quote!(::std::vec::Vec::new())),
11592 }
11593 }
11594}
11595
11596/// Result of walking a field's Rust type. `kind` is the underlying
11597/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11598/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11599/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11600/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11601#[derive(Clone, Copy)]
11602struct DetectedType<'a> {
11603 kind: DetectedKind,
11604 nullable: bool,
11605 auto: bool,
11606 fk_inner: Option<&'a syn::Type>,
11607}
11608
11609/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11610/// non-`Auto` types — the caller should already have routed Auto-only
11611/// codegen through this helper, so a `None` indicates a macro-internal
11612/// invariant break.
11613fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11614 let Type::Path(TypePath { path, qself: None }) = ty else {
11615 return None;
11616 };
11617 let last = path.segments.last()?;
11618 if last.ident != "Auto" {
11619 return None;
11620 }
11621 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11622 return None;
11623 };
11624 args.args.iter().find_map(|a| match a {
11625 syn::GenericArgument::Type(t) => Some(t),
11626 _ => None,
11627 })
11628}
11629
11630fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11631 let Type::Path(TypePath { path, qself: None }) = ty else {
11632 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11633 };
11634 let last = path
11635 .segments
11636 .last()
11637 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11638
11639 if last.ident == "Option" {
11640 let inner = generic_inner(ty, &last.arguments, "Option")?;
11641 let inner_det = detect_type(inner)?;
11642 if inner_det.nullable {
11643 return Err(syn::Error::new_spanned(
11644 ty,
11645 "nested Option is not supported",
11646 ));
11647 }
11648 if inner_det.auto {
11649 return Err(syn::Error::new_spanned(
11650 ty,
11651 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11652 ));
11653 }
11654 return Ok(DetectedType {
11655 nullable: true,
11656 ..inner_det
11657 });
11658 }
11659
11660 if last.ident == "Auto" {
11661 let inner = generic_inner(ty, &last.arguments, "Auto")?;
11662 let inner_det = detect_type(inner)?;
11663 if inner_det.auto {
11664 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11665 }
11666 if inner_det.nullable {
11667 return Err(syn::Error::new_spanned(
11668 ty,
11669 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11670 ));
11671 }
11672 if inner_det.fk_inner.is_some() {
11673 return Err(syn::Error::new_spanned(
11674 ty,
11675 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11676 ));
11677 }
11678 if !matches!(
11679 inner_det.kind,
11680 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11681 ) {
11682 return Err(syn::Error::new_spanned(
11683 ty,
11684 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11685 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11686 (DEFAULT now())",
11687 ));
11688 }
11689 return Ok(DetectedType {
11690 auto: true,
11691 ..inner_det
11692 });
11693 }
11694
11695 if last.ident == "ForeignKey" {
11696 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11697 // Resolve the FK column's underlying SQL type from `K`. When the
11698 // user wrote `ForeignKey<T>` without a key parameter, the type
11699 // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11700 // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11701 // recurse into K so the column DDL emits the right SQL type
11702 // (VARCHAR for String, UUID for Uuid, …) and the load_related
11703 // emitter knows which `SqlValue` variant to match.
11704 let kind = match key_ty {
11705 Some(k) => detect_type(k)?.kind,
11706 None => DetectedKind::I64,
11707 };
11708 return Ok(DetectedType {
11709 kind,
11710 nullable: false,
11711 auto: false,
11712 fk_inner: Some(inner),
11713 });
11714 }
11715
11716 let kind = match last.ident.to_string().as_str() {
11717 "i16" => DetectedKind::I16,
11718 "i32" => DetectedKind::I32,
11719 "i64" => DetectedKind::I64,
11720 "f32" => DetectedKind::F32,
11721 "f64" => DetectedKind::F64,
11722 "bool" => DetectedKind::Bool,
11723 "String" => DetectedKind::String,
11724 "DateTime" => DetectedKind::DateTime,
11725 "NaiveDate" => DetectedKind::Date,
11726 "NaiveTime" => DetectedKind::Time,
11727 "Uuid" => DetectedKind::Uuid,
11728 "Value" => DetectedKind::Json,
11729 "Decimal" => DetectedKind::Decimal,
11730 // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11731 // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11732 // — that would emit Binary DDL and decode-fail at runtime.
11733 "Vec" => {
11734 let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11735 if let Type::Path(TypePath { path, qself: None }) = inner {
11736 if let Some(seg) = path.segments.last() {
11737 if seg.ident == "u8" && seg.arguments.is_empty() {
11738 return Ok(DetectedType {
11739 kind: DetectedKind::Binary,
11740 nullable: false,
11741 auto: false,
11742 fk_inner: None,
11743 });
11744 }
11745 }
11746 }
11747 return Err(syn::Error::new_spanned(
11748 ty,
11749 "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11750 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11751 ));
11752 }
11753 // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11754 // `integer[]` / `bigint[]` (Django `ArrayField`, #341).
11755 "Array" => {
11756 let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11757 let elem = match inner {
11758 Type::Path(TypePath { path, qself: None }) => {
11759 path.segments.last().map(|s| s.ident.to_string())
11760 }
11761 _ => None,
11762 };
11763 let kind = match elem.as_deref() {
11764 Some("String") => DetectedKind::ArrayText,
11765 Some("i32") => DetectedKind::ArrayInt,
11766 Some("i64") => DetectedKind::ArrayBigInt,
11767 _ => {
11768 return Err(syn::Error::new_spanned(
11769 ty,
11770 "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
11771 `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
11772 ));
11773 }
11774 };
11775 return Ok(DetectedType {
11776 kind,
11777 nullable: false,
11778 auto: false,
11779 fk_inner: None,
11780 });
11781 }
11782 // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
11783 // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
11784 // `int8range` / `numrange` / `daterange` / `tstzrange` (Django
11785 // `RangeField` family, #343).
11786 "Range" => {
11787 let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
11788 let elem = match inner {
11789 Type::Path(TypePath { path, qself: None }) => {
11790 path.segments.last().map(|s| s.ident.to_string())
11791 }
11792 _ => None,
11793 };
11794 let kind = match elem.as_deref() {
11795 Some("i32") => DetectedKind::RangeInt,
11796 Some("i64") => DetectedKind::RangeBigInt,
11797 Some("Decimal") => DetectedKind::RangeNumeric,
11798 Some("NaiveDate") => DetectedKind::RangeDate,
11799 Some("DateTime") => DetectedKind::RangeDateTime,
11800 _ => {
11801 return Err(syn::Error::new_spanned(
11802 ty,
11803 "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
11804 `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
11805 `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
11806 (→ tstzrange) are supported (#343)",
11807 ));
11808 }
11809 };
11810 return Ok(DetectedType {
11811 kind,
11812 nullable: false,
11813 auto: false,
11814 fk_inner: None,
11815 });
11816 }
11817 // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
11818 // (the `CastValue` impl bridges logical↔stored); the field's own
11819 // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
11820 // schema just needs `FieldType::String`.
11821 "Cast" => {
11822 return Ok(DetectedType {
11823 kind: DetectedKind::String,
11824 nullable: false,
11825 auto: false,
11826 fk_inner: None,
11827 });
11828 }
11829 // `HStore` → PG `hstore` (Django `HStoreField`, #342). No generic
11830 // parameter — always a string→string map.
11831 "HStore" => {
11832 return Ok(DetectedType {
11833 kind: DetectedKind::HStore,
11834 nullable: false,
11835 auto: false,
11836 fk_inner: None,
11837 });
11838 }
11839 // `Vector` → pgvector `vector(N)` (#824). The dimension is
11840 // supplied by `#[rustango(vector(dims = N))]`, not the type.
11841 "Vector" => {
11842 return Ok(DetectedType {
11843 kind: DetectedKind::Vector,
11844 nullable: false,
11845 auto: false,
11846 fk_inner: None,
11847 });
11848 }
11849 // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
11850 // supplied by `#[rustango(geometry(srid = N))]`, not the type.
11851 "Point" => {
11852 return Ok(DetectedType {
11853 kind: DetectedKind::Geometry,
11854 nullable: false,
11855 auto: false,
11856 fk_inner: None,
11857 });
11858 }
11859 other => {
11860 return Err(syn::Error::new_spanned(
11861 ty,
11862 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)"),
11863 ));
11864 }
11865 };
11866 Ok(DetectedType {
11867 kind,
11868 nullable: false,
11869 auto: false,
11870 fk_inner: None,
11871 })
11872}
11873
11874fn generic_inner<'a>(
11875 ty: &'a Type,
11876 arguments: &'a PathArguments,
11877 wrapper: &str,
11878) -> syn::Result<&'a Type> {
11879 let PathArguments::AngleBracketed(args) = arguments else {
11880 return Err(syn::Error::new_spanned(
11881 ty,
11882 format!("{wrapper} requires a generic argument"),
11883 ));
11884 };
11885 args.args
11886 .iter()
11887 .find_map(|a| match a {
11888 GenericArgument::Type(t) => Some(t),
11889 _ => None,
11890 })
11891 .ok_or_else(|| {
11892 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11893 })
11894}
11895
11896/// Like [`generic_inner`] but pulls *two* type args — the first is
11897/// required, the second is optional. Used by the `ForeignKey<T, K>`
11898/// detection where K defaults to `i64` when omitted.
11899fn generic_pair<'a>(
11900 ty: &'a Type,
11901 arguments: &'a PathArguments,
11902 wrapper: &str,
11903) -> syn::Result<(&'a Type, Option<&'a Type>)> {
11904 let PathArguments::AngleBracketed(args) = arguments else {
11905 return Err(syn::Error::new_spanned(
11906 ty,
11907 format!("{wrapper} requires a generic argument"),
11908 ));
11909 };
11910 let mut types = args.args.iter().filter_map(|a| match a {
11911 GenericArgument::Type(t) => Some(t),
11912 _ => None,
11913 });
11914 let first = types.next().ok_or_else(|| {
11915 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11916 })?;
11917 let second = types.next();
11918 Ok((first, second))
11919}
11920
11921fn to_snake_case(s: &str) -> String {
11922 let mut out = String::with_capacity(s.len() + 4);
11923 for (i, ch) in s.chars().enumerate() {
11924 if ch.is_ascii_uppercase() {
11925 if i > 0 {
11926 out.push('_');
11927 }
11928 out.push(ch.to_ascii_lowercase());
11929 } else {
11930 out.push(ch);
11931 }
11932 }
11933 out
11934}
11935
11936// ============================================================
11937// #[derive(Form)] — slice 8.4B
11938// ============================================================
11939
11940/// Per-field `#[form(...)]` attributes recognised by the derive.
11941#[derive(Default)]
11942struct FormFieldAttrs {
11943 min: Option<i64>,
11944 max: Option<i64>,
11945 min_length: Option<u32>,
11946 max_length: Option<u32>,
11947 /// `#[form(clean = "fn_name")]` — Django-shape `clean_<field>` hook.
11948 /// The named static method on the form struct is called after the
11949 /// field's typed parse + length/range checks; it gets the parsed
11950 /// value by reference and returns `Result<<FieldType>, String>`.
11951 /// On Ok, the returned value replaces the parsed one; on Err, the
11952 /// message is attached to the field error list. Issue #372.
11953 clean: Option<syn::Ident>,
11954}
11955
11956/// Container-level `#[form(...)]` attributes. Currently only the
11957/// Django-shape cross-field `validate` hook (issue #373).
11958#[derive(Default)]
11959struct FormContainerAttrs {
11960 /// `#[form(validate = "fn_name")]` — Django-shape `clean()` hook.
11961 /// After every per-field parse succeeds, the named method on the
11962 /// form struct is called with `&self` and may return
11963 /// `Result<(), FormErrors>`. Errors merge into the field error
11964 /// list. Issue #373.
11965 validate: Option<syn::Ident>,
11966}
11967
11968/// Detected shape of a form field's Rust type.
11969#[derive(Clone, Copy)]
11970enum FormFieldKind {
11971 String,
11972 I16,
11973 I32,
11974 I64,
11975 F32,
11976 F64,
11977 Bool,
11978}
11979
11980impl FormFieldKind {
11981 fn parse_method(self) -> &'static str {
11982 match self {
11983 Self::I16 => "i16",
11984 Self::I32 => "i32",
11985 Self::I64 => "i64",
11986 Self::F32 => "f32",
11987 Self::F64 => "f64",
11988 // String + Bool don't go through `str::parse`; the codegen
11989 // handles them inline.
11990 Self::String | Self::Bool => "",
11991 }
11992 }
11993}
11994
11995fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
11996 let root = rustango_root();
11997 let struct_name = &input.ident;
11998
11999 let Data::Struct(data) = &input.data else {
12000 return Err(syn::Error::new_spanned(
12001 struct_name,
12002 "Form can only be derived on structs",
12003 ));
12004 };
12005 let Fields::Named(named) = &data.fields else {
12006 return Err(syn::Error::new_spanned(
12007 struct_name,
12008 "Form requires a struct with named fields",
12009 ));
12010 };
12011
12012 // #373 — container-level `#[form(validate = "fn")]` hook.
12013 let container = parse_form_container_attrs(input)?;
12014 let post_field_clean: Vec<TokenStream2> = Vec::new();
12015 let _ = post_field_clean;
12016
12017 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12018 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12019
12020 for field in &named.named {
12021 let ident = field
12022 .ident
12023 .as_ref()
12024 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12025 let attrs = parse_form_field_attrs(field)?;
12026 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12027
12028 let name_lit = ident.to_string();
12029 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12030 // #372 — append the per-field `clean_<field>` call right after
12031 // the parse block when the attribute is set. The clean fn
12032 // takes &T and returns Result<T, String>; on Err we attach
12033 // the message to the field error list without aborting
12034 // (matches Django's "collect all field errors" shape).
12035 let clean_block = if let Some(clean_fn) = &attrs.clean {
12036 quote! {
12037 if __errors.fields().get(#name_lit).is_none() {
12038 match Self::#clean_fn(&#ident) {
12039 ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
12040 ::core::result::Result::Err(__msg) => {
12041 __errors.add(#name_lit, __msg);
12042 }
12043 }
12044 }
12045 }
12046 } else {
12047 quote! {}
12048 };
12049 field_blocks.push(quote! {
12050 #parse_block
12051 #clean_block
12052 });
12053 field_idents.push(ident);
12054 }
12055
12056 // #373 — after every per-field parse + clean succeeds, call the
12057 // cross-field validator if declared. Errors merge into the
12058 // outgoing FormErrors via the existing `FormErrors::merge` helper
12059 // (same primitive the DRF serializer cross-field hook uses).
12060 let cross_field_call = if let Some(validate_fn) = &container.validate {
12061 quote! {
12062 if __errors.is_empty() {
12063 let __candidate = Self { #( #field_idents ),* };
12064 if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12065 __errors.merge(__other);
12066 }
12067 if !__errors.is_empty() {
12068 return ::core::result::Result::Err(__errors);
12069 }
12070 return ::core::result::Result::Ok(__candidate);
12071 }
12072 }
12073 } else {
12074 quote! {}
12075 };
12076
12077 Ok(quote! {
12078 impl #root::forms::Form for #struct_name {
12079 fn parse(
12080 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12081 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12082 let mut __errors = #root::forms::FormErrors::default();
12083 #( #field_blocks )*
12084 #cross_field_call
12085 if !__errors.is_empty() {
12086 return ::core::result::Result::Err(__errors);
12087 }
12088 ::core::result::Result::Ok(Self {
12089 #( #field_idents ),*
12090 })
12091 }
12092 }
12093 })
12094}
12095
12096fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12097 let mut out = FormContainerAttrs::default();
12098 for attr in &input.attrs {
12099 if !attr.path().is_ident("form") {
12100 continue;
12101 }
12102 attr.parse_nested_meta(|meta| {
12103 if meta.path.is_ident("validate") {
12104 let s: LitStr = meta.value()?.parse()?;
12105 out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12106 return Ok(());
12107 }
12108 Err(meta.error("unknown form container attribute (supported: `validate`)"))
12109 })?;
12110 }
12111 Ok(out)
12112}
12113
12114fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12115 let mut out = FormFieldAttrs::default();
12116 for attr in &field.attrs {
12117 if !attr.path().is_ident("form") {
12118 continue;
12119 }
12120 attr.parse_nested_meta(|meta| {
12121 if meta.path.is_ident("min") {
12122 let lit: syn::LitInt = meta.value()?.parse()?;
12123 out.min = Some(lit.base10_parse::<i64>()?);
12124 return Ok(());
12125 }
12126 if meta.path.is_ident("max") {
12127 let lit: syn::LitInt = meta.value()?.parse()?;
12128 out.max = Some(lit.base10_parse::<i64>()?);
12129 return Ok(());
12130 }
12131 if meta.path.is_ident("min_length") {
12132 let lit: syn::LitInt = meta.value()?.parse()?;
12133 out.min_length = Some(lit.base10_parse::<u32>()?);
12134 return Ok(());
12135 }
12136 if meta.path.is_ident("max_length") {
12137 let lit: syn::LitInt = meta.value()?.parse()?;
12138 out.max_length = Some(lit.base10_parse::<u32>()?);
12139 return Ok(());
12140 }
12141 if meta.path.is_ident("clean") {
12142 let s: LitStr = meta.value()?.parse()?;
12143 out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12144 return Ok(());
12145 }
12146 Err(meta.error(
12147 "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12148 ))
12149 })?;
12150 }
12151 Ok(out)
12152}
12153
12154fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12155 let Type::Path(TypePath { path, qself: None }) = ty else {
12156 return Err(syn::Error::new(
12157 span,
12158 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12159 ));
12160 };
12161 let last = path
12162 .segments
12163 .last()
12164 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12165
12166 if last.ident == "Option" {
12167 let inner = generic_inner(ty, &last.arguments, "Option")?;
12168 let (kind, nested) = detect_form_field(inner, span)?;
12169 if nested {
12170 return Err(syn::Error::new(
12171 span,
12172 "nested Option in Form fields is not supported",
12173 ));
12174 }
12175 return Ok((kind, true));
12176 }
12177
12178 let kind = match last.ident.to_string().as_str() {
12179 "String" => FormFieldKind::String,
12180 "i16" => FormFieldKind::I16,
12181 "i32" => FormFieldKind::I32,
12182 "i64" => FormFieldKind::I64,
12183 "f32" => FormFieldKind::F32,
12184 "f64" => FormFieldKind::F64,
12185 "bool" => FormFieldKind::Bool,
12186 other => {
12187 return Err(syn::Error::new(
12188 span,
12189 format!(
12190 "Form field type `{other}` is not supported in v0.8 — use String / \
12191 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12192 ),
12193 ));
12194 }
12195 };
12196 Ok((kind, false))
12197}
12198
12199#[allow(clippy::too_many_lines)]
12200fn render_form_field_parse(
12201 ident: &syn::Ident,
12202 name_lit: &str,
12203 kind: FormFieldKind,
12204 nullable: bool,
12205 attrs: &FormFieldAttrs,
12206) -> TokenStream2 {
12207 // Pull the raw &str from the payload. Uses variable name `data` to
12208 // match the new `Form::parse(data: &HashMap<…>)` signature.
12209 let lookup = quote! {
12210 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12211 };
12212
12213 let parsed_value = match kind {
12214 FormFieldKind::Bool => quote! {
12215 let __v: bool = match __raw {
12216 ::core::option::Option::None => false,
12217 ::core::option::Option::Some(__s) => !matches!(
12218 __s.to_ascii_lowercase().as_str(),
12219 "" | "false" | "0" | "off" | "no"
12220 ),
12221 };
12222 },
12223 FormFieldKind::String => {
12224 if nullable {
12225 quote! {
12226 let __v: ::core::option::Option<::std::string::String> = match __raw {
12227 ::core::option::Option::None => ::core::option::Option::None,
12228 ::core::option::Option::Some(__s) if __s.is_empty() => {
12229 ::core::option::Option::None
12230 }
12231 ::core::option::Option::Some(__s) => {
12232 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12233 }
12234 };
12235 }
12236 } else {
12237 quote! {
12238 let __v: ::std::string::String = match __raw {
12239 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12240 ::core::clone::Clone::clone(__s)
12241 }
12242 _ => {
12243 __errors.add(#name_lit, "This field is required.");
12244 ::std::string::String::new()
12245 }
12246 };
12247 }
12248 }
12249 }
12250 FormFieldKind::I16
12251 | FormFieldKind::I32
12252 | FormFieldKind::I64
12253 | FormFieldKind::F32
12254 | FormFieldKind::F64 => {
12255 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12256 let ty_lit = kind.parse_method();
12257 let default_val = match kind {
12258 FormFieldKind::I16 => quote! { 0i16 },
12259 FormFieldKind::I32 => quote! { 0i32 },
12260 FormFieldKind::I64 => quote! { 0i64 },
12261 FormFieldKind::F32 => quote! { 0f32 },
12262 FormFieldKind::F64 => quote! { 0f64 },
12263 _ => quote! { Default::default() },
12264 };
12265 if nullable {
12266 quote! {
12267 let __v: ::core::option::Option<#parse_ty> = match __raw {
12268 ::core::option::Option::None => ::core::option::Option::None,
12269 ::core::option::Option::Some(__s) if __s.is_empty() => {
12270 ::core::option::Option::None
12271 }
12272 ::core::option::Option::Some(__s) => {
12273 match __s.parse::<#parse_ty>() {
12274 ::core::result::Result::Ok(__n) => {
12275 ::core::option::Option::Some(__n)
12276 }
12277 ::core::result::Result::Err(__e) => {
12278 __errors.add(
12279 #name_lit,
12280 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12281 );
12282 ::core::option::Option::None
12283 }
12284 }
12285 }
12286 };
12287 }
12288 } else {
12289 quote! {
12290 let __v: #parse_ty = match __raw {
12291 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12292 match __s.parse::<#parse_ty>() {
12293 ::core::result::Result::Ok(__n) => __n,
12294 ::core::result::Result::Err(__e) => {
12295 __errors.add(
12296 #name_lit,
12297 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12298 );
12299 #default_val
12300 }
12301 }
12302 }
12303 _ => {
12304 __errors.add(#name_lit, "This field is required.");
12305 #default_val
12306 }
12307 };
12308 }
12309 }
12310 }
12311 };
12312
12313 let validators = render_form_validators(name_lit, kind, nullable, attrs);
12314
12315 quote! {
12316 // `mut` so the per-field `clean` hook (#372) can rewrite the
12317 // parsed value in-place when it returns Ok with a normalized
12318 // form (e.g. trim / lowercase).
12319 let mut #ident = {
12320 #lookup
12321 #parsed_value
12322 #validators
12323 __v
12324 };
12325 }
12326}
12327
12328fn render_form_validators(
12329 name_lit: &str,
12330 kind: FormFieldKind,
12331 nullable: bool,
12332 attrs: &FormFieldAttrs,
12333) -> TokenStream2 {
12334 let mut checks: Vec<TokenStream2> = Vec::new();
12335
12336 let val_ref = if nullable {
12337 quote! { __v.as_ref() }
12338 } else {
12339 quote! { ::core::option::Option::Some(&__v) }
12340 };
12341
12342 let is_string = matches!(kind, FormFieldKind::String);
12343 let is_numeric = matches!(
12344 kind,
12345 FormFieldKind::I16
12346 | FormFieldKind::I32
12347 | FormFieldKind::I64
12348 | FormFieldKind::F32
12349 | FormFieldKind::F64
12350 );
12351
12352 if is_string {
12353 if let Some(min_len) = attrs.min_length {
12354 let min_len_usize = min_len as usize;
12355 checks.push(quote! {
12356 if let ::core::option::Option::Some(__s) = #val_ref {
12357 if __s.len() < #min_len_usize {
12358 __errors.add(
12359 #name_lit,
12360 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12361 );
12362 }
12363 }
12364 });
12365 }
12366 if let Some(max_len) = attrs.max_length {
12367 let max_len_usize = max_len as usize;
12368 checks.push(quote! {
12369 if let ::core::option::Option::Some(__s) = #val_ref {
12370 if __s.len() > #max_len_usize {
12371 __errors.add(
12372 #name_lit,
12373 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12374 );
12375 }
12376 }
12377 });
12378 }
12379 }
12380
12381 if is_numeric {
12382 if let Some(min) = attrs.min {
12383 checks.push(quote! {
12384 if let ::core::option::Option::Some(__n) = #val_ref {
12385 if (*__n as f64) < (#min as f64) {
12386 __errors.add(
12387 #name_lit,
12388 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12389 );
12390 }
12391 }
12392 });
12393 }
12394 if let Some(max) = attrs.max {
12395 checks.push(quote! {
12396 if let ::core::option::Option::Some(__n) = #val_ref {
12397 if (*__n as f64) > (#max as f64) {
12398 __errors.add(
12399 #name_lit,
12400 ::std::format!("Ensure this value is less than or equal to {}.", #max),
12401 );
12402 }
12403 }
12404 });
12405 }
12406 }
12407
12408 quote! { #( #checks )* }
12409}
12410
12411// ============================================================
12412// #[derive(ViewSet)]
12413// ============================================================
12414
12415struct ViewSetAttrs {
12416 model: syn::Path,
12417 fields: Option<Vec<String>>,
12418 filter_fields: Vec<String>,
12419 search_fields: Vec<String>,
12420 /// (field_name, desc)
12421 ordering: Vec<(String, bool)>,
12422 page_size: Option<usize>,
12423 read_only: bool,
12424 perms: ViewSetPermsAttrs,
12425 /// `#[viewset(serializer = SomeSerializer)]` — render list /
12426 /// retrieve / create responses through this `#[derive(Serializer)]`
12427 /// type instead of the default field-level projection (requires the
12428 /// `serializer` feature). Tri-dialect.
12429 serializer: Option<syn::Path>,
12430}
12431
12432#[derive(Default)]
12433struct ViewSetPermsAttrs {
12434 list: Vec<String>,
12435 retrieve: Vec<String>,
12436 create: Vec<String>,
12437 update: Vec<String>,
12438 destroy: Vec<String>,
12439}
12440
12441fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12442 let root = rustango_root();
12443 let struct_name = &input.ident;
12444
12445 // Must be a unit struct or an empty named struct.
12446 match &input.data {
12447 Data::Struct(s) => match &s.fields {
12448 Fields::Unit | Fields::Named(_) => {}
12449 Fields::Unnamed(_) => {
12450 return Err(syn::Error::new_spanned(
12451 struct_name,
12452 "ViewSet can only be derived on a unit struct or an empty named struct",
12453 ));
12454 }
12455 },
12456 _ => {
12457 return Err(syn::Error::new_spanned(
12458 struct_name,
12459 "ViewSet can only be derived on a struct",
12460 ));
12461 }
12462 }
12463
12464 let attrs = parse_viewset_attrs(input)?;
12465 let model_path = &attrs.model;
12466
12467 // `.fields(&[...])` call — None means skip (use all scalar fields).
12468 let fields_call = if let Some(ref fields) = attrs.fields {
12469 let lits = fields.iter().map(|f| f.as_str());
12470 quote!(.fields(&[ #(#lits),* ]))
12471 } else {
12472 quote!()
12473 };
12474
12475 let filter_fields_call = if attrs.filter_fields.is_empty() {
12476 quote!()
12477 } else {
12478 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12479 quote!(.filter_fields(&[ #(#lits),* ]))
12480 };
12481
12482 let search_fields_call = if attrs.search_fields.is_empty() {
12483 quote!()
12484 } else {
12485 let lits = attrs.search_fields.iter().map(|f| f.as_str());
12486 quote!(.search_fields(&[ #(#lits),* ]))
12487 };
12488
12489 let ordering_call = if attrs.ordering.is_empty() {
12490 quote!()
12491 } else {
12492 let pairs = attrs.ordering.iter().map(|(f, desc)| {
12493 let f = f.as_str();
12494 quote!((#f, #desc))
12495 });
12496 quote!(.ordering(&[ #(#pairs),* ]))
12497 };
12498
12499 let page_size_call = if let Some(n) = attrs.page_size {
12500 quote!(.page_size(#n))
12501 } else {
12502 quote!()
12503 };
12504
12505 let read_only_call = if attrs.read_only {
12506 quote!(.read_only())
12507 } else {
12508 quote!()
12509 };
12510
12511 // `.serializer::<S>()` — reshape responses through a derived
12512 // serializer. Requires the downstream crate to enable the
12513 // `serializer` feature (the method is gated on it).
12514 let serializer_call = if let Some(ref ser) = attrs.serializer {
12515 quote!(.serializer::<#ser>())
12516 } else {
12517 quote!()
12518 };
12519
12520 let perms = &attrs.perms;
12521 let perms_call = if perms.list.is_empty()
12522 && perms.retrieve.is_empty()
12523 && perms.create.is_empty()
12524 && perms.update.is_empty()
12525 && perms.destroy.is_empty()
12526 {
12527 quote!()
12528 } else {
12529 let list_lits = perms.list.iter().map(|s| s.as_str());
12530 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12531 let create_lits = perms.create.iter().map(|s| s.as_str());
12532 let update_lits = perms.update.iter().map(|s| s.as_str());
12533 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12534 quote! {
12535 .permissions(#root::viewset::ViewSetPerms {
12536 list: ::std::vec![ #(#list_lits.to_owned()),* ],
12537 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12538 create: ::std::vec![ #(#create_lits.to_owned()),* ],
12539 update: ::std::vec![ #(#update_lits.to_owned()),* ],
12540 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
12541 })
12542 }
12543 };
12544
12545 Ok(quote! {
12546 impl #struct_name {
12547 /// Build an `axum::Router` with the six standard REST endpoints
12548 /// for this ViewSet, mounted at `prefix`.
12549 pub fn router(prefix: &str, pool: #root::sql::sqlx::PgPool) -> #root::__axum::Router {
12550 #root::viewset::ViewSet::for_model(
12551 <#model_path as #root::core::Model>::SCHEMA
12552 )
12553 #fields_call
12554 #filter_fields_call
12555 #search_fields_call
12556 #ordering_call
12557 #page_size_call
12558 #perms_call
12559 #read_only_call
12560 #serializer_call
12561 .router(prefix, pool)
12562 }
12563 }
12564 })
12565}
12566
12567fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
12568 let mut model: Option<syn::Path> = None;
12569 let mut fields: Option<Vec<String>> = None;
12570 let mut filter_fields: Vec<String> = Vec::new();
12571 let mut search_fields: Vec<String> = Vec::new();
12572 let mut ordering: Vec<(String, bool)> = Vec::new();
12573 let mut page_size: Option<usize> = None;
12574 let mut read_only = false;
12575 let mut perms = ViewSetPermsAttrs::default();
12576 let mut serializer: Option<syn::Path> = None;
12577
12578 for attr in &input.attrs {
12579 if !attr.path().is_ident("viewset") {
12580 continue;
12581 }
12582 attr.parse_nested_meta(|meta| {
12583 if meta.path.is_ident("model") {
12584 let path: syn::Path = meta.value()?.parse()?;
12585 model = Some(path);
12586 return Ok(());
12587 }
12588 if meta.path.is_ident("serializer") {
12589 let path: syn::Path = meta.value()?.parse()?;
12590 serializer = Some(path);
12591 return Ok(());
12592 }
12593 if meta.path.is_ident("fields") {
12594 let s: LitStr = meta.value()?.parse()?;
12595 fields = Some(split_field_list(&s.value()));
12596 return Ok(());
12597 }
12598 if meta.path.is_ident("filter_fields") {
12599 let s: LitStr = meta.value()?.parse()?;
12600 filter_fields = split_field_list(&s.value());
12601 return Ok(());
12602 }
12603 if meta.path.is_ident("search_fields") {
12604 let s: LitStr = meta.value()?.parse()?;
12605 search_fields = split_field_list(&s.value());
12606 return Ok(());
12607 }
12608 if meta.path.is_ident("ordering") {
12609 let s: LitStr = meta.value()?.parse()?;
12610 ordering = parse_ordering_list(&s.value());
12611 return Ok(());
12612 }
12613 if meta.path.is_ident("page_size") {
12614 let lit: syn::LitInt = meta.value()?.parse()?;
12615 page_size = Some(lit.base10_parse::<usize>()?);
12616 return Ok(());
12617 }
12618 if meta.path.is_ident("read_only") {
12619 read_only = true;
12620 return Ok(());
12621 }
12622 if meta.path.is_ident("permissions") {
12623 meta.parse_nested_meta(|inner| {
12624 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
12625 let s: LitStr = inner.value()?.parse()?;
12626 Ok(split_field_list(&s.value()))
12627 };
12628 if inner.path.is_ident("list") {
12629 perms.list = parse_codenames(&inner)?;
12630 } else if inner.path.is_ident("retrieve") {
12631 perms.retrieve = parse_codenames(&inner)?;
12632 } else if inner.path.is_ident("create") {
12633 perms.create = parse_codenames(&inner)?;
12634 } else if inner.path.is_ident("update") {
12635 perms.update = parse_codenames(&inner)?;
12636 } else if inner.path.is_ident("destroy") {
12637 perms.destroy = parse_codenames(&inner)?;
12638 } else {
12639 return Err(inner.error(
12640 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
12641 ));
12642 }
12643 Ok(())
12644 })?;
12645 return Ok(());
12646 }
12647 Err(meta.error(
12648 "unknown viewset attribute (supported: model, fields, filter_fields, \
12649 search_fields, ordering, page_size, read_only, serializer, permissions(...))",
12650 ))
12651 })?;
12652 }
12653
12654 let model = model.ok_or_else(|| {
12655 syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
12656 })?;
12657
12658 Ok(ViewSetAttrs {
12659 model,
12660 fields,
12661 filter_fields,
12662 search_fields,
12663 ordering,
12664 page_size,
12665 read_only,
12666 perms,
12667 serializer,
12668 })
12669}
12670
12671// ============================================================ #[derive(Serializer)]
12672
12673struct SerializerContainerAttrs {
12674 model: syn::Path,
12675 /// `#[serializer(validate = "fn_name")]` on the struct — DRF-shape
12676 /// cross-field validation hook (#436). The named inherent method
12677 /// must take `&self` and return
12678 /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
12679 /// `validate()` runs every per-field validator first; then calls
12680 /// the cross-field method if declared; aggregates all errors into
12681 /// one `FormErrors`.
12682 cross_validate: Option<syn::Ident>,
12683}
12684
12685#[derive(Default)]
12686struct SerializerFieldAttrs {
12687 read_only: bool,
12688 write_only: bool,
12689 source: Option<String>,
12690 skip: bool,
12691 /// `#[serializer(method = "fn_name")]` — DRF SerializerMethodField
12692 /// analog. The macro emits `from_model` initializer that calls
12693 /// `Self::fn_name(&model)` and stores the return value.
12694 method: Option<String>,
12695 /// `#[serializer(validate = "fn_name")]` — per-field validator
12696 /// callable run by `Self::validate(&self)`. Must return
12697 /// `Result<(), String>`. Errors land in `FormErrors` keyed by
12698 /// the field name.
12699 validate: Option<String>,
12700 /// `#[serializer(nested)]` on a field whose type is another
12701 /// `Serializer` — the macro emits `from_model` initializer that
12702 /// reads the parent via `model.<source>.value()` then calls the
12703 /// child serializer's `from_model(parent)`. When the FK is
12704 /// unloaded the field falls back to `Default::default()` (does
12705 /// NOT panic) so a missing prefetch in prod degrades gracefully.
12706 /// Source field on the model defaults to the field name; override
12707 /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
12708 /// panic-on-unloaded behavior for tests.
12709 nested: bool,
12710 /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
12711 /// strict behavior: panic when the FK isn't loaded. Useful in
12712 /// test code where forgetting select_related must trip a hard
12713 /// failure rather than render a blank nested object.
12714 nested_strict: bool,
12715 /// `#[serializer(many = TagSerializer)]` — declare the field as
12716 /// a list of nested serializers. Field type must be `Vec<S>`
12717 /// where `S` is the inner serializer. The macro initializes the
12718 /// field to `Vec::new()` in `from_model` and emits a typed
12719 /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
12720 /// maps each model row through `S::from_model`. Auto-load isn't
12721 /// possible (the M2M / one-to-many accessor is async); callers
12722 /// fetch the children + call the setter post-from_model.
12723 many: Option<syn::Type>,
12724 /// `#[serializer(slug = "name")]` — DRF `SlugRelatedField` analog.
12725 /// Source field on the model must be a `ForeignKey<T>`; the
12726 /// macro emits `from_model` glue that walks
12727 /// `model.<source>.value()?.<slug>` and clones it. Field type on
12728 /// the serializer is typically `String` (whatever type the slug
12729 /// column has). When the FK is unloaded the field falls back to
12730 /// `Default::default()`, same graceful-degrade contract as
12731 /// `nested`. Source defaults to the field name; override with
12732 /// `source = "..."`. v0.44.
12733 slug: Option<String>,
12734 /// `#[serializer(max_length = N)]` — DRF `MaxLengthValidator`. Caps
12735 /// the character count of a string field on write. Overrides the
12736 /// model's `max_length`; when absent the model value is inherited.
12737 max_length: Option<u64>,
12738 /// `#[serializer(min_length = N)]` — DRF `MinLengthValidator`.
12739 /// Serializer-only (the model has no `min_length` column).
12740 min_length: Option<u64>,
12741 /// `#[serializer(min = N)]` — DRF `MinValueValidator`. Inclusive
12742 /// integer lower bound; overrides the model's `min` when given.
12743 min: Option<i64>,
12744 /// `#[serializer(max = N)]` — DRF `MaxValueValidator`. Inclusive
12745 /// integer upper bound; overrides the model's `max` when given.
12746 max: Option<i64>,
12747}
12748
12749fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
12750 let mut model: Option<syn::Path> = None;
12751 let mut cross_validate: Option<syn::Ident> = None;
12752 for attr in &input.attrs {
12753 if !attr.path().is_ident("serializer") {
12754 continue;
12755 }
12756 attr.parse_nested_meta(|meta| {
12757 if meta.path.is_ident("model") {
12758 let _eq: syn::Token![=] = meta.input.parse()?;
12759 model = Some(meta.input.parse()?);
12760 return Ok(());
12761 }
12762 if meta.path.is_ident("validate") {
12763 // #436 — container-level `validate = "fn_name"` for the
12764 // DRF cross-field-validation shape. Field-level
12765 // `#[serializer(validate = "...")]` on a field is
12766 // parsed separately in `parse_serializer_field_attrs`.
12767 let s: LitStr = meta.value()?.parse()?;
12768 cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
12769 return Ok(());
12770 }
12771 Err(meta.error(
12772 "unknown serializer container attribute \
12773 (supported: `model`, `validate`)",
12774 ))
12775 })?;
12776 }
12777 let model = model.ok_or_else(|| {
12778 syn::Error::new_spanned(
12779 &input.ident,
12780 "`#[serializer(model = SomeModel)]` is required",
12781 )
12782 })?;
12783 Ok(SerializerContainerAttrs {
12784 model,
12785 cross_validate,
12786 })
12787}
12788
12789fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
12790 let mut out = SerializerFieldAttrs::default();
12791 for attr in &field.attrs {
12792 if !attr.path().is_ident("serializer") {
12793 continue;
12794 }
12795 attr.parse_nested_meta(|meta| {
12796 if meta.path.is_ident("read_only") {
12797 out.read_only = true;
12798 return Ok(());
12799 }
12800 if meta.path.is_ident("write_only") {
12801 out.write_only = true;
12802 return Ok(());
12803 }
12804 if meta.path.is_ident("skip") {
12805 out.skip = true;
12806 return Ok(());
12807 }
12808 if meta.path.is_ident("source") {
12809 let s: LitStr = meta.value()?.parse()?;
12810 out.source = Some(s.value());
12811 return Ok(());
12812 }
12813 if meta.path.is_ident("method") {
12814 let s: LitStr = meta.value()?.parse()?;
12815 out.method = Some(s.value());
12816 return Ok(());
12817 }
12818 if meta.path.is_ident("validate") {
12819 let s: LitStr = meta.value()?.parse()?;
12820 out.validate = Some(s.value());
12821 return Ok(());
12822 }
12823 if meta.path.is_ident("many") {
12824 let _eq: syn::Token![=] = meta.input.parse()?;
12825 out.many = Some(meta.input.parse()?);
12826 return Ok(());
12827 }
12828 if meta.path.is_ident("nested") {
12829 out.nested = true;
12830 // Optional strict flag inside parentheses:
12831 // #[serializer(nested(strict))]
12832 if meta.input.peek(syn::token::Paren) {
12833 meta.parse_nested_meta(|inner| {
12834 if inner.path.is_ident("strict") {
12835 out.nested_strict = true;
12836 return Ok(());
12837 }
12838 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
12839 })?;
12840 }
12841 return Ok(());
12842 }
12843 if meta.path.is_ident("slug") {
12844 let s: LitStr = meta.value()?.parse()?;
12845 out.slug = Some(s.value());
12846 return Ok(());
12847 }
12848 if meta.path.is_ident("max_length") {
12849 let lit: syn::LitInt = meta.value()?.parse()?;
12850 out.max_length = Some(lit.base10_parse::<u64>()?);
12851 return Ok(());
12852 }
12853 if meta.path.is_ident("min_length") {
12854 let lit: syn::LitInt = meta.value()?.parse()?;
12855 out.min_length = Some(lit.base10_parse::<u64>()?);
12856 return Ok(());
12857 }
12858 if meta.path.is_ident("min") {
12859 let lit: syn::LitInt = meta.value()?.parse()?;
12860 out.min = Some(lit.base10_parse::<i64>()?);
12861 return Ok(());
12862 }
12863 if meta.path.is_ident("max") {
12864 let lit: syn::LitInt = meta.value()?.parse()?;
12865 out.max = Some(lit.base10_parse::<i64>()?);
12866 return Ok(());
12867 }
12868 Err(meta.error(
12869 "unknown serializer field attribute (supported: \
12870 `read_only`, `write_only`, `source`, `skip`, `method`, \
12871 `validate`, `nested`, `many`, `slug`, `max_length`, \
12872 `min_length`, `min`, `max`)",
12873 ))
12874 })?;
12875 }
12876 // Validate: read_only + write_only is nonsensical
12877 if out.read_only && out.write_only {
12878 return Err(syn::Error::new_spanned(
12879 field,
12880 "a field cannot be both `read_only` and `write_only`",
12881 ));
12882 }
12883 if out.method.is_some() && out.source.is_some() {
12884 return Err(syn::Error::new_spanned(
12885 field,
12886 "`method` and `source` are mutually exclusive — `method` computes \
12887 the value from a method, `source` reads it from a different model field",
12888 ));
12889 }
12890 if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
12891 return Err(syn::Error::new_spanned(
12892 field,
12893 "`slug` is mutually exclusive with `method`, `nested`, and `many` \
12894 — pick one strategy for populating the field",
12895 ));
12896 }
12897 Ok(out)
12898}
12899
12900fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
12901 let root = rustango_root();
12902 let struct_name = &input.ident;
12903 let struct_name_lit = struct_name.to_string();
12904
12905 let Data::Struct(data) = &input.data else {
12906 return Err(syn::Error::new_spanned(
12907 struct_name,
12908 "Serializer can only be derived on structs",
12909 ));
12910 };
12911 let Fields::Named(named) = &data.fields else {
12912 return Err(syn::Error::new_spanned(
12913 struct_name,
12914 "Serializer requires a struct with named fields",
12915 ));
12916 };
12917
12918 let container = parse_serializer_container_attrs(input)?;
12919 let model_path = &container.model;
12920
12921 // Classify each field. `ty` is only consumed by the
12922 // `#[cfg(feature = "openapi")]` block below, but we always
12923 // capture it to keep the field-info build a single pass.
12924 #[allow(dead_code)]
12925 struct FieldInfo {
12926 ident: syn::Ident,
12927 ty: syn::Type,
12928 attrs: SerializerFieldAttrs,
12929 }
12930 let mut fields_info: Vec<FieldInfo> = Vec::new();
12931 for field in &named.named {
12932 let ident = field.ident.clone().expect("named field has ident");
12933 let attrs = parse_serializer_field_attrs(field)?;
12934 fields_info.push(FieldInfo {
12935 ident,
12936 ty: field.ty.clone(),
12937 attrs,
12938 });
12939 }
12940
12941 // Generate from_model body: struct literal with each field assigned.
12942 let from_model_fields = fields_info.iter().map(|fi| {
12943 let ident = &fi.ident;
12944 let ty = &fi.ty;
12945 if let Some(_inner) = &fi.attrs.many {
12946 // Many — collection field. Initialize empty; caller
12947 // populates via the macro-emitted set_<field> helper
12948 // after fetching the M2M children.
12949 quote! { #ident: ::std::vec::Vec::new() }
12950 } else if let Some(method) = &fi.attrs.method {
12951 // SerializerMethodField: call Self::<method>(&model) to
12952 // compute the value. Method signature must be
12953 // `fn <method>(model: &T) -> <field type>`.
12954 let method_ident = syn::Ident::new(method, ident.span());
12955 quote! { #ident: Self::#method_ident(model) }
12956 } else if let Some(slug_field) = &fi.attrs.slug {
12957 // v0.44 — SlugRelatedField. Source defaults to the field
12958 // name on this struct; override via `source = "..."`. The
12959 // source field on the model is expected to be a
12960 // `ForeignKey<T>`; the slug field on the parent is named
12961 // by the attribute value. When the FK is unloaded the
12962 // field falls back to `Default::default()` — same
12963 // graceful-degrade contract as `nested`.
12964 let src_name = fi
12965 .attrs
12966 .source
12967 .as_deref()
12968 .unwrap_or(&fi.ident.to_string())
12969 .to_owned();
12970 let src_ident = syn::Ident::new(&src_name, ident.span());
12971 let slug_ident = syn::Ident::new(slug_field, ident.span());
12972 quote! {
12973 #ident: match model.#src_ident.value() {
12974 ::core::option::Option::Some(__loaded) =>
12975 ::core::clone::Clone::clone(&__loaded.#slug_ident),
12976 ::core::option::Option::None =>
12977 ::core::default::Default::default(),
12978 }
12979 }
12980 } else if fi.attrs.nested {
12981 // Nested serializer. Source defaults to the field name on
12982 // this struct; override via `source = "..."`. The source
12983 // field on the model is expected to be a `ForeignKey<T>`
12984 // whose `.value()` returns `Option<&T>` after lazy-load.
12985 //
12986 // Behavior matrix (tweakable per-field):
12987 // * FK loaded → nested object materializes via
12988 // ChildSerializer::from_model(parent).
12989 // * FK unloaded → fall back to ChildSerializer::default()
12990 // (so prod doesn't crash on a missing
12991 // prefetch — just renders a blank nested
12992 // object). Add `#[serializer(nested,
12993 // strict)]` to keep the v0.18.1
12994 // panic-on-unloaded behavior for tests
12995 // that want hard guardrails.
12996 let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
12997 let src_ident = syn::Ident::new(&src_name, ident.span());
12998 if fi.attrs.nested_strict {
12999 let panic_msg = format!(
13000 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
13001 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
13002 );
13003 quote! {
13004 #ident: <#ty as #root::serializer::ModelSerializer>::from_model(
13005 model.#src_ident.value().expect(#panic_msg),
13006 )
13007 }
13008 } else {
13009 quote! {
13010 #ident: match model.#src_ident.value() {
13011 ::core::option::Option::Some(__loaded) =>
13012 <#ty as #root::serializer::ModelSerializer>::from_model(__loaded),
13013 ::core::option::Option::None =>
13014 ::core::default::Default::default(),
13015 }
13016 }
13017 }
13018 } else if fi.attrs.write_only || fi.attrs.skip {
13019 // Not read from model — use default
13020 quote! { #ident: ::core::default::Default::default() }
13021 } else if let Some(src) = &fi.attrs.source {
13022 let src_ident = syn::Ident::new(src, ident.span());
13023 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13024 } else {
13025 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13026 }
13027 });
13028
13029 // is_writable predicate (also used by writable_lits /
13030 // writable_source_lits below).
13031 let is_writable = |fi: &&FieldInfo| {
13032 !fi.attrs.read_only
13033 && !fi.attrs.skip
13034 && fi.attrs.method.is_none()
13035 && !fi.attrs.nested
13036 && fi.attrs.many.is_none()
13037 && fi.attrs.slug.is_none()
13038 };
13039
13040 // Declarative field constraints (DRF `validators=[...]`): one block
13041 // per writable field, run inside `validate()`. Each resolves its
13042 // bounds as the serializer attr when given (`#[serializer(max_length
13043 // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13044 // `max` / `choices`), then dispatches on the field's JSON value via
13045 // `forms::validators::check_value` (string → length/choices, integer
13046 // → min/max).
13047 let opt_usize = |v: Option<u64>| match v {
13048 Some(n) => {
13049 let n = n as usize;
13050 quote!(::core::option::Option::Some(#n))
13051 }
13052 None => quote!(::core::option::Option::None),
13053 };
13054 let opt_i64 = |v: Option<i64>| match v {
13055 Some(n) => quote!(::core::option::Option::Some(#n)),
13056 None => quote!(::core::option::Option::None),
13057 };
13058 let constraint_blocks: Vec<_> = fields_info
13059 .iter()
13060 .filter(is_writable)
13061 .map(|fi| {
13062 let ident = &fi.ident;
13063 let fname = ident.to_string();
13064 let mname = fi
13065 .attrs
13066 .source
13067 .clone()
13068 .unwrap_or_else(|| fi.ident.to_string());
13069 let attr_max_len = opt_usize(fi.attrs.max_length);
13070 let attr_min_len = opt_usize(fi.attrs.min_length);
13071 let attr_min = opt_i64(fi.attrs.min);
13072 let attr_max = opt_i64(fi.attrs.max);
13073 quote! {
13074 {
13075 let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13076 let __max_length: ::core::option::Option<usize> = #attr_max_len
13077 .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13078 let __min_length: ::core::option::Option<usize> = #attr_min_len;
13079 let __min: ::core::option::Option<i64> =
13080 #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13081 let __max: ::core::option::Option<i64> =
13082 #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13083 let __choices = __sf.and_then(|__f| __f.choices);
13084 let __v = #root::__serde_json::to_value(&self.#ident)
13085 .unwrap_or(#root::__serde_json::Value::Null);
13086 #root::forms::validators::check_value(
13087 #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13088 &mut __errors,
13089 );
13090 }
13091 }
13092 })
13093 .collect();
13094 let has_constraints = !constraint_blocks.is_empty();
13095
13096 // Per-field validators (DRF-shape `validators=[...]`). Emit a
13097 // `validate(&self)` method that runs each user-defined validator
13098 // and aggregates errors into `FormErrors`.
13099 let validator_calls: Vec<_> = fields_info
13100 .iter()
13101 .filter_map(|fi| {
13102 let ident = &fi.ident;
13103 let name_lit = ident.to_string();
13104 let method = fi.attrs.validate.as_ref()?;
13105 let method_ident = syn::Ident::new(method, ident.span());
13106 Some(quote! {
13107 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13108 __errors.add(#name_lit.to_owned(), __e);
13109 }
13110 })
13111 })
13112 .collect();
13113 // #436 — DRF cross-field `validate(self)` shape. If the
13114 // container declared `#[serializer(validate = "fn_name")]`,
13115 // the macro-generated `validate(&self)` runs every per-field
13116 // validator first, then calls the user's cross-field method,
13117 // merging its `FormErrors` into the per-field errors. Either
13118 // alone is enough to emit the wrapper.
13119 let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13120 quote! {
13121 // Merge cross-field errors into the per-field bucket so
13122 // a single .validate() call surfaces both layers.
13123 if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13124 __errors.merge(__cross);
13125 }
13126 }
13127 });
13128 let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13129 // Anything to run? Declarative constraints (for any writable field) +
13130 // per-field validators + cross-field hook.
13131 let has_run_validations = has_validators || has_constraints;
13132 // Shared body: declarative field constraints first (length / range /
13133 // choices, serializer-attr-or-model), then per-field validators, then
13134 // the cross-field hook.
13135 let validate_body = quote! {
13136 let mut __errors = #root::forms::FormErrors::default();
13137 #( #constraint_blocks )*
13138 #( #validator_calls )*
13139 #cross_validate_call
13140 if __errors.is_empty() {
13141 ::core::result::Result::Ok(())
13142 } else {
13143 ::core::result::Result::Err(__errors)
13144 }
13145 };
13146 // Inherent `validate(&self)` — kept for back-compat with direct
13147 // `serializer.validate()` calls that don't import `ModelSerializer`.
13148 // Emitted only when the serializer declares per-field/cross-field
13149 // validators (unchanged rule) so it never collides with a
13150 // hand-written inherent `validate`.
13151 let validate_method = if has_validators {
13152 quote! {
13153 impl #struct_name {
13154 /// Run the declarative field constraints, every
13155 /// `#[serializer(validate = "...")]` per-field validator,
13156 /// and (when declared) the container-level cross-field
13157 /// validator. Aggregates errors into `FormErrors` keyed by
13158 /// the field name. Returns `Ok(())` when all pass.
13159 pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13160 #validate_body
13161 }
13162 }
13163 }
13164 } else {
13165 quote! {}
13166 };
13167 // Trait-method override so the type-erased ViewSet write path can
13168 // dispatch `ModelSerializer::validate` generically. Emitted whenever
13169 // there's anything to run (declarative constraints inherited from the
13170 // model count); otherwise the trait's default no-op is used.
13171 let trait_validate_override = if has_run_validations {
13172 quote! {
13173 fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13174 #validate_body
13175 }
13176 }
13177 } else {
13178 quote! {}
13179 };
13180
13181 // For every `#[serializer(many = S)]` field, emit a
13182 // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13183 // helper that maps the parents through `S::from_model`.
13184 let many_setters: Vec<_> = fields_info
13185 .iter()
13186 .filter_map(|fi| {
13187 let many_ty = fi.attrs.many.as_ref()?;
13188 let ident = &fi.ident;
13189 let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
13190 Some(quote! {
13191 /// Populate this `many` field by mapping each parent model
13192 /// through the inner serializer's `from_model`. Call after
13193 /// fetching the M2M / one-to-many children since
13194 /// `from_model` itself can't await an SQL query.
13195 pub fn #setter(
13196 &mut self,
13197 models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13198 ) -> &mut Self {
13199 self.#ident = models.iter()
13200 .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13201 .collect();
13202 self
13203 }
13204 })
13205 })
13206 .collect();
13207 let many_setters_impl = if many_setters.is_empty() {
13208 quote! {}
13209 } else {
13210 quote! {
13211 impl #struct_name {
13212 #( #many_setters )*
13213 }
13214 }
13215 };
13216
13217 // Generate custom Serialize: skip write_only fields
13218 let output_fields: Vec<_> = fields_info
13219 .iter()
13220 .filter(|fi| !fi.attrs.write_only)
13221 .collect();
13222 let output_field_count = output_fields.len();
13223 let serialize_fields = output_fields.iter().map(|fi| {
13224 let ident = &fi.ident;
13225 let name_lit = ident.to_string();
13226 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13227 });
13228
13229 // writable_fields: normal + write_only.
13230 // Exclude:
13231 // - `read_only` — server-computed.
13232 // - `skip` — caller sets manually post-from_model.
13233 // - `method` — computed from a Self::fn(&model) call; accepting
13234 // it on write is meaningless.
13235 // - `nested` / `many` — populated from related-model data, not
13236 // from a field on the wire body.
13237 // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13238 // `many` in `writable_fields()`, which made the ViewSet write
13239 // path accept those fields from the JSON body and try to bind
13240 // them to the SQL UPDATE — a silent no-op at best, a type
13241 // mismatch at worst.
13242 // (`is_writable` is defined above, near the constraint codegen.)
13243 let writable_lits: Vec<_> = fields_info
13244 .iter()
13245 .filter(is_writable)
13246 .map(|fi| fi.ident.to_string())
13247 .collect();
13248
13249 // `writable_source_fields`: the MODEL field names of writable
13250 // serializer fields (source-resolved). The ViewSet write path skips
13251 // every model column NOT in this set, so `read_only` / `method` /
13252 // computed fields a client posts are ignored instead of written.
13253 let writable_source_lits: Vec<String> = fields_info
13254 .iter()
13255 .filter(is_writable)
13256 .map(|fi| {
13257 fi.attrs
13258 .source
13259 .clone()
13260 .unwrap_or_else(|| fi.ident.to_string())
13261 })
13262 .collect();
13263
13264 // `from_writable_json`: build a partial instance for input
13265 // validation. Writable fields are parsed from the JSON body (keyed
13266 // by serializer field name); every other field defaults. Per-field
13267 // type errors collect into `FormErrors` keyed by the field name.
13268 // Construction is field-by-field (not `Self::default()`) so it works
13269 // for serializers regardless of a struct-level `Default`.
13270 let from_writable_json_inits: Vec<_> = fields_info
13271 .iter()
13272 .map(|fi| {
13273 let ident = &fi.ident;
13274 let fname = ident.to_string();
13275 let ty = &fi.ty;
13276 if is_writable(&fi) {
13277 quote! {
13278 #ident: match __obj.and_then(|__o| __o.get(#fname)) {
13279 ::core::option::Option::Some(__v) => {
13280 match #root::__serde_json::from_value::<#ty>(
13281 ::core::clone::Clone::clone(__v),
13282 ) {
13283 ::core::result::Result::Ok(__x) => __x,
13284 ::core::result::Result::Err(__e) => {
13285 __errors.add(#fname.to_owned(), __e.to_string());
13286 ::core::default::Default::default()
13287 }
13288 }
13289 }
13290 ::core::option::Option::None => ::core::default::Default::default(),
13291 }
13292 }
13293 } else {
13294 quote! { #ident: ::core::default::Default::default() }
13295 }
13296 })
13297 .collect();
13298
13299 // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
13300 // Only includes fields shown in JSON output (skips write_only). For each
13301 // `Option<T>` field, omit from `required` and add `.nullable()`.
13302 let openapi_impl = {
13303 #[cfg(feature = "openapi")]
13304 {
13305 let property_calls = output_fields.iter().map(|fi| {
13306 let ident = &fi.ident;
13307 let name_lit = ident.to_string();
13308 let ty = &fi.ty;
13309 let nullable_call = if is_option(ty) {
13310 quote! { .nullable() }
13311 } else {
13312 quote! {}
13313 };
13314 quote! {
13315 .property(
13316 #name_lit,
13317 <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13318 #nullable_call,
13319 )
13320 }
13321 });
13322 let required_lits: Vec<_> = output_fields
13323 .iter()
13324 .filter(|fi| !is_option(&fi.ty))
13325 .map(|fi| fi.ident.to_string())
13326 .collect();
13327 quote! {
13328 impl #root::openapi::OpenApiSchema for #struct_name {
13329 fn openapi_schema() -> #root::openapi::Schema {
13330 #root::openapi::Schema::object()
13331 #( #property_calls )*
13332 .required([ #( #required_lits ),* ])
13333 }
13334 }
13335 }
13336 }
13337 #[cfg(not(feature = "openapi"))]
13338 {
13339 quote! {}
13340 }
13341 };
13342
13343 Ok(quote! {
13344 impl #root::serializer::ModelSerializer for #struct_name {
13345 type Model = #model_path;
13346
13347 fn from_model(model: &Self::Model) -> Self {
13348 Self {
13349 #( #from_model_fields ),*
13350 }
13351 }
13352
13353 fn writable_fields() -> &'static [&'static str] {
13354 &[ #( #writable_lits ),* ]
13355 }
13356
13357 fn writable_source_fields() -> &'static [&'static str] {
13358 &[ #( #writable_source_lits ),* ]
13359 }
13360
13361 fn from_writable_json(
13362 __body: &#root::__serde_json::Value,
13363 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
13364 let mut __errors = #root::forms::FormErrors::default();
13365 let __obj = __body.as_object();
13366 let __out = Self {
13367 #( #from_writable_json_inits ),*
13368 };
13369 if __errors.is_empty() {
13370 ::core::result::Result::Ok(__out)
13371 } else {
13372 ::core::result::Result::Err(__errors)
13373 }
13374 }
13375
13376 #trait_validate_override
13377 }
13378
13379 impl #root::__serde::Serialize for #struct_name {
13380 fn serialize<S>(&self, serializer: S)
13381 -> ::core::result::Result<S::Ok, S::Error>
13382 where
13383 S: #root::__serde::Serializer,
13384 {
13385 use #root::__serde::ser::SerializeStruct;
13386 let mut __state = serializer.serialize_struct(
13387 #struct_name_lit,
13388 #output_field_count,
13389 )?;
13390 #( #serialize_fields )*
13391 __state.end()
13392 }
13393 }
13394
13395 #openapi_impl
13396
13397 #validate_method
13398
13399 #many_setters_impl
13400 })
13401}
13402
13403/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
13404/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
13405/// when the feature is off.
13406#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
13407fn is_option(ty: &syn::Type) -> bool {
13408 if let syn::Type::Path(p) = ty {
13409 if let Some(last) = p.path.segments.last() {
13410 return last.ident == "Option";
13411 }
13412 }
13413 false
13414}