rustango_macros/lib.rs
1//! Proc-macros for rustango.
2//!
3//! v0.1 ships `#[derive(Model)]`, which emits:
4//! * a `Model` impl carrying a static `ModelSchema`,
5//! * an `inventory::submit!` so the model is discoverable from the registry,
6//! * an inherent `objects()` returning a `QuerySet<Self>`,
7//! * a `sqlx::FromRow` impl so query results decode into the struct.
8
9use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::quote;
12use syn::{
13 parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields, GenericArgument, LitStr,
14 PathArguments, Type, TypePath,
15};
16
17/// Resolve the consumer's local name for the `rustango` crate so the
18/// macro-emitted code keeps compiling when a downstream `Cargo.toml`
19/// renames the dep (`[dependencies] orm = { package = "rustango", … }`)
20/// or when the standalone `rustango-orm` crate ships in a future
21/// slice of the [orm-extract epic](https://github.com/ujeenet/rustango/issues/149).
22///
23/// Returns one of:
24/// - `quote!(::rustango)` — the consumer IS the `rustango` crate
25/// itself. We emit the absolute path `::rustango` (NOT `crate`)
26/// because examples and integration tests inside the rustango
27/// package compile as separate binaries — their own `crate::`
28/// namespace is the example/test file, not rustango's lib root.
29/// The absolute `::rustango` resolves correctly in both contexts
30/// (rustango lib code AND rustango/examples/*.rs).
31/// - `quote!(::ident)` — the consumer renamed the dep; emit the
32/// user's chosen ident.
33/// - `quote!(::rustango)` — fallback when `proc-macro-crate` can't
34/// read the manifest (rare; preserves today's behavior).
35///
36/// Issue [#142](https://github.com/ujeenet/rustango/issues/142).
37fn rustango_root() -> TokenStream2 {
38 use proc_macro_crate::{crate_name, FoundCrate};
39 match crate_name("rustango") {
40 Ok(FoundCrate::Itself) => quote!(::rustango),
41 Ok(FoundCrate::Name(name)) => {
42 let ident = proc_macro2::Ident::new(&name, proc_macro2::Span::call_site());
43 quote!(::#ident)
44 }
45 Err(_) => quote!(::rustango),
46 }
47}
48
49/// Derive a `Model` impl. See crate docs for the supported attributes.
50#[proc_macro_derive(Model, attributes(rustango))]
51pub fn derive_model(input: TokenStream) -> TokenStream {
52 let input = parse_macro_input!(input as DeriveInput);
53 expand(&input)
54 .unwrap_or_else(syn::Error::into_compile_error)
55 .into()
56}
57
58/// Derive a `router(prefix, pool) -> axum::Router` associated method on a
59/// marker struct, wiring the full CRUD ViewSet in one annotation.
60///
61/// ```ignore
62/// #[derive(ViewSet)]
63/// #[viewset(
64/// model = Post,
65/// fields = "id, title, body, author_id",
66/// filter_fields = "author_id",
67/// search_fields = "title, body",
68/// ordering = "-published_at",
69/// page_size = 20,
70/// )]
71/// pub struct PostViewSet;
72///
73/// // Mount into your app:
74/// let app = Router::new()
75/// .merge(PostViewSet::router("/api/posts", pool.clone()));
76/// ```
77///
78/// Attributes:
79/// * `model = TypeName` — *required*. The `#[derive(Model)]` struct whose
80/// `SCHEMA` constant drives the endpoints.
81/// * `fields = "a, b, c"` — scalar fields included in list/retrieve JSON
82/// and accepted on create/update (default: all scalar fields).
83/// * `filter_fields = "a, b"` — fields filterable via `?a=v` query params.
84/// * `search_fields = "a, b"` — fields searched by `?search=...`.
85/// * `ordering = "a, -b"` — default list ordering; prefix `-` for DESC.
86/// * `page_size = N` — default page size (default: 20, max: 1000).
87/// * `read_only` — flag; wires only `list` + `retrieve` (no mutations).
88/// * `serializer = SomeSerializer` — render list / retrieve / create
89/// responses through a `#[derive(Serializer)]` type instead of the
90/// default field-level projection (`read_only` / `source` / `method`
91/// / `write_only` overrides apply). Tri-dialect. Requires the
92/// `serializer` feature.
93/// * `permissions(list = "...", retrieve = "...", create = "...",
94/// update = "...", destroy = "...")` — codenames required per action.
95#[proc_macro_derive(ViewSet, attributes(viewset))]
96pub fn derive_viewset(input: TokenStream) -> TokenStream {
97 let input = parse_macro_input!(input as DeriveInput);
98 expand_viewset(&input)
99 .unwrap_or_else(syn::Error::into_compile_error)
100 .into()
101}
102
103/// Derive `rustango::forms::Form` (slice 8.4B). Generates a
104/// `parse(&HashMap<String, String>) -> Result<Self, FormErrors>` impl
105/// that walks every named field and:
106///
107/// * Parses the string value into the field's Rust type (`String`,
108/// `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
109/// nullable case).
110/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
111/// `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
112/// validators in declaration order, returning `FormError::Parse`
113/// on the first failure.
114///
115/// Example:
116///
117/// ```ignore
118/// #[derive(Form)]
119/// pub struct CreateItemForm {
120/// #[form(min_length = 1, max_length = 64)]
121/// pub name: String,
122/// #[form(min = 0, max = 150)]
123/// pub age: i32,
124/// pub active: bool,
125/// pub email: Option<String>,
126/// }
127///
128/// let parsed = CreateItemForm::parse(&form_map)?;
129/// ```
130#[proc_macro_derive(Form, attributes(form))]
131pub fn derive_form(input: TokenStream) -> TokenStream {
132 let input = parse_macro_input!(input as DeriveInput);
133 expand_form(&input)
134 .unwrap_or_else(syn::Error::into_compile_error)
135 .into()
136}
137
138/// Derive `rustango::serializer::ModelSerializer` for a struct.
139/// (intra-doc link disabled — the macro crate doesn't depend on
140/// `rustango` itself, so rustdoc can't resolve the path.)
141///
142/// # Container attribute (required)
143/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
144///
145/// # Field attributes
146/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
147/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
148/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
149/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
150/// - `#[serializer(method = "fn_name")]` — DRF `SerializerMethodField`: calls `Self::fn_name(&model)` for the field value; excluded from `writable_fields()`
151/// - `#[serializer(nested)]` / `nested(strict)` — auto-resolves nested serializer from a loaded `ForeignKey`; excluded from `writable_fields()`
152/// - `#[serializer(many = ChildSerializer)]` — collection of nested serializers; populated via macro-emitted `set_<field>(&[Child::Model])`; excluded from `writable_fields()`
153/// - `#[serializer(slug = "name")]` — DRF `SlugRelatedField`: clones `model.<source>.value()?.name`; excluded from `writable_fields()` (v0.44)
154/// - `#[serializer(validate = "fn_name")]` — per-field validator surfaced by `Self::validate(&self)`
155/// - `#[serializer(max_length = N)]` / `min_length` / `min` / `max` — declarative bounds checked on
156/// write; auto-inherit from the model's `FieldSchema` (`max_length`/`min`/`max`/`choices`) when
157/// no attr is given, override it when given (`min_length` is serializer-only)
158///
159/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
160#[proc_macro_derive(Serializer, attributes(serializer))]
161pub fn derive_serializer(input: TokenStream) -> TokenStream {
162 let input = parse_macro_input!(input as DeriveInput);
163 expand_serializer(&input)
164 .unwrap_or_else(syn::Error::into_compile_error)
165 .into()
166}
167
168/// Bake every `*.json` migration file in a directory into the binary
169/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
170/// of `(name, json_content)` pairs, lex-sorted by file stem.
171///
172/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
173/// behaviour as `migrate(pool, dir)` but with no filesystem access.
174/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
175/// (i.e. the crate that invokes the macro). Default is
176/// `"./migrations"` if no argument is supplied.
177///
178/// ```ignore
179/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
180/// // or:
181/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
182///
183/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
184/// ```
185///
186/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
187/// file's `name` field must equal its file stem, every `prev`
188/// reference must point to another migration in the same directory,
189/// and the JSON must parse. A broken chain — orphan `prev`, missing
190/// predecessor, malformed file — fails at macro-expansion time with
191/// a clear `compile_error!`. *No other Django-shape Rust framework
192/// validates migration chains at compile time*: Cot's migrations are
193/// imperative Rust code (no static chain), Loco's are SeaORM
194/// up/down (same), Rwf's are raw SQL (no chain at all).
195///
196/// Each migration is included via `include_str!` so cargo's rebuild
197/// detection picks up file *content* changes. **Caveat:** cargo
198/// doesn't watch directory listings, so adding or removing a
199/// migration file inside the dir won't auto-trigger a rebuild — run
200/// `cargo clean` (or just bump any other source file) when you add
201/// new migrations during embedded development.
202#[proc_macro]
203pub fn embed_migrations(input: TokenStream) -> TokenStream {
204 expand_embed_migrations(input.into())
205 .unwrap_or_else(syn::Error::into_compile_error)
206 .into()
207}
208
209/// `Q!()` — Django-shape filter syntax compile-time-resolved against
210/// typed columns. Issue #269 / T1.7.
211///
212/// Each invocation lowers to the equivalent typed-column method call:
213///
214/// ```ignore
215/// // These expand identically:
216/// Q!(User.email__icontains = "alice")
217/// User::email.ilike("%alice%")
218/// ```
219///
220/// Field-name typos fail the build (the macro emits `User::no_such_field`
221/// which doesn't exist) — the headline ergonomic win of this slice over
222/// Django's stringly-typed `__lookup` filters.
223///
224/// # Supported lookup suffixes
225///
226/// * bare `=` / `__exact` → `.eq(value)`
227/// * `__iexact` → `.ilike(value)` (case-insensitive equality, no wildcards)
228/// * `__ne` → `.ne(value)`
229/// * `__gt` / `__gte` / `__lt` / `__lte` → corresponding comparison
230/// * `__contains` / `__icontains` → `.like("%v%")` / `.ilike("%v%")`
231/// * `__startswith` / `__istartswith` → `.like("v%")` / `.ilike("v%")`
232/// * `__endswith` / `__iendswith` → `.like("%v")` / `.ilike("%v")`
233/// * `__in` → `.is_in(iterable)`
234/// * `__not_in` → `.not_in(iterable)`
235/// * `__isnull = true` → `.is_null()`; `__isnull = false` → `.is_not_null()`
236/// * `__between` accepts a tuple literal `(lo, hi)` → `.between(lo, hi)`
237/// * `__regex` / `__iregex` → `.regex(pattern)` / `.iregex(pattern)`
238///
239/// Unknown suffixes fail the build with a `compile_error!` pointing at
240/// the lookup token.
241///
242/// # Combine
243///
244/// Each `Q!()` returns a `TypedFilter<Model>` — chain via the existing
245/// `.and()` / `.or()` / `.not()` methods:
246///
247/// ```ignore
248/// User::objects()
249/// .where_(
250/// Q!(User.active = true)
251/// .and(Q!(User.email__icontains = "alice"))
252/// )
253/// .fetch(&pool).await?;
254/// ```
255///
256/// All emitted code routes through existing per-dialect writers — no new
257/// SQL emission machinery. Tri-dialect support is inherent.
258#[allow(non_snake_case)]
259#[proc_macro]
260pub fn Q(input: TokenStream) -> TokenStream {
261 expand_q(input.into())
262 .unwrap_or_else(syn::Error::into_compile_error)
263 .into()
264}
265
266/// `#[rustango::main]` — the Django-shape runserver entrypoint. Wraps
267/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
268/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
269/// functions are zero-boilerplate:
270///
271/// ```ignore
272/// #[rustango::main]
273/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
274/// rustango::server::Builder::from_env().await?
275/// .migrate("migrations").await?
276/// .api(my_app::urls::api())
277/// .seed_with(my_app::seed::run).await?
278/// .serve("0.0.0.0:8080").await
279/// }
280/// ```
281///
282/// Optional `flavor = "current_thread"` passes through to
283/// `#[tokio::main]`; default is the multi-threaded runtime.
284///
285/// Pulls `tracing-subscriber` into the rustango crate behind the
286/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
287/// out get plain `#[tokio::main]` ergonomics without the dependency.
288#[proc_macro_attribute]
289pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
290 expand_main(args.into(), item.into())
291 .unwrap_or_else(syn::Error::into_compile_error)
292 .into()
293}
294
295fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
296 let mut input: syn::ItemFn = syn::parse2(item)?;
297 if input.sig.asyncness.is_none() {
298 return Err(syn::Error::new(
299 input.sig.ident.span(),
300 "`#[rustango::main]` must wrap an `async fn`",
301 ));
302 }
303
304 // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
305 // to `#[tokio::main]`. Tokio's proc-macro internally emits
306 // `::tokio::*` paths that resolve against the user crate's deps,
307 // so calling it through the rustango re-export still requires the
308 // user to add tokio to their own Cargo.toml. Building the
309 // runtime ourselves keeps the dep transitive through the
310 // `runtime` feature on rustango.
311 //
312 // Parse optional `flavor = "current_thread"` / `flavor =
313 // "multi_thread"` from the attribute args. Unknown args are
314 // tolerated (forward-compat with tokio's own arg surface).
315 let root = rustango_root();
316 let flavor = parse_flavor(&args);
317 let builder_call = match flavor {
318 Flavor::CurrentThread => quote! {
319 #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
320 },
321 Flavor::MultiThread => quote! {
322 #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
323 },
324 };
325
326 // Detach the user body and rewrite `main` as a sync fn that
327 // builds the runtime and blocks on the async body.
328 let user_body = input.block.clone();
329 input.sig.asyncness = None;
330 input.block = syn::parse2(quote! {{
331 {
332 use #root::__private_runtime::tracing_subscriber::{self, EnvFilter};
333 // `try_init` so duplicate installers (e.g. tests already
334 // holding a subscriber) don't panic.
335 let _ = tracing_subscriber::fmt()
336 .with_env_filter(
337 EnvFilter::try_from_default_env()
338 .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
339 )
340 .try_init();
341 }
342 let __rt = #builder_call
343 .enable_all()
344 .build()
345 .expect("failed to build tokio runtime");
346 __rt.block_on(async move #user_body)
347 }})?;
348
349 Ok(quote! {
350 #input
351 })
352}
353
354enum Flavor {
355 MultiThread,
356 CurrentThread,
357}
358
359fn parse_flavor(args: &TokenStream2) -> Flavor {
360 // Cheap parser: look for the literal token sequence
361 // `flavor = "current_thread"`. Everything else (including
362 // bare `multi_thread` or no args) defaults to multi-thread.
363 let s = args.to_string();
364 if s.contains("current_thread") {
365 Flavor::CurrentThread
366 } else {
367 Flavor::MultiThread
368 }
369}
370
371/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
372struct QInput {
373 base_path: syn::Path,
374 field: syn::Ident,
375 value: syn::Expr,
376}
377
378impl syn::parse::Parse for QInput {
379 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
380 let base_path: syn::Path = input.parse()?;
381 input.parse::<syn::Token![.]>()?;
382 let field: syn::Ident = input.parse()?;
383 input.parse::<syn::Token![=]>()?;
384 let value: syn::Expr = input.parse()?;
385 Ok(QInput {
386 base_path,
387 field,
388 value,
389 })
390 }
391}
392
393fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
394 let q: QInput = syn::parse2(input)?;
395 let root = rustango_root();
396 let field_str = q.field.to_string();
397 let field_span = q.field.span();
398 let (base, suffix) = match field_str.find("__") {
399 Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
400 None => (field_str.as_str(), ""),
401 };
402 if base.is_empty() {
403 return Err(syn::Error::new(
404 field_span,
405 "Q!(): field name is empty before `__` suffix",
406 ));
407 }
408 let base_ident = syn::Ident::new(base, field_span);
409 let value = &q.value;
410 let path = &q.base_path;
411
412 // Most suffixes map directly to a Column method with the value
413 // forwarded unchanged. Some need value-shape massaging (wildcards
414 // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
415 // ISNULL). Unknown suffixes fail the build.
416 let expanded = match suffix {
417 "" | "exact" => quote! {
418 #root::core::Column::eq(#path::#base_ident, #value)
419 },
420 "ne" => quote! {
421 #root::core::Column::ne(#path::#base_ident, #value)
422 },
423 "gt" => quote! {
424 #root::core::Column::gt(#path::#base_ident, #value)
425 },
426 "gte" => quote! {
427 #root::core::Column::gte(#path::#base_ident, #value)
428 },
429 "lt" => quote! {
430 #root::core::Column::lt(#path::#base_ident, #value)
431 },
432 "lte" => quote! {
433 #root::core::Column::lte(#path::#base_ident, #value)
434 },
435 // The escaped-LIKE lookups delegate to the typed Column
436 // methods, which escape the value and pair it with the
437 // *Escaped ops so `%` / `_` match literally on every dialect
438 // (#1257) — the macro no longer owns any pattern wrapping.
439 "iexact" => quote! {
440 #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
441 },
442 "contains" => quote! {
443 #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
444 },
445 "icontains" => quote! {
446 #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
447 },
448 "startswith" => quote! {
449 #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
450 },
451 "istartswith" => quote! {
452 #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
453 },
454 "endswith" => quote! {
455 #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
456 },
457 "iendswith" => quote! {
458 #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
459 },
460 "in" => quote! {
461 #root::core::Column::is_in(#path::#base_ident, #value)
462 },
463 "not_in" => quote! {
464 #root::core::Column::not_in(#path::#base_ident, #value)
465 },
466 "isnull" => {
467 // Must be a bool literal at macro time so we can route to
468 // is_null vs is_not_null without a runtime branch.
469 let b = match value {
470 syn::Expr::Lit(syn::ExprLit {
471 lit: syn::Lit::Bool(b),
472 ..
473 }) => b.value(),
474 _ => {
475 return Err(syn::Error::new_spanned(
476 value,
477 "Q!(): `__isnull` requires a `true` or `false` literal",
478 ));
479 }
480 };
481 if b {
482 quote! { #root::core::Column::is_null(#path::#base_ident) }
483 } else {
484 quote! { #root::core::Column::is_not_null(#path::#base_ident) }
485 }
486 }
487 "between" => {
488 // Accept a tuple literal `(lo, hi)`.
489 let tuple = match value {
490 syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
491 _ => {
492 return Err(syn::Error::new_spanned(
493 value,
494 "Q!(): `__between` requires a tuple literal `(lo, hi)`",
495 ));
496 }
497 };
498 let lo = &tuple.elems[0];
499 let hi = &tuple.elems[1];
500 quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
501 }
502 "regex" => quote! {
503 #root::core::Column::regex(#path::#base_ident, #value)
504 },
505 "iregex" => quote! {
506 #root::core::Column::iregex(#path::#base_ident, #value)
507 },
508 _ => {
509 return Err(syn::Error::new(
510 field_span,
511 format!(
512 "Q!(): unknown lookup suffix `__{}`. Supported: __exact / __iexact / __ne / __gt / __gte / __lt / __lte / __contains / __icontains / __startswith / __istartswith / __endswith / __iendswith / __in / __not_in / __isnull / __between / __regex / __iregex",
513 suffix
514 ),
515 ));
516 }
517 };
518 Ok(expanded)
519}
520
521fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
522 // Default to "./migrations" if invoked without args.
523 let path_str = if input.is_empty() {
524 "./migrations".to_string()
525 } else {
526 let lit: LitStr = syn::parse2(input)?;
527 lit.value()
528 };
529
530 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
531 syn::Error::new(
532 proc_macro2::Span::call_site(),
533 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
534 )
535 })?;
536 let abs = std::path::Path::new(&manifest).join(&path_str);
537
538 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
539 if abs.is_dir() {
540 let read = std::fs::read_dir(&abs).map_err(|e| {
541 syn::Error::new(
542 proc_macro2::Span::call_site(),
543 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
544 )
545 })?;
546 for entry in read.flatten() {
547 let path = entry.path();
548 if !path.is_file() {
549 continue;
550 }
551 if path.extension().and_then(|s| s.to_str()) != Some("json") {
552 continue;
553 }
554 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
555 continue;
556 };
557 entries.push((stem.to_owned(), path));
558 }
559 }
560 entries.sort_by(|a, b| a.0.cmp(&b.0));
561
562 // Compile-time chain validation: read each migration's JSON,
563 // pull `name` and `prev` (file-stem-keyed for the chain check),
564 // and verify every `prev` points to another migration in the
565 // slice. Mismatches between the file stem and the embedded
566 // `name` field — and broken `prev` chains — fail at MACRO
567 // EXPANSION time so a misshapen migration set never compiles.
568 //
569 // This is the v0.4 Slice 5 distinguisher: rustango's JSON
570 // migrations + a Rust proc-macro that reads them is the unique
571 // combo nothing else in the Django-shape Rust camp can match
572 // (Cot's are imperative Rust code, Loco's are SeaORM up/down,
573 // Rwf's are raw SQL — none have a static chain to validate).
574 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
575 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
576 for (stem, path) in &entries {
577 let raw = std::fs::read_to_string(path).map_err(|e| {
578 syn::Error::new(
579 proc_macro2::Span::call_site(),
580 format!(
581 "embed_migrations!: cannot read {} for chain validation: {e}",
582 path.display()
583 ),
584 )
585 })?;
586 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
587 syn::Error::new(
588 proc_macro2::Span::call_site(),
589 format!(
590 "embed_migrations!: {} is not valid JSON: {e}",
591 path.display()
592 ),
593 )
594 })?;
595 let name = json
596 .get("name")
597 .and_then(|v| v.as_str())
598 .ok_or_else(|| {
599 syn::Error::new(
600 proc_macro2::Span::call_site(),
601 format!(
602 "embed_migrations!: {} is missing the `name` field",
603 path.display()
604 ),
605 )
606 })?
607 .to_owned();
608 if name != *stem {
609 return Err(syn::Error::new(
610 proc_macro2::Span::call_site(),
611 format!(
612 "embed_migrations!: file stem `{stem}` does not match the migration's \
613 `name` field `{name}` — rename the file or fix the JSON",
614 ),
615 ));
616 }
617 let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
618 chain_names.push(name.clone());
619 prev_refs.push((name, prev));
620 }
621
622 let name_set: std::collections::HashSet<&str> =
623 chain_names.iter().map(String::as_str).collect();
624 for (name, prev) in &prev_refs {
625 if let Some(p) = prev {
626 if !name_set.contains(p.as_str()) {
627 return Err(syn::Error::new(
628 proc_macro2::Span::call_site(),
629 format!(
630 "embed_migrations!: broken migration chain — `{name}` declares \
631 prev=`{p}` but no migration with that name exists in {}",
632 abs.display()
633 ),
634 ));
635 }
636 }
637 }
638
639 let pairs: Vec<TokenStream2> = entries
640 .iter()
641 .map(|(name, path)| {
642 let path_lit = path.display().to_string();
643 quote! { (#name, ::core::include_str!(#path_lit)) }
644 })
645 .collect();
646
647 Ok(quote! {
648 {
649 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
650 __RUSTANGO_EMBEDDED
651 }
652 })
653}
654
655fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
656 let root = rustango_root();
657 let struct_name = &input.ident;
658
659 let Data::Struct(data) = &input.data else {
660 return Err(syn::Error::new_spanned(
661 struct_name,
662 "Model can only be derived on structs",
663 ));
664 };
665 let Fields::Named(named) = &data.fields else {
666 return Err(syn::Error::new_spanned(
667 struct_name,
668 "Model requires a struct with named fields",
669 ));
670 };
671
672 let container = parse_container_attrs(input)?;
673 let table = container
674 .table
675 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
676 let model_name = struct_name.to_string();
677
678 let collected = collect_fields(named, &table)?;
679
680 // Validate that #[rustango(display = "…")] names a real field.
681 if let Some((ref display, span)) = container.display {
682 if !collected.field_names.iter().any(|n| n == display) {
683 return Err(syn::Error::new(
684 span,
685 format!("`display = \"{display}\"` does not match any field on this struct"),
686 ));
687 }
688 }
689 let display = container.display.map(|(name, _)| name);
690 let app_label = container.app.clone();
691
692 // Validate admin field-name lists against declared field names.
693 // Note: `list_display` is intentionally NOT validated here. As of
694 // v0.32 it may also reference inventory-registered computed
695 // fields (via `register_admin_computed!`) whose existence the
696 // macro can't see at compile time — they're submitted from any
697 // crate that depends on rustango. The runtime list-view resolves
698 // unknown names against the inventory + silently drops the
699 // truly-bogus ones, which is the cheaper trade-off versus
700 // forcing a per-Model attr to opt out.
701 if let Some(admin) = &container.admin {
702 for (label, list) in [
703 ("search_fields", &admin.search_fields),
704 ("readonly_fields", &admin.readonly_fields),
705 ("list_filter", &admin.list_filter),
706 ] {
707 if let Some((names, span)) = list {
708 for name in names {
709 if !collected.field_names.iter().any(|n| n == name) {
710 return Err(syn::Error::new(
711 *span,
712 format!(
713 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
714 ),
715 ));
716 }
717 }
718 }
719 }
720 if let Some((pairs, span)) = &admin.ordering {
721 for (name, _) in pairs {
722 if !collected.field_names.iter().any(|n| n == name) {
723 return Err(syn::Error::new(
724 *span,
725 format!(
726 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
727 ),
728 ));
729 }
730 }
731 }
732 if let Some((groups, span)) = &admin.fieldsets {
733 for (_, fields) in groups {
734 for name in fields {
735 if !collected.field_names.iter().any(|n| n == name) {
736 return Err(syn::Error::new(
737 *span,
738 format!(
739 "`fieldsets`: \"{name}\" is not a declared field on this struct"
740 ),
741 ));
742 }
743 }
744 }
745 }
746 }
747 if let Some(audit) = &container.audit {
748 if let Some((names, span)) = &audit.track {
749 for name in names {
750 if !collected.field_names.iter().any(|n| n == name) {
751 return Err(syn::Error::new(
752 *span,
753 format!(
754 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
755 ),
756 ));
757 }
758 }
759 }
760 }
761
762 // Issue #291 / T2.5 — validate each `default_order` column name
763 // against the model's collected fields. Typos fail at macro-expand
764 // time, not at the database.
765 for (col, _desc, span) in &container.default_order {
766 if !collected.field_names.iter().any(|n| n == col) {
767 return Err(syn::Error::new(
768 *span,
769 format!(
770 "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
771 ),
772 ));
773 }
774 }
775
776 // Build the audit_track list for ModelSchema: None when no audit attr,
777 // Some(empty) when audit present without track, Some(names) when explicit.
778 let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
779 audit
780 .track
781 .as_ref()
782 .map(|(names, _)| names.clone())
783 .unwrap_or_default()
784 });
785
786 // Merge field-level indexes into the container's index list.
787 let mut all_indexes: Vec<IndexAttr> = container.indexes;
788 for field in &named.named {
789 let ident = field.ident.as_ref().expect("named");
790 let col = to_snake_case(&ident.to_string()); // column name fallback
791 // Re-parse field attrs to check for index flag
792 if let Ok(fa) = parse_field_attrs(field) {
793 if fa.index {
794 let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
795 let auto_name = if fa.index_unique {
796 format!("{table}_{col_name}_uq_idx")
797 } else {
798 format!("{table}_{col_name}_idx")
799 };
800 all_indexes.push(IndexAttr {
801 name: fa.index_name.or(Some(auto_name)),
802 columns: vec![col_name],
803 unique: fa.index_unique,
804 method: fa.index_method,
805 where_clause: None,
806 include: Vec::new(),
807 });
808 }
809 }
810 }
811
812 let model_impl = model_impl_tokens(
813 struct_name,
814 &model_name,
815 &table,
816 display.as_deref(),
817 app_label.as_deref(),
818 container.admin.as_ref(),
819 &container.default_order,
820 &collected.field_schemas,
821 collected.soft_delete_column.as_deref(),
822 container.permissions,
823 audit_track_names.as_deref(),
824 &container.m2m,
825 &all_indexes,
826 &container.checks,
827 &container.excludes,
828 &container.composite_fks,
829 &container.generic_fks,
830 container.scope.as_deref(),
831 container.is_view,
832 container.verbose_name.as_deref(),
833 container.verbose_name_plural.as_deref(),
834 container.managed,
835 container.base_manager_name.as_deref(),
836 container.order_with_respect_to.as_deref(),
837 container.proxy,
838 &container.required_db_features,
839 container.required_db_vendor.as_deref(),
840 container.default_related_name.as_deref(),
841 container.db_table_comment.as_deref(),
842 container
843 .get_latest_by
844 .as_ref()
845 .map(|(c, d)| (c.as_str(), *d)),
846 &container.extra_permissions,
847 &container.default_permissions,
848 &container.global_scopes,
849 &container.reverse_has_relations,
850 &container.generic_has_relations,
851 );
852 let module_ident = column_module_ident(struct_name);
853 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
854 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
855 let track_set: Option<std::collections::HashSet<&str>> = audit
856 .track
857 .as_ref()
858 .map(|(names, _)| names.iter().map(String::as_str).collect());
859 collected
860 .column_entries
861 .iter()
862 .filter(|c| {
863 track_set
864 .as_ref()
865 .map_or(true, |s| s.contains(c.name.as_str()))
866 })
867 .collect()
868 });
869 let inherent_impl = inherent_impl_tokens(
870 struct_name,
871 &collected,
872 collected.primary_key.as_ref(),
873 &column_consts,
874 audited_fields.as_deref(),
875 &all_indexes,
876 &container.manager_fns,
877 );
878 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
879 let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
880 let reverse_helpers = reverse_helper_tokens(
881 struct_name,
882 &collected.fk_relations,
883 container.default_related_name.as_deref(),
884 );
885 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
886 let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
887 // Issue #817 — `#[rustango(through(...))]` accessors.
888 let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
889 // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
890 let reverse_has_accessors =
891 reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
892 let generic_fk_accessors = generic_fk_accessor_tokens(
893 struct_name,
894 &container.generic_fks,
895 &collected.column_entries,
896 );
897
898 // Issue #271 / T1.9 — `#[rustango(manager(ext = "FooManagerExt"))]`
899 // emits an empty extension trait so users can add methods via
900 // `impl FooManagerExt for QuerySet<Foo>` without hand-writing the
901 // trait declaration. See `crates/rustango/src/manager.rs` for the
902 // pattern this replaces.
903 let manager_trait = container.manager_ext.as_ref().map(|name| {
904 let model_name_str = struct_name.to_string();
905 let doc = format!(
906 "Custom-Manager extension trait for [`{model_name_str}`]. \
907 Generated by `#[rustango(manager(ext = ...))]`. Add methods \
908 via `impl {name} for QuerySet<{model_name_str}> {{ ... }}`."
909 );
910 quote! {
911 #[doc = #doc]
912 pub trait #name: ::core::marker::Sized {}
913 }
914 });
915
916 Ok(quote! {
917 #model_impl
918 #inherent_impl
919 #from_row_impl
920 #column_module
921 #reverse_helpers
922 #m2m_accessors
923 #generic_m2m_accessors
924 #through_accessors
925 #reverse_has_accessors
926 #generic_fk_accessors
927 #manager_trait
928
929 #root::core::inventory::submit! {
930 #root::core::ModelEntry {
931 schema: <#struct_name as #root::core::Model>::SCHEMA,
932 // `module_path!()` evaluates at the registration site,
933 // so a Model declared in `crate::blog::models` records
934 // `"<crate>::blog::models"` and `resolved_app_label()`
935 // can infer "blog" without an explicit attribute.
936 module_path: ::core::module_path!(),
937 }
938 }
939 })
940}
941
942/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
943/// matches `field_name` against the model's FK fields and, for a
944/// match, decodes the FK target via the parent's macro-generated
945/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
946/// `ForeignKey::Loaded` on `self`.
947///
948/// Always emitted (with empty arms for FK-less models, which
949/// return `Ok(false)` for any field name) so the `T: LoadRelated`
950/// trait bound on `fetch_on` is universally satisfied — users
951/// never have to think about implementing it.
952fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
953 let root = rustango_root();
954 let arms = fk_relations.iter().map(|rel| {
955 let parent_ty = &rel.parent_type;
956 let fk_col = rel.fk_column.as_str();
957 // FK field's Rust ident matches its SQL column name in v0.8
958 // (no `column = "..."` rename ships on FK fields).
959 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
960 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
961 let assign = if rel.nullable {
962 quote! {
963 self.#field_ident = ::core::option::Option::Some(
964 #root::sql::ForeignKey::loaded(_pk, _parent),
965 );
966 }
967 } else {
968 quote! {
969 self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
970 }
971 };
972 quote! {
973 #fk_col => {
974 let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
975 // Audit #451 — multi-hop `select_related("a__b__c")`:
976 // stitch the deeper relation onto this parent first,
977 // decoding it at the accumulated `__next_alias`. The
978 // parent type also impls `LoadRelated`, so this recurses
979 // the FK chain to arbitrary depth.
980 if let ::core::option::Option::Some(__r) = __rest {
981 let _ = #root::sql::LoadRelated::__rustango_load_related(
982 &mut _parent, row, __r, &__next_alias,
983 )?;
984 }
985 // Loud-in-debug, default-in-release: a divergence
986 // between the FK field's declared `K` (drives the
987 // expected `SqlValue::<Variant>`) and the parent's
988 // `__rustango_pk_value` output is a macro-internal
989 // invariant break — surfacing the panic in dev
990 // catches it before users hit silent PK=0 corruption.
991 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
992 #root::core::SqlValue::#variant_ident(v) => v,
993 _other => {
994 ::core::debug_assert!(
995 false,
996 "rustango macro bug: load_related on FK `{}` expected \
997 SqlValue::{} from parent's __rustango_pk_value but got \
998 {:?} — file a bug at https://github.com/ujeenet/rustango",
999 #fk_col,
1000 ::core::stringify!(#variant_ident),
1001 _other,
1002 );
1003 #default_expr
1004 }
1005 };
1006 #assign
1007 ::core::result::Result::Ok(true)
1008 }
1009 }
1010 });
1011 quote! {
1012 #[cfg(feature = "postgres")]
1013 impl #root::sql::LoadRelated for #struct_name {
1014 #[allow(unused_variables)]
1015 fn __rustango_load_related(
1016 &mut self,
1017 row: &#root::sql::sqlx::postgres::PgRow,
1018 field_name: &str,
1019 alias: &str,
1020 ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1021 // Audit #451 — split the multi-hop path: `base` is the FK
1022 // on THIS model, `__rest` (if any) is the remaining chain
1023 // to stitch onto the loaded parent. `__next_alias` is the
1024 // accumulated join alias the parent's columns live under
1025 // (`{alias}__{next-hop}`), matching `lower_select_related`.
1026 let (__base, __rest): (&str, ::core::option::Option<&str>) =
1027 match field_name.split_once("__") {
1028 ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1029 ::core::option::Option::None => (field_name, ::core::option::Option::None),
1030 };
1031 let __next_alias: ::std::string::String = match __rest {
1032 ::core::option::Option::Some(__r) => {
1033 let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1034 ::std::format!("{}__{}", alias, __rb)
1035 }
1036 ::core::option::Option::None => ::std::string::String::new(),
1037 };
1038 match __base {
1039 #( #arms )*
1040 _ => ::core::result::Result::Ok(false),
1041 }
1042 }
1043 }
1044 }
1045}
1046
1047/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1048/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1049/// which expands to a `LoadRelatedMy` impl when rustango is built with
1050/// the `mysql` feature, and to nothing otherwise. The decoded parent
1051/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1052/// decoder, also batch8) so the dual emission is symmetric across
1053/// backends.
1054fn load_related_impl_my_tokens(
1055 struct_name: &syn::Ident,
1056 fk_relations: &[FkRelation],
1057) -> TokenStream2 {
1058 let root = rustango_root();
1059 let arms = fk_relations.iter().map(|rel| {
1060 let parent_ty = &rel.parent_type;
1061 let fk_col = rel.fk_column.as_str();
1062 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1063 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1064 let assign = if rel.nullable {
1065 quote! {
1066 __self.#field_ident = ::core::option::Option::Some(
1067 #root::sql::ForeignKey::loaded(_pk, _parent),
1068 );
1069 }
1070 } else {
1071 quote! {
1072 __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1073 }
1074 };
1075 // `self` IS hygiene-tracked through macro_rules — emitted from
1076 // a different context than the `&mut self` parameter inside
1077 // the macro_rules-expanded fn. Pass it through as `__self`
1078 // and let the macro_rules rebind it to the receiver.
1079 quote! {
1080 #fk_col => {
1081 let mut _parent: #parent_ty =
1082 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1083 // Audit #451 — multi-hop: stitch the deeper relation onto
1084 // the parent at the accumulated alias (see PG twin).
1085 if let ::core::option::Option::Some(__r) = __rest {
1086 let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1087 &mut _parent, row, __r, &__next_alias,
1088 )?;
1089 }
1090 // See note in `load_related_impl_tokens` (PG twin) —
1091 // the same loud-in-debug invariant guard.
1092 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1093 #root::core::SqlValue::#variant_ident(v) => v,
1094 _other => {
1095 ::core::debug_assert!(
1096 false,
1097 "rustango macro bug: load_related on FK `{}` expected \
1098 SqlValue::{} from parent's __rustango_pk_value but got \
1099 {:?} — file a bug at https://github.com/ujeenet/rustango",
1100 #fk_col,
1101 ::core::stringify!(#variant_ident),
1102 _other,
1103 );
1104 #default_expr
1105 }
1106 };
1107 #assign
1108 ::core::result::Result::Ok(true)
1109 }
1110 }
1111 });
1112 quote! {
1113 #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1114 #( #arms )*
1115 });
1116 }
1117}
1118
1119/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1120/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1121/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1122fn load_related_impl_sqlite_tokens(
1123 struct_name: &syn::Ident,
1124 fk_relations: &[FkRelation],
1125) -> TokenStream2 {
1126 let root = rustango_root();
1127 let arms = fk_relations.iter().map(|rel| {
1128 let parent_ty = &rel.parent_type;
1129 let fk_col = rel.fk_column.as_str();
1130 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1131 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1132 let assign = if rel.nullable {
1133 quote! {
1134 __self.#field_ident = ::core::option::Option::Some(
1135 #root::sql::ForeignKey::loaded(_pk, _parent),
1136 );
1137 }
1138 } else {
1139 quote! {
1140 __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1141 }
1142 };
1143 quote! {
1144 #fk_col => {
1145 let mut _parent: #parent_ty =
1146 <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1147 // Audit #451 — multi-hop: stitch the deeper relation onto
1148 // the parent at the accumulated alias (see PG twin).
1149 if let ::core::option::Option::Some(__r) = __rest {
1150 let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1151 &mut _parent, row, __r, &__next_alias,
1152 )?;
1153 }
1154 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1155 #root::core::SqlValue::#variant_ident(v) => v,
1156 _other => {
1157 ::core::debug_assert!(
1158 false,
1159 "rustango macro bug: load_related on FK `{}` expected \
1160 SqlValue::{} from parent's __rustango_pk_value but got \
1161 {:?} — file a bug at https://github.com/ujeenet/rustango",
1162 #fk_col,
1163 ::core::stringify!(#variant_ident),
1164 _other,
1165 );
1166 #default_expr
1167 }
1168 };
1169 #assign
1170 ::core::result::Result::Ok(true)
1171 }
1172 }
1173 });
1174 quote! {
1175 #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1176 #( #arms )*
1177 });
1178 }
1179}
1180
1181/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1182/// matches `field_name` against the model's FK fields and returns
1183/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1184/// group children by parent PK.
1185///
1186/// Always emitted (with `_ => None` for FK-less models) so the
1187/// trait bound on `fetch_with_prefetch` is universally satisfied.
1188fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1189 let root = rustango_root();
1190 let arms = fk_relations.iter().map(|rel| {
1191 let fk_col = rel.fk_column.as_str();
1192 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1193 if rel.pk_kind == DetectedKind::I64 {
1194 // i64 FK — return the stored PK so prefetch_related can
1195 // group children by it. Nullable variant unwraps via
1196 // `as_ref().map(...)`: an unset (NULL) FK column yields
1197 // `None` and that child sits out of the grouping (correct
1198 // semantics — it has no parent to attach to).
1199 if rel.nullable {
1200 quote! {
1201 #fk_col => self.#field_ident
1202 .as_ref()
1203 .map(|fk| #root::sql::ForeignKey::pk(fk)),
1204 }
1205 } else {
1206 quote! {
1207 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1208 }
1209 }
1210 } else {
1211 // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1212 // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1213 // i64-keyed grouping path — the trait signature is
1214 // `Option<i64>` and a non-i64 PK can't lower into it.
1215 // The FK still works for everything else (CRUD, lazy
1216 // load via `.get()`, select_related JOINs); only the
1217 // bulk prefetch grouper needs the integer key.
1218 quote! {
1219 #fk_col => ::core::option::Option::None,
1220 }
1221 }
1222 });
1223 // PK-type-agnostic version: every FK arm emits an
1224 // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1225 // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1226 // opt OUT of the legacy i64 method (it returns None) but opt IN
1227 // here.
1228 let value_arms = fk_relations.iter().map(|rel| {
1229 let fk_col = rel.fk_column.as_str();
1230 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1231 if rel.nullable {
1232 quote! {
1233 #fk_col => self.#field_ident
1234 .as_ref()
1235 .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1236 #root::sql::ForeignKey::pk(fk)
1237 )),
1238 }
1239 } else {
1240 quote! {
1241 #fk_col => ::core::option::Option::Some(
1242 ::core::convert::Into::<#root::core::SqlValue>::into(
1243 self.#field_ident.pk()
1244 )
1245 ),
1246 }
1247 }
1248 });
1249 quote! {
1250 impl #root::sql::FkPkAccess for #struct_name {
1251 #[allow(unused_variables)]
1252 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1253 match field_name {
1254 #( #arms )*
1255 _ => ::core::option::Option::None,
1256 }
1257 }
1258 #[allow(unused_variables)]
1259 fn __rustango_fk_pk_value(
1260 &self,
1261 field_name: &str,
1262 ) -> ::core::option::Option<#root::core::SqlValue> {
1263 match field_name {
1264 #( #value_arms )*
1265 _ => ::core::option::Option::None,
1266 }
1267 }
1268 }
1269 }
1270}
1271
1272/// For every `ForeignKey<Parent>` field on `Child`, emit
1273/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1274/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
1275/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
1276/// — the canonical reverse-FK fetch. One round trip, no N+1.
1277///
1278/// **PG-only emission**: the accessor is bounded on
1279/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1280/// both of which are gated behind the `postgres` cargo feature. The
1281/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1282/// model derive itself compiles on tri-dialect / sqlite-only
1283/// downstream builds — the accessor just isn't materialised. A tri-
1284/// dialect `_set_pool` variant is a separate follow-up.
1285fn reverse_helper_tokens(
1286 child_ident: &syn::Ident,
1287 fk_relations: &[FkRelation],
1288 default_related_name: Option<&str>,
1289) -> TokenStream2 {
1290 let root = rustango_root();
1291 if fk_relations.is_empty() {
1292 return TokenStream2::new();
1293 }
1294 // Method-name resolution per FK (issue #816 + follow-up):
1295 // 1. Field-level `#[rustango(related_name = "...")]` on the FK
1296 // itself — wins over everything else. Django's
1297 // `ForeignKey(related_name="...")`.
1298 // 2. Container-level `default_related_name = "..."` on the
1299 // child — Django's `class Meta: default_related_name`.
1300 // Applies to every FK on this model that didn't override.
1301 // 3. Fallback: `<child_snake>_set` — Django's `<child>_set`
1302 // convention. `Post` → `post_set`, `BlogComment` →
1303 // `blog_comment_set`. Avoids English-plural edge cases.
1304 //
1305 // The PG-on-executor variant keeps the resolved name; the
1306 // tri-dialect `_pool` variant appends `_pool` to it (matches the
1307 // framework's convention for the `&Pool` flavor of every helper).
1308 let default_pg_suffix = default_related_name
1309 .map(str::to_owned)
1310 .unwrap_or_else(|| format!("{}_set", to_snake_case(&child_ident.to_string())));
1311 let impls = fk_relations.iter().map(|rel| {
1312 let pg_suffix = rel
1313 .related_name
1314 .clone()
1315 .unwrap_or_else(|| default_pg_suffix.clone());
1316 let pool_suffix = format!("{}_pool", pg_suffix);
1317 let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1318 let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1319 let parent_ty = &rel.parent_type;
1320 let fk_col = rel.fk_column.as_str();
1321 let doc = format!(
1322 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1323 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1324 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1325 further `{child_ident}::objects()` filters via direct queryset use."
1326 );
1327 let pool_doc = format!(
1328 "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1329 [`#root::sql::Pool`] and dispatches per backend so the \
1330 reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1331 Use this from framework code that holds a `&Pool` (admin, \
1332 tenancy resolver, viewset handlers); reach for the executor- \
1333 bound variant when you already have a typed `sqlx::Executor`."
1334 );
1335 quote! {
1336 #[cfg(feature = "postgres")]
1337 impl #parent_ty {
1338 #[doc = #doc]
1339 ///
1340 /// # Errors
1341 /// Returns [`#root::sql::ExecError`] for SQL-writing
1342 /// or driver failures.
1343 pub async fn #pg_method_ident<'_c, _E>(
1344 &self,
1345 _executor: _E,
1346 ) -> ::core::result::Result<
1347 ::std::vec::Vec<#child_ident>,
1348 #root::sql::ExecError,
1349 >
1350 where
1351 _E: #root::sql::sqlx::Executor<
1352 '_c,
1353 Database = #root::sql::sqlx::Postgres,
1354 >,
1355 {
1356 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1357 #root::query::QuerySet::<#child_ident>::new()
1358 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1359 .fetch_on(_executor)
1360 .await
1361 }
1362 }
1363
1364 impl #parent_ty {
1365 #[doc = #pool_doc]
1366 ///
1367 /// # Errors
1368 /// Returns [`#root::sql::ExecError`] for SQL-writing
1369 /// or driver failures.
1370 pub async fn #pool_method_ident(
1371 &self,
1372 pool: &#root::sql::Pool,
1373 ) -> ::core::result::Result<
1374 ::std::vec::Vec<#child_ident>,
1375 #root::sql::ExecError,
1376 > {
1377 use #root::sql::FetcherPool as _;
1378 let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1379 #root::query::QuerySet::<#child_ident>::new()
1380 .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1381 .fetch(pool)
1382 .await
1383 }
1384 }
1385 }
1386 });
1387 quote! { #( #impls )* }
1388}
1389
1390/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1391/// relation declared on the model.
1392/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1393/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1394///
1395/// Closes #239 + #240 — the Django-shape `comment.content_object` /
1396/// `comment.content_object = post` ergonomics on top of the existing
1397/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1398///
1399/// `column_entries` is passed so we can resolve each `ct_column` /
1400/// `pk_column` SQL name back to its Rust field ident — the macro
1401/// only sees the column-side strings in the attribute, but the
1402/// emitted accessor needs to read the actual struct field.
1403fn generic_fk_accessor_tokens(
1404 struct_name: &syn::Ident,
1405 generic_fks: &[GenericFkAttr],
1406 column_entries: &[ColumnEntry],
1407) -> TokenStream2 {
1408 let root = rustango_root();
1409 if generic_fks.is_empty() {
1410 return TokenStream2::new();
1411 }
1412 let methods = generic_fks.iter().filter_map(|gfk| {
1413 // Resolve `ct_column` + `pk_column` to the struct's Rust
1414 // field idents. A typo (column name doesn't match any field)
1415 // emits no method for that registration — the user will see
1416 // the compiler reject the SCHEMA literal anyway, so there's
1417 // a clear error path without us double-reporting.
1418 let ct_ident = column_entries
1419 .iter()
1420 .find(|c| c.column == gfk.ct_column)
1421 .map(|c| c.ident.clone())?;
1422 let pk_ident = column_entries
1423 .iter()
1424 .find(|c| c.column == gfk.pk_column)
1425 .map(|c| c.ident.clone())?;
1426
1427 let accessor_ident =
1428 syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1429 let setter_ident =
1430 syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1431 let name_literal = gfk.name.as_str();
1432
1433 Some(quote! {
1434 #[doc = concat!(
1435 "Resolve the polymorphic `",
1436 #name_literal,
1437 "` relation. Reads `self.",
1438 stringify!(#ct_ident),
1439 "` + `self.",
1440 stringify!(#pk_ident),
1441 "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1442 "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1443 #name_literal,
1444 "\", ...))]`."
1445 )]
1446 pub async fn #accessor_ident(
1447 &self,
1448 pool: &#root::sql::Pool,
1449 ) -> ::core::result::Result<
1450 ::core::option::Option<#root::__serde_json::Value>,
1451 #root::sql::ExecError,
1452 > {
1453 let gfk = #root::contenttypes::GenericForeignKey::new(
1454 self.#ct_ident as i64,
1455 self.#pk_ident as i64,
1456 );
1457 gfk.get_object(pool).await
1458 }
1459
1460 #[doc = concat!(
1461 "Set the polymorphic `",
1462 #name_literal,
1463 "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1464 stringify!(#ct_ident),
1465 "` and `self.",
1466 stringify!(#pk_ident),
1467 "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1468 #name_literal,
1469 "\", ...))]`."
1470 )]
1471 pub async fn #setter_ident<T: #root::core::Model>(
1472 &mut self,
1473 pool: &#root::sql::Pool,
1474 target_pk: i64,
1475 ) -> ::core::result::Result<(), #root::sql::ExecError> {
1476 let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1477 pool,
1478 target_pk,
1479 ).await?;
1480 self.#ct_ident = gfk.content_type_id as _;
1481 self.#pk_ident = gfk.object_pk as _;
1482 ::core::result::Result::Ok(())
1483 }
1484 })
1485 });
1486 quote! {
1487 impl #struct_name {
1488 #( #methods )*
1489 }
1490 }
1491}
1492
1493fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1494 let root = rustango_root();
1495 if m2m_relations.is_empty() {
1496 return TokenStream2::new();
1497 }
1498 let methods = m2m_relations.iter().map(|rel| {
1499 let method_name = format!("{}_m2m", rel.name);
1500 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1501 let through = rel.through.as_str();
1502 let src_col = rel.src.as_str();
1503 let dst_col = rel.dst.as_str();
1504 quote! {
1505 pub fn #method_ident(&self) -> #root::sql::M2MManager {
1506 #root::sql::M2MManager {
1507 src_pk: self.__rustango_pk_value(),
1508 through: #through,
1509 src_col: #src_col,
1510 dst_col: #dst_col,
1511 }
1512 }
1513 }
1514 });
1515 quote! {
1516 impl #struct_name {
1517 #( #methods )*
1518 }
1519 }
1520}
1521
1522/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1523/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1524fn generic_m2m_accessor_tokens(
1525 struct_name: &syn::Ident,
1526 relations: &[GenericM2MAttr],
1527) -> TokenStream2 {
1528 let root = rustango_root();
1529 if relations.is_empty() {
1530 return TokenStream2::new();
1531 }
1532 let methods = relations.iter().map(|rel| {
1533 let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1534 let through = rel.through.as_str();
1535 let pk_col = rel.pk_column.as_str();
1536 let ct_col = rel.ct_column.as_str();
1537 let related_col = rel.related_column.as_str();
1538 quote! {
1539 pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1540 #root::sql::GenericM2MManager {
1541 src_pk: self.__rustango_pk_value(),
1542 src_schema: <Self as #root::core::Model>::SCHEMA,
1543 through: #through,
1544 pk_col: #pk_col,
1545 ct_col: #ct_col,
1546 dst_col: #related_col,
1547 }
1548 }
1549 }
1550 });
1551 quote! {
1552 impl #struct_name {
1553 #( #methods )*
1554 }
1555 }
1556}
1557
1558/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1559/// associated functions for each `#[rustango(reverse_has(...))]`
1560/// attribute. Issue #830.
1561///
1562/// The two emitted functions return ready-to-use `WhereExpr` nodes
1563/// that downstream callers drop into
1564/// `QuerySet::<Self>::where_raw(...)`:
1565///
1566/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1567/// <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1568/// `whereHas` parity (without the closure-style sub-predicate
1569/// refinement — that's a follow-up; users can layer additional
1570/// predicates by constructing the SelectQuery themselves and
1571/// calling `where_raw(exists(query))` from `crate::core::subquery`).
1572/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1573/// `whereDoesntHave` parity.
1574///
1575/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1576/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1577/// machinery threads the outer-table reference through automatically
1578/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1579fn reverse_has_accessor_tokens(
1580 struct_name: &syn::Ident,
1581 reverse_has_relations: &[ReverseHasAttr],
1582) -> TokenStream2 {
1583 let root = rustango_root();
1584 if reverse_has_relations.is_empty() {
1585 return TokenStream2::new();
1586 }
1587 let methods = reverse_has_relations.iter().map(|rel| {
1588 let exists_name = format!("{}_exists_expr", rel.name);
1589 let not_exists_name = format!("{}_not_exists_expr", rel.name);
1590 let count_name = format!("{}_count", rel.name);
1591 let fetch_name = format!("{}_fetch", rel.name);
1592 let first_name = format!("{}_first", rel.name);
1593 let pluck_name = format!("{}_pluck", rel.name);
1594 let accessor_name = rel.name.as_str();
1595 let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1596 let not_exists_ident = syn::Ident::new(¬_exists_name, struct_name.span());
1597 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1598 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1599 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1600 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1601 let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1602 let child = &rel.child;
1603 let child_fk_column = rel.child_fk_column.as_str();
1604 let self_pk_column = rel.self_pk_column.as_str();
1605 let exists_doc = format!(
1606 "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1607 FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1608 Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1609 filter to {struct_name}s with at least one matching child.",
1610 );
1611 let not_exists_doc = format!(
1612 "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1613 (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1614 <outer>.{self_pk_column})`. Drop into \
1615 `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1616 {struct_name}s with **no** matching child.",
1617 );
1618 let count_doc = format!(
1619 "Eloquent `$model->{name}->count()` analog — returns \
1620 the number of `{child}` rows whose `{child_fk_column}` \
1621 matches this `{struct_name}` instance's primary key. \
1622 Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1623 {child_fk_column} = <self.pk>`.",
1624 name = rel.name,
1625 );
1626 let accessor_doc = format!(
1627 "Eloquent `$model->{name}` accessor — returns a \
1628 `QuerySet<{child}>` filtered to rows whose \
1629 `{child_fk_column}` matches this `{struct_name}` \
1630 instance's primary key. **Chainable**: compose `.filter()` \
1631 / `.order_by()` / `.limit()` etc. on top, then call \
1632 `.fetch(&pool)` (the QuerySet trait method) when \
1633 done. For the simple \"fetch all\" hot path with no \
1634 further composition, prefer the bare-name \
1635 `{name}_fetch(&pool)` companion.",
1636 name = rel.name,
1637 );
1638 let fetch_doc = format!(
1639 "Eloquent `$model->{name}->get()` — bare-name hot-path \
1640 over `{name}(&self).fetch(&pool)`. Use this when \
1641 you don't need further `.filter()` / `.order_by()` \
1642 composition; falls back to the chainable accessor when \
1643 you do. Avoids the `_pool` suffix on the most common \
1644 call-site shape.",
1645 name = rel.name,
1646 );
1647 quote! {
1648 #[doc = #accessor_doc]
1649 pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1650 #root::query::QuerySet::<#child>::new()
1651 .filter(#child_fk_column, self.__rustango_pk_value())
1652 }
1653
1654 #[doc = #fetch_doc]
1655 pub async fn #fetch_ident(
1656 &self,
1657 pool: &#root::sql::Pool,
1658 ) -> ::core::result::Result<
1659 ::std::vec::Vec<#child>,
1660 #root::sql::ExecError,
1661 > {
1662 use #root::sql::FetcherPool as _;
1663 self.#accessor_ident().fetch(pool).await
1664 }
1665
1666 /// Eloquent `$model->relation->first()` / `hasOne`
1667 /// semantics — bare-name shortcut over
1668 /// `self.<name>().first(&pool)`. Returns `None` when no
1669 /// child rows match. Useful when the relation is
1670 /// nominally many-to-one in shape but at most one row is
1671 /// expected (latest comment, primary tag, etc.).
1672 pub async fn #first_ident(
1673 &self,
1674 pool: &#root::sql::Pool,
1675 ) -> ::core::result::Result<
1676 ::core::option::Option<#child>,
1677 #root::sql::ExecError,
1678 > {
1679 self.#accessor_ident().first(pool).await
1680 }
1681
1682 /// Eloquent `$model->relation->pluck($col)` — project a
1683 /// single column from the child rows into a `Vec<U>`.
1684 /// Skips the typed `Child` decode, which is cheaper when
1685 /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1686 pub async fn #pluck_ident<U>(
1687 &self,
1688 col: &'static str,
1689 pool: &#root::sql::Pool,
1690 ) -> ::core::result::Result<
1691 ::std::vec::Vec<U>,
1692 #root::sql::ExecError,
1693 >
1694 where
1695 U: #root::sql::MaybePgScalar
1696 + #root::sql::MaybeMyScalar
1697 + #root::sql::MaybeSqliteScalar
1698 + ::core::marker::Send
1699 + ::core::marker::Unpin,
1700 {
1701 self.#accessor_ident()
1702 .values_list_flat(col)
1703 .fetch::<U>(pool)
1704 .await
1705 }
1706
1707 #[doc = #exists_doc]
1708 pub fn #exists_ident() -> #root::core::WhereExpr {
1709 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1710 let child_schema =
1711 <#child as #root::core::Model>::SCHEMA;
1712 let inner = SelectQuery {
1713 where_clause: WhereExpr::ExprCompare {
1714 lhs: Expr::Column(#child_fk_column),
1715 op: Op::Eq,
1716 rhs: Expr::OuterRef(#self_pk_column),
1717 },
1718 ..SelectQuery::new(child_schema)
1719 };
1720 WhereExpr::Exists(::std::boxed::Box::new(inner))
1721 }
1722
1723 #[doc = #not_exists_doc]
1724 pub fn #not_exists_ident() -> #root::core::WhereExpr {
1725 use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1726 let child_schema =
1727 <#child as #root::core::Model>::SCHEMA;
1728 let inner = SelectQuery {
1729 where_clause: WhereExpr::ExprCompare {
1730 lhs: Expr::Column(#child_fk_column),
1731 op: Op::Eq,
1732 rhs: Expr::OuterRef(#self_pk_column),
1733 },
1734 ..SelectQuery::new(child_schema)
1735 };
1736 WhereExpr::NotExists(::std::boxed::Box::new(inner))
1737 }
1738
1739 #[doc = #count_doc]
1740 pub async fn #count_ident(
1741 &self,
1742 pool: &#root::sql::Pool,
1743 ) -> ::core::result::Result<
1744 i64,
1745 #root::sql::ExecError,
1746 > {
1747 use #root::sql::CounterPool as _;
1748 #root::query::QuerySet::<#child>::new()
1749 .filter(#child_fk_column, self.__rustango_pk_value())
1750 .count(pool)
1751 .await
1752 }
1753 }
1754 });
1755 quote! {
1756 impl #struct_name {
1757 #( #methods )*
1758 }
1759 }
1760}
1761
1762/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1763/// `#[rustango(through(...))]` attribute. Issue #817.
1764///
1765/// Each accessor builds a correlated subquery via
1766/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1767/// intermediate table, filters on the FK column pointing at this
1768/// model, and projects the intermediate PK. The outer queryset
1769/// filters the far model by its FK-to-intermediate column being in
1770/// that set.
1771///
1772/// The returned `QuerySet<Far>` is **chainable** — the subquery
1773/// lives inside a `where_raw` clause so the user's later
1774/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1775/// disturb it.
1776///
1777/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1778/// SQLite — no LATERAL or backend-specific syntax involved.
1779fn through_accessor_tokens(
1780 struct_name: &syn::Ident,
1781 through_relations: &[ThroughAttr],
1782) -> TokenStream2 {
1783 let root = rustango_root();
1784 if through_relations.is_empty() {
1785 return TokenStream2::new();
1786 }
1787 let methods = through_relations.iter().map(|rel| {
1788 let method_name = format!("{}_through", rel.name);
1789 let count_name = format!("{}_through_count", rel.name);
1790 let fetch_name = format!("{}_through_fetch", rel.name);
1791 let first_name = format!("{}_through_first", rel.name);
1792 let pluck_name = format!("{}_through_pluck", rel.name);
1793 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1794 let count_ident = syn::Ident::new(&count_name, struct_name.span());
1795 let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1796 let first_ident = syn::Ident::new(&first_name, struct_name.span());
1797 let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1798 let far = &rel.far;
1799 let intermediate = &rel.intermediate;
1800 let far_fk_column = rel.far_fk_column.as_str();
1801 let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1802 let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1803 let doc = format!(
1804 "Eloquent `hasManyThrough` accessor — returns a \
1805 `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1806 instance through the intermediate `{intermediate}` table. \
1807 Generated SQL shape: \
1808 `… WHERE {far_fk_column} IN (SELECT \
1809 {intermediate_pk_column} FROM <{intermediate}> WHERE \
1810 {intermediate_fk_column} = self.pk)`. Chainable like any \
1811 other QuerySet — compose `.filter()` / `.order_by()` / \
1812 `.limit()` etc. on top.",
1813 );
1814 let count_doc = format!(
1815 "Eloquent `$model->{name}->count()` analog for the \
1816 through-relation — returns the number of `{far}` rows \
1817 reachable through `{intermediate}`. Equivalent to \
1818 `self.{name}_through().count(pool)` but spelled \
1819 as a bare instance method for parity with the \
1820 `reverse_has` `<name>_count` shape.",
1821 name = rel.name,
1822 );
1823 quote! {
1824 #[doc = #doc]
1825 pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1826 use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1827 let intermediate_schema =
1828 <#intermediate as #root::core::Model>::SCHEMA;
1829 let sub = SelectQuery {
1830 where_clause: WhereExpr::Predicate(Filter {
1831 column: #intermediate_fk_column,
1832 op: Op::Eq,
1833 value: self.__rustango_pk_value(),
1834 }),
1835 projection: ::core::option::Option::Some(
1836 ::std::vec![#intermediate_pk_column],
1837 ),
1838 ..SelectQuery::new(intermediate_schema)
1839 };
1840 #root::query::QuerySet::<#far>::new().where_raw(
1841 WhereExpr::InSubquery {
1842 column: #far_fk_column,
1843 negated: false,
1844 subquery: ::std::boxed::Box::new(sub),
1845 },
1846 )
1847 }
1848
1849 #[doc = #count_doc]
1850 pub async fn #count_ident(
1851 &self,
1852 pool: &#root::sql::Pool,
1853 ) -> ::core::result::Result<
1854 i64,
1855 #root::sql::ExecError,
1856 > {
1857 use #root::sql::CounterPool as _;
1858 self.#method_ident().count(pool).await
1859 }
1860
1861 /// Eloquent `$model->relation->get()` for the
1862 /// through-relation — bare-name hot-path over
1863 /// `self.<name>_through().fetch(&pool)`. Use this
1864 /// when you don't need further `.filter()` /
1865 /// `.order_by()` composition; falls back to the
1866 /// chainable accessor when you do. Avoids the `_pool`
1867 /// suffix on the most common call-site shape.
1868 pub async fn #fetch_ident(
1869 &self,
1870 pool: &#root::sql::Pool,
1871 ) -> ::core::result::Result<
1872 ::std::vec::Vec<#far>,
1873 #root::sql::ExecError,
1874 > {
1875 use #root::sql::FetcherPool as _;
1876 self.#method_ident().fetch(pool).await
1877 }
1878
1879 /// Eloquent `hasOneThrough` analog — bare-name shortcut
1880 /// over `self.<name>_through().first(&pool)`. Returns
1881 /// `None` when no far rows are reachable through the
1882 /// intermediate. Useful when at most one row is
1883 /// expected (latest comment by country, primary tag,
1884 /// etc.).
1885 pub async fn #first_ident(
1886 &self,
1887 pool: &#root::sql::Pool,
1888 ) -> ::core::result::Result<
1889 ::core::option::Option<#far>,
1890 #root::sql::ExecError,
1891 > {
1892 self.#method_ident().first(pool).await
1893 }
1894
1895 /// Pluck a single column from the far rows into `Vec<U>`
1896 /// — cheaper than the typed `<Far>` decode when only one
1897 /// scalar column is needed.
1898 pub async fn #pluck_ident<U>(
1899 &self,
1900 col: &'static str,
1901 pool: &#root::sql::Pool,
1902 ) -> ::core::result::Result<
1903 ::std::vec::Vec<U>,
1904 #root::sql::ExecError,
1905 >
1906 where
1907 U: #root::sql::MaybePgScalar
1908 + #root::sql::MaybeMyScalar
1909 + #root::sql::MaybeSqliteScalar
1910 + ::core::marker::Send
1911 + ::core::marker::Unpin,
1912 {
1913 self.#method_ident()
1914 .values_list_flat(col)
1915 .fetch::<U>(pool)
1916 .await
1917 }
1918 }
1919 });
1920 quote! {
1921 impl #struct_name {
1922 #( #methods )*
1923 }
1924 }
1925}
1926
1927struct ColumnEntry {
1928 /// The struct field ident, used both for the inherent const name on
1929 /// the model and for the inner column type's name.
1930 ident: syn::Ident,
1931 /// The struct's field type, used as `Column::Value`.
1932 value_ty: Type,
1933 /// Rust-side field name (e.g. `"id"`).
1934 name: String,
1935 /// SQL-side column name (e.g. `"user_id"`).
1936 column: String,
1937 /// `#root::core::FieldType::I64` etc.
1938 field_type_tokens: TokenStream2,
1939}
1940
1941struct CollectedFields {
1942 field_schemas: Vec<TokenStream2>,
1943 from_row_inits: Vec<TokenStream2>,
1944 /// Aliased counterparts of `from_row_inits` — read columns via
1945 /// `format!("{prefix}__{col}")` aliases so a Model can be
1946 /// decoded from a JOINed row's projected target columns.
1947 from_aliased_row_inits: Vec<TokenStream2>,
1948 /// Static column-name list — used by the simple insert path
1949 /// (no `Auto<T>` fields). Aligned with `insert_values`.
1950 insert_columns: Vec<TokenStream2>,
1951 /// Static `Into<SqlValue>` expressions, one per field. Aligned
1952 /// with `insert_columns`. Used by the simple insert path only.
1953 insert_values: Vec<TokenStream2>,
1954 /// Per-field push expressions for the dynamic (Auto-aware)
1955 /// insert path. Each statement either unconditionally pushes
1956 /// `(column, value)` or, for an `Auto<T>` field, conditionally
1957 /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
1958 insert_pushes: Vec<TokenStream2>,
1959 /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
1960 /// when `has_auto == false`.
1961 returning_cols: Vec<TokenStream2>,
1962 /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
1963 /// field. Run after `insert_returning` to populate the model.
1964 auto_assigns: Vec<TokenStream2>,
1965 /// `(ident, column_literal)` pairs for every Auto field. Used by
1966 /// the bulk_insert codegen to rebuild assigns against `_row_mut`
1967 /// instead of `self`.
1968 auto_field_idents: Vec<(syn::Ident, String)>,
1969 /// #1028 — `(ident, column)` for each `generated_as` field. Drives
1970 /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
1971 /// back into the struct after insert (MySQL has no RETURNING → the
1972 /// field stays at its placeholder, deferred, matching Django 6.0).
1973 generated_field_idents: Vec<(syn::Ident, String)>,
1974 /// Inner `T` of the first `Auto<T>` field, for the MySQL
1975 /// `LAST_INSERT_ID()` assignment in `AssignAutoPkPool`.
1976 first_auto_value_ty: Option<Type>,
1977 /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
1978 /// by the all-Auto-Unset bulk path (Auto cols dropped from
1979 /// `columns`).
1980 bulk_pushes_no_auto: Vec<TokenStream2>,
1981 /// Bulk-insert per-row pushes for **all fields including Auto**.
1982 /// Used by the all-Auto-Set bulk path (Auto col included with the
1983 /// caller-supplied value).
1984 bulk_pushes_all: Vec<TokenStream2>,
1985 /// Column-name literals for non-Auto fields only (paired with
1986 /// `bulk_pushes_no_auto`).
1987 bulk_columns_no_auto: Vec<TokenStream2>,
1988 /// Column-name literals for every field including Auto (paired
1989 /// with `bulk_pushes_all`).
1990 bulk_columns_all: Vec<TokenStream2>,
1991 /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
1992 /// + the loop that asserts every row matches. One pair per Auto
1993 /// field. Empty when `has_auto == false`.
1994 bulk_auto_uniformity: Vec<TokenStream2>,
1995 /// Identifier of the first Auto field, used as the witness for
1996 /// "all rows agree on Set vs Unset". Set only when `has_auto`.
1997 first_auto_ident: Option<syn::Ident>,
1998 /// `true` if any field on the struct is `Auto<T>`.
1999 has_auto: bool,
2000 /// `true` when the primary-key field's Rust type is `Auto<T>`.
2001 /// Gates `save()` codegen — only Auto PKs let us infer
2002 /// insert-vs-update from the in-memory value.
2003 pk_is_auto: bool,
2004 /// `Assignment` constructors for every non-PK column. Drives the
2005 /// UPDATE branch of `save()`.
2006 update_assignments: Vec<TokenStream2>,
2007 /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2008 /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2009 upsert_update_columns: Vec<TokenStream2>,
2010 primary_key: Option<(syn::Ident, String)>,
2011 column_entries: Vec<ColumnEntry>,
2012 /// Rust-side field names, in declaration order. Used to validate
2013 /// container attributes like `display = "…"`.
2014 field_names: Vec<String>,
2015 /// FK fields on this child model. Drives the reverse-relation
2016 /// helper emit — for each FK, the macro adds an inherent
2017 /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2018 /// method on the parent type.
2019 fk_relations: Vec<FkRelation>,
2020 /// SQL column name of the `#[rustango(soft_delete)]` field, if
2021 /// the model has one. Drives emission of the `soft_delete_on` /
2022 /// `restore_on` inherent methods. At most one such column per
2023 /// model is allowed; collect_fields rejects duplicates.
2024 soft_delete_column: Option<String>,
2025 /// Rust field ident of the `#[rustango(soft_delete)]` field —
2026 /// companion to `soft_delete_column` for emitting predicates
2027 /// that need to read the field off `&self` (e.g. `trashed()`).
2028 soft_delete_field_ident: Option<syn::Ident>,
2029}
2030
2031#[derive(Clone)]
2032struct FkRelation {
2033 /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2034 /// helper is emitted as `impl <ParentType> { … }`.
2035 parent_type: Type,
2036 /// SQL column name on the child table for this FK (e.g. `"author"`).
2037 /// Used in the generated `WHERE <fk_column> = $1` clause.
2038 fk_column: String,
2039 /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2040 /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2041 /// default `ForeignKey<T>` (no explicit K); other kinds when the
2042 /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2043 pk_kind: DetectedKind,
2044 /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2045 /// FK column). Drives the `Some(...)` wrapping in load_related
2046 /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2047 /// the codegen matches the field's declared shape.
2048 nullable: bool,
2049 /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2050 /// override. When set, the reverse helper picks this name instead
2051 /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2052 /// #816 (issue's "Related" note re: per-FK override).
2053 related_name: Option<String>,
2054}
2055
2056fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2057 let root = rustango_root();
2058 let cap = named.named.len();
2059 let mut out = CollectedFields {
2060 field_schemas: Vec::with_capacity(cap),
2061 from_row_inits: Vec::with_capacity(cap),
2062 from_aliased_row_inits: Vec::with_capacity(cap),
2063 insert_columns: Vec::with_capacity(cap),
2064 insert_values: Vec::with_capacity(cap),
2065 insert_pushes: Vec::with_capacity(cap),
2066 returning_cols: Vec::new(),
2067 auto_assigns: Vec::new(),
2068 auto_field_idents: Vec::new(),
2069 generated_field_idents: Vec::new(),
2070 first_auto_value_ty: None,
2071 bulk_pushes_no_auto: Vec::with_capacity(cap),
2072 bulk_pushes_all: Vec::with_capacity(cap),
2073 bulk_columns_no_auto: Vec::with_capacity(cap),
2074 bulk_columns_all: Vec::with_capacity(cap),
2075 bulk_auto_uniformity: Vec::new(),
2076 first_auto_ident: None,
2077 has_auto: false,
2078 pk_is_auto: false,
2079 update_assignments: Vec::with_capacity(cap),
2080 upsert_update_columns: Vec::with_capacity(cap),
2081 primary_key: None,
2082 column_entries: Vec::with_capacity(cap),
2083 field_names: Vec::with_capacity(cap),
2084 fk_relations: Vec::new(),
2085 soft_delete_column: None,
2086 soft_delete_field_ident: None,
2087 };
2088
2089 for field in &named.named {
2090 let info = process_field(field, table)?;
2091 out.field_names.push(info.ident.to_string());
2092 out.field_schemas.push(info.schema);
2093 out.from_row_inits.push(info.from_row_init);
2094 out.from_aliased_row_inits.push(info.from_aliased_row_init);
2095 if let Some(parent_ty) = info.fk_inner.clone() {
2096 out.fk_relations.push(FkRelation {
2097 parent_type: parent_ty,
2098 fk_column: info.column.clone(),
2099 pk_kind: info.fk_pk_kind,
2100 nullable: info.nullable,
2101 related_name: info.related_name.clone(),
2102 });
2103 }
2104 if info.soft_delete {
2105 if out.soft_delete_column.is_some() {
2106 return Err(syn::Error::new_spanned(
2107 field,
2108 "only one field may be marked `#[rustango(soft_delete)]`",
2109 ));
2110 }
2111 out.soft_delete_column = Some(info.column.clone());
2112 out.soft_delete_field_ident = Some(info.ident.clone());
2113 }
2114 let column = info.column.as_str();
2115 let ident = info.ident;
2116 // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2117 // skip every write path — Postgres recomputes the value
2118 // from EXPR. Push only the column-entry record (so typed
2119 // column constants still exist for filtering / projection)
2120 // and the schema literal (already pushed above) and move
2121 // on. No insert_columns/values, no insert_pushes, no
2122 // bulk_*, no update_assignments, no upsert_update_columns,
2123 // no returning_cols.
2124 if info.generated_as.is_some() {
2125 out.column_entries.push(ColumnEntry {
2126 ident: ident.clone(),
2127 value_ty: info.value_ty.clone(),
2128 name: ident.to_string(),
2129 column: info.column.clone(),
2130 field_type_tokens: info.field_type_tokens,
2131 });
2132 // #1028 — refresh the DB-computed value after insert on
2133 // RETURNING-capable backends. The column joins `returning_cols`
2134 // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2135 // ident is recorded so the `AssignAutoPkPool` impl decodes it
2136 // back into the struct. MySQL has no `INSERT … RETURNING`, so
2137 // it keeps the placeholder (deferred refresh, matching Django).
2138 out.returning_cols.push(quote!(#column));
2139 out.generated_field_idents
2140 .push((ident.clone(), info.column.clone()));
2141 continue;
2142 }
2143 out.insert_columns.push(quote!(#column));
2144 out.insert_values.push(quote! {
2145 ::core::convert::Into::<#root::core::SqlValue>::into(
2146 ::core::clone::Clone::clone(&self.#ident)
2147 )
2148 });
2149 if info.auto {
2150 out.has_auto = true;
2151 if out.first_auto_ident.is_none() {
2152 out.first_auto_ident = Some(ident.clone());
2153 out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
2154 }
2155 // `default_uuid_v7` (issue #823) generates the PK Rust-side
2156 // before binding, so the value is already in
2157 // `self.#ident` after the insert_push — RETURNING is
2158 // redundant. Skip adding this column to returning_cols /
2159 // auto_assigns / auto_field_idents to avoid (a) an
2160 // unnecessary RETURNING column on every dialect, and (b)
2161 // the MySQL `LAST_INSERT_ID()` path that can only fill an
2162 // integer PK.
2163 if !info.default_uuid_v7 {
2164 out.returning_cols.push(quote!(#column));
2165 out.auto_field_idents
2166 .push((ident.clone(), info.column.clone()));
2167 out.auto_assigns.push(quote! {
2168 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2169 });
2170 }
2171 if info.default_uuid_v7 {
2172 // Rust-side UUIDv7 generation (issue #823, Eloquent
2173 // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2174 // and bind; Auto::Set → bind the user's value. The
2175 // column is ALWAYS present in the INSERT statement —
2176 // no RETURNING / no DB DEFAULT needed.
2177 out.insert_pushes.push(quote! {
2178 if matches!(&self.#ident, #root::sql::Auto::Unset) {
2179 self.#ident = #root::sql::Auto::Set(
2180 #root::__uuid::Uuid::now_v7(),
2181 );
2182 }
2183 if let #root::sql::Auto::Set(_v) = &self.#ident {
2184 _columns.push(#column);
2185 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2186 ::core::clone::Clone::clone(_v)
2187 ));
2188 }
2189 });
2190 } else {
2191 out.insert_pushes.push(quote! {
2192 if let #root::sql::Auto::Set(_v) = &self.#ident {
2193 _columns.push(#column);
2194 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2195 ::core::clone::Clone::clone(_v)
2196 ));
2197 }
2198 });
2199 }
2200 // Bulk: Auto fields appear only in the all-Set path,
2201 // never in the Unset path (we drop them from `columns`).
2202 out.bulk_columns_all.push(quote!(#column));
2203 out.bulk_pushes_all.push(quote! {
2204 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2205 ::core::clone::Clone::clone(&_row.#ident)
2206 ));
2207 });
2208 // Uniformity check: every row's Auto state must match the
2209 // first row's. Mixed Set/Unset within one bulk_insert is
2210 // rejected here so the column list stays consistent.
2211 let ident_clone = ident.clone();
2212 out.bulk_auto_uniformity.push(quote! {
2213 for _r in rows.iter().skip(1) {
2214 if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2215 return ::core::result::Result::Err(
2216 #root::sql::ExecError::Sql(
2217 #root::sql::SqlError::BulkAutoMixed
2218 )
2219 );
2220 }
2221 }
2222 });
2223 } else {
2224 out.insert_pushes.push(quote! {
2225 _columns.push(#column);
2226 _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2227 ::core::clone::Clone::clone(&self.#ident)
2228 ));
2229 });
2230 // Bulk: non-Auto fields appear in BOTH paths.
2231 out.bulk_columns_no_auto.push(quote!(#column));
2232 out.bulk_columns_all.push(quote!(#column));
2233 let push_expr = quote! {
2234 _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2235 ::core::clone::Clone::clone(&_row.#ident)
2236 ));
2237 };
2238 out.bulk_pushes_no_auto.push(push_expr.clone());
2239 out.bulk_pushes_all.push(push_expr);
2240 }
2241 if info.primary_key {
2242 if out.primary_key.is_some() {
2243 return Err(syn::Error::new_spanned(
2244 field,
2245 "only one field may be marked `#[rustango(primary_key)]`",
2246 ));
2247 }
2248 out.primary_key = Some((ident.clone(), info.column.clone()));
2249 if info.auto {
2250 out.pk_is_auto = true;
2251 }
2252 } else if info.auto_now_add {
2253 // Immutable post-insert: skip from UPDATE entirely.
2254 } else if info.auto_now {
2255 // `auto_now` columns: bind `chrono::Utc::now()` on every
2256 // UPDATE so the column is always overridden with the
2257 // wall-clock at write time, regardless of what value the
2258 // user left in the struct field.
2259 out.update_assignments.push(quote! {
2260 #root::core::Assignment {
2261 column: #column,
2262 value: ::core::convert::Into::<#root::core::Expr>::into(
2263 ::core::convert::Into::<#root::core::SqlValue>::into(
2264 #root::__chrono::Utc::now()
2265 )
2266 ),
2267 }
2268 });
2269 out.upsert_update_columns.push(quote!(#column));
2270 } else {
2271 out.update_assignments.push(quote! {
2272 #root::core::Assignment {
2273 column: #column,
2274 value: ::core::convert::Into::<#root::core::Expr>::into(
2275 ::core::convert::Into::<#root::core::SqlValue>::into(
2276 ::core::clone::Clone::clone(&self.#ident)
2277 )
2278 ),
2279 }
2280 });
2281 out.upsert_update_columns.push(quote!(#column));
2282 }
2283 out.column_entries.push(ColumnEntry {
2284 ident: ident.clone(),
2285 value_ty: info.value_ty.clone(),
2286 name: ident.to_string(),
2287 column: info.column.clone(),
2288 field_type_tokens: info.field_type_tokens,
2289 });
2290 }
2291 Ok(out)
2292}
2293
2294fn model_impl_tokens(
2295 struct_name: &syn::Ident,
2296 model_name: &str,
2297 table: &str,
2298 display: Option<&str>,
2299 app_label: Option<&str>,
2300 admin: Option<&AdminAttrs>,
2301 default_order: &[(String, bool, proc_macro2::Span)],
2302 field_schemas: &[TokenStream2],
2303 soft_delete_column: Option<&str>,
2304 permissions: bool,
2305 audit_track: Option<&[String]>,
2306 m2m_relations: &[M2MAttr],
2307 indexes: &[IndexAttr],
2308 checks: &[CheckAttr],
2309 excludes: &[ExcludeAttr],
2310 composite_fks: &[CompositeFkAttr],
2311 generic_fks: &[GenericFkAttr],
2312 scope: Option<&str>,
2313 is_view: bool,
2314 verbose_name: Option<&str>,
2315 verbose_name_plural: Option<&str>,
2316 managed: bool,
2317 base_manager_name: Option<&str>,
2318 order_with_respect_to: Option<&str>,
2319 proxy: bool,
2320 required_db_features: &[String],
2321 required_db_vendor: Option<&str>,
2322 default_related_name: Option<&str>,
2323 db_table_comment: Option<&str>,
2324 get_latest_by: Option<(&str, bool)>,
2325 extra_permissions: &[(String, String)],
2326 default_permissions: &[String],
2327 global_scopes: &[GlobalScopeAttr],
2328 reverse_has_relations: &[ReverseHasAttr],
2329 generic_has_relations: &[GenericHasAttr],
2330) -> TokenStream2 {
2331 let root = rustango_root();
2332 let display_tokens = if let Some(name) = display {
2333 quote!(::core::option::Option::Some(#name))
2334 } else {
2335 quote!(::core::option::Option::None)
2336 };
2337 let app_label_tokens = if let Some(name) = app_label {
2338 quote!(::core::option::Option::Some(#name))
2339 } else {
2340 quote!(::core::option::Option::None)
2341 };
2342 let soft_delete_tokens = if let Some(col) = soft_delete_column {
2343 quote!(::core::option::Option::Some(#col))
2344 } else {
2345 quote!(::core::option::Option::None)
2346 };
2347 let audit_track_tokens = match audit_track {
2348 None => quote!(::core::option::Option::None),
2349 Some(names) => {
2350 let lits = names.iter().map(|n| n.as_str());
2351 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2352 }
2353 };
2354 let admin_tokens = admin_config_tokens(admin);
2355 // Default `tenant` so single-tenant projects (no `scope` attr
2356 // anywhere) keep the v0.24.x behavior. Container-attr parser
2357 // already validated the value is "registry" or "tenant".
2358 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2359 Some("registry") => quote!(#root::core::ModelScope::Registry),
2360 _ => quote!(#root::core::ModelScope::Tenant),
2361 };
2362 let verbose_name_tokens = optional_str(verbose_name);
2363 let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2364 let base_manager_name_tokens = optional_str(base_manager_name);
2365 let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2366 let required_db_features_lits: Vec<&str> =
2367 required_db_features.iter().map(String::as_str).collect();
2368 let required_db_vendor_tokens = optional_str(required_db_vendor);
2369 let default_related_name_tokens = optional_str(default_related_name);
2370 let db_table_comment_tokens = optional_str(db_table_comment);
2371 let get_latest_by_tokens = match get_latest_by {
2372 Some((col, desc)) => {
2373 quote!(::core::option::Option::Some((#col, #desc)))
2374 }
2375 None => quote!(::core::option::Option::None),
2376 };
2377 let extra_permission_tokens: Vec<_> = extra_permissions
2378 .iter()
2379 .map(|(c, l)| quote!((#c, #l)))
2380 .collect();
2381 let default_permission_tokens: Vec<_> = default_permissions
2382 .iter()
2383 .map(|action| quote!(#action))
2384 .collect();
2385 let indexes_tokens = indexes.iter().map(|idx| {
2386 // When no explicit `name = "..."` was given, derive a stable,
2387 // collision-free name from the table + columns (Django-shape
2388 // `<table>_<col>_<col>_idx`) instead of a shared literal that
2389 // would clash the moment a model declares two unnamed indexes.
2390 // Capped at Postgres's 63-char identifier limit.
2391 let derived_name = idx.name.clone().unwrap_or_else(|| {
2392 let mut n = format!("{table}_{}_idx", idx.columns.join("_"));
2393 if n.len() > 63 {
2394 n.truncate(63);
2395 }
2396 n
2397 });
2398 let name = derived_name.as_str();
2399 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2400 let unique = idx.unique;
2401 // Map the parsed method string onto the IndexMethod enum
2402 // variant — kept at the codegen layer so the IR doesn't
2403 // carry the string form.
2404 let method_variant = match idx.method.as_str() {
2405 "gin" => quote!(#root::core::IndexMethod::Gin),
2406 "gist" => quote!(#root::core::IndexMethod::Gist),
2407 "brin" => quote!(#root::core::IndexMethod::Brin),
2408 "spgist" => quote!(#root::core::IndexMethod::SpGist),
2409 "hash" => quote!(#root::core::IndexMethod::Hash),
2410 "bloom" => quote!(#root::core::IndexMethod::Bloom),
2411 _ => quote!(#root::core::IndexMethod::BTree),
2412 };
2413 let where_clause = match &idx.where_clause {
2414 Some(s) => quote!(::core::option::Option::Some(#s)),
2415 None => quote!(::core::option::Option::None),
2416 };
2417 let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2418 quote! {
2419 #root::core::IndexSchema {
2420 name: #name,
2421 columns: &[ #(#cols),* ],
2422 unique: #unique,
2423 method: #method_variant,
2424 where_clause: #where_clause,
2425 include: &[ #(#include_lits),* ],
2426 }
2427 }
2428 });
2429 let checks_tokens = checks.iter().map(|c| {
2430 let name = c.name.as_str();
2431 let expr = c.expr.as_str();
2432 quote! {
2433 #root::core::CheckConstraint {
2434 name: #name,
2435 expr: #expr,
2436 }
2437 }
2438 });
2439 let excludes_tokens = excludes.iter().map(|e| {
2440 let name = e.name.as_str();
2441 let using = e.using.as_str();
2442 let element_tokens = e.elements.iter().map(|(col, op)| {
2443 let col_s = col.as_str();
2444 let op_s = op.as_str();
2445 quote!((#col_s, #op_s))
2446 });
2447 let where_tokens = match e.where_clause.as_deref() {
2448 Some(w) => quote!(::core::option::Option::Some(#w)),
2449 None => quote!(::core::option::Option::None),
2450 };
2451 quote! {
2452 #root::core::ExclusionConstraint {
2453 name: #name,
2454 using: #using,
2455 elements: &[ #(#element_tokens),* ],
2456 where_clause: #where_tokens,
2457 }
2458 }
2459 });
2460 let composite_fk_tokens = composite_fks.iter().map(|rel| {
2461 let name = rel.name.as_str();
2462 let to = rel.to.as_str();
2463 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2464 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2465 quote! {
2466 #root::core::CompositeFkRelation {
2467 name: #name,
2468 to: #to,
2469 from: &[ #(#from_cols),* ],
2470 on: &[ #(#on_cols),* ],
2471 }
2472 }
2473 });
2474 let generic_fk_tokens = generic_fks.iter().map(|rel| {
2475 let name = rel.name.as_str();
2476 let ct_col = rel.ct_column.as_str();
2477 let pk_col = rel.pk_column.as_str();
2478 quote! {
2479 #root::core::GenericRelation {
2480 name: #name,
2481 ct_column: #ct_col,
2482 pk_column: #pk_col,
2483 }
2484 }
2485 });
2486 // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2487 // no `#[rustango(default_order = "...")]` attribute was supplied.
2488 let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2489 let col_lit = col.as_str();
2490 quote! { (#col_lit, #desc) }
2491 });
2492
2493 // Issue #820 — `global_scopes` slice literal. Empty when no
2494 // `#[rustango(global_scope(...))]` attribute was supplied. The
2495 // `apply` path is re-emitted verbatim so it resolves in the
2496 // consumer's scope; the name is stored as a string literal.
2497 let global_scope_tokens = global_scopes.iter().map(|s| {
2498 let name = s.name.as_str();
2499 let apply = &s.apply;
2500 quote! {
2501 #root::core::GlobalScope {
2502 name: #name,
2503 apply: #apply,
2504 }
2505 }
2506 });
2507
2508 let m2m_tokens = m2m_relations.iter().map(|rel| {
2509 let name = rel.name.as_str();
2510 let to = rel.to.as_str();
2511 let through = rel.through.as_str();
2512 let src = rel.src.as_str();
2513 let dst = rel.dst.as_str();
2514 let auto_create = rel.auto_create;
2515 quote! {
2516 #root::core::M2MRelation {
2517 name: #name,
2518 to: #to,
2519 through: #through,
2520 src_col: #src,
2521 dst_col: #dst,
2522 auto_create: #auto_create,
2523 }
2524 }
2525 });
2526 // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2527 // when the model declares `#[rustango(reverse_has(...))]`. Each
2528 // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2529 // a const expression. Models without reverse_has fall through to
2530 // the trait's empty default — no override emitted.
2531 let reverse_relations_override = if reverse_has_relations.is_empty() {
2532 quote!()
2533 } else {
2534 let entries = reverse_has_relations.iter().map(|rel| {
2535 let name = rel.name.as_str();
2536 let child = &rel.child;
2537 let child_fk_column = rel.child_fk_column.as_str();
2538 let self_pk_column = rel.self_pk_column.as_str();
2539 quote! {
2540 #root::core::ReverseRelation {
2541 name: #name,
2542 child_schema: <#child as #root::core::Model>::SCHEMA,
2543 child_fk_column: #child_fk_column,
2544 self_pk_column: #self_pk_column,
2545 }
2546 }
2547 });
2548 quote! {
2549 fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2550 const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2551 RELS
2552 }
2553 }
2554 };
2555 // Issue #830 — `Model::generic_reverse_relations()` override for
2556 // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2557 let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2558 quote!()
2559 } else {
2560 let entries = generic_has_relations.iter().map(|rel| {
2561 let name = rel.name.as_str();
2562 let child = &rel.child;
2563 let ct_column = rel.ct_column.as_str();
2564 let pk_column = rel.pk_column.as_str();
2565 let self_pk_column = rel.self_pk_column.as_str();
2566 quote! {
2567 #root::core::GenericReverseRelation {
2568 name: #name,
2569 child_schema: <#child as #root::core::Model>::SCHEMA,
2570 ct_column: #ct_column,
2571 pk_column: #pk_column,
2572 self_pk_column: #self_pk_column,
2573 }
2574 }
2575 });
2576 quote! {
2577 fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2578 const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2579 RELS
2580 }
2581 }
2582 };
2583 quote! {
2584 impl #root::core::Model for #struct_name {
2585 const SCHEMA: &'static #root::core::ModelSchema = &#root::core::ModelSchema {
2586 name: #model_name,
2587 table: #table,
2588 fields: &[ #(#field_schemas),* ],
2589 display: #display_tokens,
2590 app_label: #app_label_tokens,
2591 admin: #admin_tokens,
2592 soft_delete_column: #soft_delete_tokens,
2593 permissions: #permissions,
2594 audit_track: #audit_track_tokens,
2595 m2m: &[ #(#m2m_tokens),* ],
2596 indexes: &[ #(#indexes_tokens),* ],
2597 check_constraints: &[ #(#checks_tokens),* ],
2598 exclusion_constraints: &[ #(#excludes_tokens),* ],
2599 composite_relations: &[ #(#composite_fk_tokens),* ],
2600 generic_relations: &[ #(#generic_fk_tokens),* ],
2601 scope: #scope_tokens,
2602 default_order: &[ #(#default_order_tokens),* ],
2603 is_view: #is_view,
2604 verbose_name: #verbose_name_tokens,
2605 verbose_name_plural: #verbose_name_plural_tokens,
2606 managed: #managed,
2607 base_manager_name: #base_manager_name_tokens,
2608 order_with_respect_to: #order_with_respect_to_tokens,
2609 proxy: #proxy,
2610 required_db_features: &[ #(#required_db_features_lits),* ],
2611 required_db_vendor: #required_db_vendor_tokens,
2612 default_related_name: #default_related_name_tokens,
2613 db_table_comment: #db_table_comment_tokens,
2614 get_latest_by: #get_latest_by_tokens,
2615 extra_permissions: &[ #(#extra_permission_tokens),* ],
2616 default_permissions: &[ #(#default_permission_tokens),* ],
2617 global_scopes: &[ #(#global_scope_tokens),* ],
2618 };
2619
2620 #reverse_relations_override
2621 #generic_reverse_relations_override
2622 }
2623 }
2624}
2625
2626/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2627/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2628/// otherwise a static reference to a populated `AdminConfig`.
2629fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2630 let root = rustango_root();
2631 let Some(admin) = admin else {
2632 return quote!(::core::option::Option::None);
2633 };
2634
2635 let list_display = admin
2636 .list_display
2637 .as_ref()
2638 .map(|(v, _)| v.as_slice())
2639 .unwrap_or(&[]);
2640 let list_display_lits = list_display.iter().map(|s| s.as_str());
2641
2642 let search_fields = admin
2643 .search_fields
2644 .as_ref()
2645 .map(|(v, _)| v.as_slice())
2646 .unwrap_or(&[]);
2647 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2648
2649 let readonly_fields = admin
2650 .readonly_fields
2651 .as_ref()
2652 .map(|(v, _)| v.as_slice())
2653 .unwrap_or(&[]);
2654 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2655
2656 let list_filter = admin
2657 .list_filter
2658 .as_ref()
2659 .map(|(v, _)| v.as_slice())
2660 .unwrap_or(&[]);
2661 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2662
2663 let actions = admin
2664 .actions
2665 .as_ref()
2666 .map(|(v, _)| v.as_slice())
2667 .unwrap_or(&[]);
2668 let actions_lits = actions.iter().map(|s| s.as_str());
2669
2670 let fieldsets = admin
2671 .fieldsets
2672 .as_ref()
2673 .map(|(v, _)| v.as_slice())
2674 .unwrap_or(&[]);
2675 let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
2676 let title = title.as_str();
2677 let field_lits = fields.iter().map(|s| s.as_str());
2678 quote!(#root::core::Fieldset {
2679 title: #title,
2680 fields: &[ #( #field_lits ),* ],
2681 })
2682 });
2683
2684 let list_display_links = admin
2685 .list_display_links
2686 .as_ref()
2687 .map(|(v, _)| v.as_slice())
2688 .unwrap_or(&[]);
2689 let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2690
2691 let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2692 let actions_on_top = admin.actions_on_top.unwrap_or(true);
2693 let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2694 let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2695
2696 let prepopulated = admin
2697 .prepopulated_fields
2698 .as_ref()
2699 .map(|(v, _)| v.as_slice())
2700 .unwrap_or(&[]);
2701 let prepopulated_tokens = prepopulated.iter().map(|(target, sources)| {
2702 let target = target.as_str();
2703 let source_lits = sources.iter().map(|s| s.as_str());
2704 quote!(#root::core::PrepopulatedField {
2705 target: #target,
2706 sources: &[ #( #source_lits ),* ],
2707 })
2708 });
2709
2710 let raw_id_fields = admin
2711 .raw_id_fields
2712 .as_ref()
2713 .map(|(v, _)| v.as_slice())
2714 .unwrap_or(&[]);
2715 let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2716
2717 let autocomplete_fields = admin
2718 .autocomplete_fields
2719 .as_ref()
2720 .map(|(v, _)| v.as_slice())
2721 .unwrap_or(&[]);
2722 let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
2723
2724 // #352 — list_select_related accepts "all" | "none" | "field, field, …".
2725 let list_select_related_tokens = match admin.list_select_related.as_deref() {
2726 None | Some("all") => quote!(#root::core::ListSelectRelated::All),
2727 Some("none") => quote!(#root::core::ListSelectRelated::None),
2728 Some(raw) => {
2729 let names: Vec<&str> = raw
2730 .split(',')
2731 .map(str::trim)
2732 .filter(|s| !s.is_empty())
2733 .collect();
2734 quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
2735 }
2736 };
2737
2738 // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
2739 // a Vec<(String, String)>. Empty / unset → no overrides.
2740 let formfield_pairs: Vec<(&str, &str)> = admin
2741 .formfield_overrides
2742 .as_ref()
2743 .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
2744 .unwrap_or_default();
2745 let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
2746 let field = *field;
2747 let widget = *widget;
2748 quote!((#field, #widget))
2749 });
2750
2751 let list_per_page = admin.list_per_page.unwrap_or(0);
2752
2753 let ordering_pairs = admin
2754 .ordering
2755 .as_ref()
2756 .map(|(v, _)| v.as_slice())
2757 .unwrap_or(&[]);
2758 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
2759 let name = name.as_str();
2760 let desc = *desc;
2761 quote!((#name, #desc))
2762 });
2763
2764 quote! {
2765 ::core::option::Option::Some(&#root::core::AdminConfig {
2766 list_display: &[ #( #list_display_lits ),* ],
2767 search_fields: &[ #( #search_fields_lits ),* ],
2768 list_per_page: #list_per_page,
2769 ordering: &[ #( #ordering_tokens ),* ],
2770 readonly_fields: &[ #( #readonly_fields_lits ),* ],
2771 list_filter: &[ #( #list_filter_lits ),* ],
2772 actions: &[ #( #actions_lits ),* ],
2773 fieldsets: &[ #( #fieldset_tokens ),* ],
2774 list_display_links: &[ #( #list_display_links_lits ),* ],
2775 search_help_text: #search_help_text,
2776 actions_on_top: #actions_on_top,
2777 actions_on_bottom: #actions_on_bottom,
2778 date_hierarchy: #date_hierarchy,
2779 prepopulated_fields: &[ #( #prepopulated_tokens ),* ],
2780 raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
2781 autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
2782 list_select_related: #list_select_related_tokens,
2783 formfield_overrides: &[ #( #formfield_tokens ),* ],
2784 })
2785 }
2786}
2787
2788fn inherent_impl_tokens(
2789 struct_name: &syn::Ident,
2790 fields: &CollectedFields,
2791 primary_key: Option<&(syn::Ident, String)>,
2792 column_consts: &TokenStream2,
2793 audited_fields: Option<&[&ColumnEntry]>,
2794 indexes: &[IndexAttr],
2795 manager_fns: &[syn::Ident],
2796) -> TokenStream2 {
2797 let root = rustango_root();
2798 // Audit-emit fragments threaded into write paths. Non-empty only
2799 // when the model carries `#[rustango(audit(...))]`. They reborrow
2800 // `_executor` (a `&mut PgConnection` for audited models — the
2801 // macro switches the signature below) so the data write and the
2802 // audit INSERT both run on the same caller-supplied connection.
2803 let executor_passes_to_data_write = if audited_fields.is_some() {
2804 quote!(&mut *_executor)
2805 } else {
2806 quote!(_executor)
2807 };
2808 let executor_param = if audited_fields.is_some() {
2809 quote!(_executor: &mut #root::sql::sqlx::PgConnection)
2810 } else {
2811 quote!(_executor: _E)
2812 };
2813 let executor_generics = if audited_fields.is_some() {
2814 quote!()
2815 } else {
2816 quote!(<'_c, _E>)
2817 };
2818 let executor_where = if audited_fields.is_some() {
2819 quote!()
2820 } else {
2821 quote! {
2822 where
2823 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
2824 }
2825 };
2826 // For audited models the `_on` methods take `&mut PgConnection`, so
2827 // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
2828 // must acquire a connection first. Non-audited models keep the
2829 // direct delegation since `&PgPool` IS an Executor.
2830 let pool_to_save_on = if audited_fields.is_some() {
2831 quote! {
2832 let mut _conn = pool.acquire().await?;
2833 self.save_on(&mut *_conn).await
2834 }
2835 } else {
2836 quote!(self.save_on(pool).await)
2837 };
2838 let pool_to_insert_on = if audited_fields.is_some() {
2839 quote! {
2840 let mut _conn = pool.acquire().await?;
2841 self.insert_on(&mut *_conn).await
2842 }
2843 } else {
2844 quote!(self.insert_on(pool).await)
2845 };
2846 let pool_to_delete_on = if audited_fields.is_some() {
2847 quote! {
2848 let mut _conn = pool.acquire().await?;
2849 self.delete_on(&mut *_conn).await
2850 }
2851 } else {
2852 quote!(self.delete_on(pool).await)
2853 };
2854 let pool_to_bulk_insert_on = if audited_fields.is_some() {
2855 quote! {
2856 let mut _conn = pool.acquire().await?;
2857 Self::bulk_insert_on(rows, &mut *_conn).await
2858 }
2859 } else {
2860 quote!(Self::bulk_insert_on(rows, pool).await)
2861 };
2862 // Pre-existing bug surfaced by batch 22's first audited Auto<T>
2863 // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
2864 // directly, but `upsert_on` for audited models takes
2865 // `&mut PgConnection` (the audit emit needs a real connection).
2866 // Add the missing acquire shim to keep audited Auto-PK upsert
2867 // compiling.
2868 let pool_to_upsert_on = if audited_fields.is_some() {
2869 quote! {
2870 let mut _conn = pool.acquire().await?;
2871 self.upsert_on(&mut *_conn).await
2872 }
2873 } else {
2874 quote!(self.upsert_on(pool).await)
2875 };
2876
2877 // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
2878 // (audit-on-connection over &Pool needs a bi-dialect transaction
2879 // helper, deferred). Two body shapes:
2880 // - has_auto: build InsertQuery skipping Auto::Unset columns,
2881 // request Auto cols in `returning`, dispatch via
2882 // `insert_returning_pool`, then on the returned `PgRow` /
2883 // `MySqlAutoId(id)` enum — pull each Auto field from the PG
2884 // row OR drop the single i64 into the first Auto field on MySQL
2885 // (multi-Auto models on MySQL error at runtime since
2886 // `LAST_INSERT_ID()` only reports one)
2887 // - non-Auto: build InsertQuery with explicit columns/values and
2888 // call `insert_pool` (no returning needed)
2889 // pool_insert_method body for the audited Auto-PK case is moved
2890 // to after audit_pair_tokens / audit_pk_to_string (they live
2891 // ~150 lines below). This block keeps the non-audited and
2892 // non-Auto branches in place — the audited Auto-PK arm is
2893 // computed below and merged via the dispatch helper variable.
2894 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
2895 // Audited models with explicit (non-Auto) PKs go through
2896 // the non-Auto insert path below — the audit emit is one
2897 // round-trip after the INSERT inside the same tx via
2898 // audit::save_one_with_audit? No, INSERT semantics
2899 // differ. For non-Auto PK + audited, route through a
2900 // dedicated insert + audit emit on the same tx, but defer
2901 // the macro emission to the audit-bundle-aware block below
2902 // — this `quote!()` placeholder gets overwritten there.
2903 quote!()
2904 } else if audited_fields.is_some() && fields.has_auto {
2905 // Audited Auto-PK insert_pool — assembled after the audit
2906 // bundles. Placeholder; real emission below.
2907 quote!()
2908 } else if fields.has_auto {
2909 let pushes = &fields.insert_pushes;
2910 let returning_cols = &fields.returning_cols;
2911 // When every `Auto<T>` field is filled Rust-side
2912 // (`default_uuid_v7`, issue #823), there is no column to read
2913 // back from the database — `returning_cols` is empty. Route
2914 // through plain `insert_pool` instead of
2915 // `insert_returning_pool` to skip the redundant RETURNING /
2916 // LAST_INSERT_ID round-trip.
2917 if fields.returning_cols.is_empty() {
2918 quote! {
2919 /// Insert this row against either backend. Every
2920 /// `Auto<T>` PK on this model is filled Rust-side
2921 /// (e.g. `default_uuid_v7`) before binding, so no
2922 /// RETURNING round-trip is needed.
2923 ///
2924 /// # Errors
2925 /// As [`Self::insert`].
2926 pub async fn insert_pool(
2927 &mut self,
2928 pool: &#root::sql::Pool,
2929 ) -> ::core::result::Result<(), #root::sql::ExecError> {
2930 let mut _columns: ::std::vec::Vec<&'static str> =
2931 ::std::vec::Vec::new();
2932 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2933 ::std::vec::Vec::new();
2934 #( #pushes )*
2935 let _query = #root::core::InsertQuery {
2936 model: <Self as #root::core::Model>::SCHEMA,
2937 columns: _columns,
2938 values: _values,
2939 returning: ::std::vec::Vec::new(),
2940 on_conflict: ::core::option::Option::None,
2941 };
2942 #root::sql::insert_pool(pool, &_query).await
2943 }
2944
2945 /// Eloquent `Model::insertOrIgnore()` — same shape
2946 /// as the auto-PK branch above. Returns `Ok(true)`
2947 /// when inserted, `Ok(false)` when a conflict caused
2948 /// the INSERT to silently skip.
2949 ///
2950 /// # Errors
2951 /// As [`Self::insert`].
2952 pub async fn insert_or_ignore(
2953 &mut self,
2954 pool: &#root::sql::Pool,
2955 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
2956 let mut _columns: ::std::vec::Vec<&'static str> =
2957 ::std::vec::Vec::new();
2958 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2959 ::std::vec::Vec::new();
2960 #( #pushes )*
2961 let _query = #root::core::InsertQuery {
2962 model: <Self as #root::core::Model>::SCHEMA,
2963 columns: _columns,
2964 values: _values,
2965 returning: ::std::vec::Vec::new(),
2966 on_conflict: ::core::option::Option::Some(
2967 #root::core::ConflictClause::DoNothing,
2968 ),
2969 };
2970 let dialect = pool.dialect();
2971 let stmt = dialect.compile_insert(&_query)?;
2972 let rows = #root::sql::raw_execute_pool(
2973 pool, &stmt.sql, stmt.params,
2974 ).await?;
2975 ::core::result::Result::Ok(rows > 0)
2976 }
2977 }
2978 } else {
2979 quote! {
2980 /// Insert this row against either backend, populating any
2981 /// `Auto<T>` PK from the auto-assigned value.
2982 ///
2983 /// # Errors
2984 /// As [`Self::insert`].
2985 pub async fn insert_pool(
2986 &mut self,
2987 pool: &#root::sql::Pool,
2988 ) -> ::core::result::Result<(), #root::sql::ExecError> {
2989 let mut _columns: ::std::vec::Vec<&'static str> =
2990 ::std::vec::Vec::new();
2991 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2992 ::std::vec::Vec::new();
2993 #( #pushes )*
2994 let _query = #root::core::InsertQuery {
2995 model: <Self as #root::core::Model>::SCHEMA,
2996 columns: _columns,
2997 values: _values,
2998 returning: ::std::vec![ #( #returning_cols ),* ],
2999 on_conflict: ::core::option::Option::None,
3000 };
3001 let _result = #root::sql::insert_returning_pool(
3002 pool, &_query,
3003 ).await?;
3004 #root::sql::apply_auto_pk(_result, self)
3005 }
3006
3007 /// Eloquent `Model::insertOrIgnore()` — INSERT this
3008 /// row or silently skip on unique-constraint
3009 /// violation. Returns `Ok(true)` when a row was
3010 /// inserted, `Ok(false)` when a conflict caused the
3011 /// INSERT to silently skip.
3012 ///
3013 /// **Caveat on auto-PK models**: when the row is
3014 /// skipped (conflict), this instance's `Auto<T>`
3015 /// fields stay `Unset` — no PK is back-populated
3016 /// because the server didn't auto-assign one. For
3017 /// "insert then read back the PK or the existing
3018 /// row's PK", use the `upsert` family or
3019 /// `get_or_create`.
3020 ///
3021 /// # Errors
3022 /// As [`Self::insert`].
3023 pub async fn insert_or_ignore(
3024 &mut self,
3025 pool: &#root::sql::Pool,
3026 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3027 let mut _columns: ::std::vec::Vec<&'static str> =
3028 ::std::vec::Vec::new();
3029 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3030 ::std::vec::Vec::new();
3031 #( #pushes )*
3032 let _query = #root::core::InsertQuery {
3033 model: <Self as #root::core::Model>::SCHEMA,
3034 columns: _columns,
3035 values: _values,
3036 returning: ::std::vec::Vec::new(),
3037 on_conflict: ::core::option::Option::Some(
3038 #root::core::ConflictClause::DoNothing,
3039 ),
3040 };
3041 let dialect = pool.dialect();
3042 let stmt = dialect.compile_insert(&_query)?;
3043 let rows = #root::sql::raw_execute_pool(
3044 pool, &stmt.sql, stmt.params,
3045 ).await?;
3046 ::core::result::Result::Ok(rows > 0)
3047 }
3048 }
3049 }
3050 } else {
3051 let insert_columns = &fields.insert_columns;
3052 let insert_values = &fields.insert_values;
3053 quote! {
3054 /// Insert this row into its table against either backend.
3055 /// Equivalent to [`Self::insert`] but takes
3056 /// [`#root::sql::Pool`].
3057 ///
3058 /// # Errors
3059 /// As [`Self::insert`].
3060 pub async fn insert_pool(
3061 &self,
3062 pool: &#root::sql::Pool,
3063 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3064 let _query = #root::core::InsertQuery {
3065 model: <Self as #root::core::Model>::SCHEMA,
3066 columns: ::std::vec![ #( #insert_columns ),* ],
3067 values: ::std::vec![ #( #insert_values ),* ],
3068 returning: ::std::vec::Vec::new(),
3069 on_conflict: ::core::option::Option::None,
3070 };
3071 #root::sql::insert_pool(pool, &_query).await
3072 }
3073
3074 /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3075 /// or silently skip on unique-constraint violation. Maps
3076 /// to per-dialect "INSERT ... DO NOTHING on conflict":
3077 /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3078 /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL
3079 /// `INSERT IGNORE INTO …`.
3080 ///
3081 /// Returns `Ok(true)` when a row was inserted,
3082 /// `Ok(false)` when a conflict caused the INSERT to
3083 /// silently skip.
3084 ///
3085 /// Use for "create-if-absent" patterns where you don't
3086 /// need the row back. For "find-or-create with the row
3087 /// returned", use the queryset-level
3088 /// `crate::sql::get_or_create` free function.
3089 ///
3090 /// # Errors
3091 /// As [`Self::insert`], plus any dialect-specific
3092 /// translation error from the ConflictClause writer.
3093 pub async fn insert_or_ignore(
3094 &self,
3095 pool: &#root::sql::Pool,
3096 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3097 let _query = #root::core::InsertQuery {
3098 model: <Self as #root::core::Model>::SCHEMA,
3099 columns: ::std::vec![ #( #insert_columns ),* ],
3100 values: ::std::vec![ #( #insert_values ),* ],
3101 returning: ::std::vec::Vec::new(),
3102 on_conflict: ::core::option::Option::Some(
3103 #root::core::ConflictClause::DoNothing,
3104 ),
3105 };
3106 let dialect = pool.dialect();
3107 let stmt = dialect.compile_insert(&_query)?;
3108 let rows = #root::sql::raw_execute_pool(pool, &stmt.sql, stmt.params).await?;
3109 ::core::result::Result::Ok(rows > 0)
3110 }
3111 }
3112 };
3113
3114 // pool_save_method moved to after audit_pair_tokens /
3115 // audit_pk_to_string (they live ~70 lines below) — needed for
3116 // the audited branch which builds an UpdateQuery + PendingEntry
3117 // and dispatches via audit::save_one_with_audit.
3118
3119 // pool_delete_method moved to after audit_pair_tokens / audit_pk_to_string
3120 // are computed (they live ~80 lines below).
3121
3122 // Build the (column, JSON value) pair list used by every
3123 // snapshot-style audit emission. Reused across delete_on,
3124 // soft_delete_on, restore_on, and (later) bulk paths. Empty
3125 // when the model isn't audited.
3126 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3127 .map(|tracked| {
3128 tracked
3129 .iter()
3130 .map(|c| {
3131 let column_lit = c.column.as_str();
3132 let ident = &c.ident;
3133 quote! {
3134 (
3135 #column_lit,
3136 #root::__serde_json::to_value(&self.#ident)
3137 .unwrap_or(#root::__serde_json::Value::Null),
3138 )
3139 }
3140 })
3141 .collect()
3142 })
3143 .unwrap_or_default();
3144 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3145 if fields.pk_is_auto {
3146 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3147 } else {
3148 quote!(::std::format!("{}", &self.#pk_ident))
3149 }
3150 } else {
3151 quote!(::std::string::String::new())
3152 };
3153 let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
3154 if audited_fields.is_some() {
3155 let pairs = audit_pair_tokens.iter();
3156 let pk_str = audit_pk_to_string.clone();
3157 quote! {
3158 let _audit_entry = #root::audit::PendingEntry {
3159 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3160 entity_pk: #pk_str,
3161 operation: #op_path,
3162 source: #root::audit::current_source(),
3163 changes: #root::audit::snapshot_changes(&[
3164 #( #pairs ),*
3165 ]),
3166 };
3167 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3168 }
3169 } else {
3170 quote!()
3171 }
3172 };
3173 let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create));
3174 let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete));
3175 let audit_softdelete_emit = make_op_emit(quote!(#root::audit::AuditOp::SoftDelete));
3176 let audit_restore_emit = make_op_emit(quote!(#root::audit::AuditOp::Restore));
3177
3178 // `save_pool(&Pool)` — emitted for every model with a PK.
3179 // Audited Auto-PK models are deferred (the Auto::Unset →
3180 // insert_pool path needs the audited-insert flow from a future
3181 // batch). Three body shapes:
3182 // - non-audited, plain PK: build UpdateQuery + dispatch through
3183 // sql::update_pool
3184 // - non-audited, Auto-PK: same, but Auto::Unset routes to
3185 // self.insert_pool which already handles RETURNING / LAST_INSERT_ID
3186 // - audited, plain PK: build UpdateQuery + PendingEntry, dispatch
3187 // through audit::save_one_with_audit (per-backend tx wraps
3188 // UPDATE + audit emit atomically). Snapshot-style audit (post-
3189 // write field values) — diff-style audit (with pre-UPDATE
3190 // SELECT for `before` values) needs per-tracked-column codegen
3191 // that doesn't fit the runtime-helper pattern; legacy &PgPool
3192 // `save` keeps the diff for now.
3193 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3194 let pk_column_lit = pk_col.as_str();
3195 let assignments = &fields.update_assignments;
3196 if audited_fields.is_some() {
3197 if fields.pk_is_auto {
3198 // Auto-PK + audited: defer. The Auto::Unset insert
3199 // path needs a transactional INSERT + LAST_INSERT_ID
3200 // + audit emit flow — that's a follow-up batch.
3201 quote!()
3202 } else {
3203 let pairs = audit_pair_tokens.iter();
3204 let pairs2 = audit_pair_tokens.iter();
3205 let pk_str = audit_pk_to_string.clone();
3206 let pk_str2 = audit_pk_to_string.clone();
3207 quote! {
3208 /// Save (UPDATE) this row against either backend
3209 /// with audit emission inside the same transaction.
3210 /// Bi-dialect counterpart of [`Self::save`] for
3211 /// audited models with non-`Auto<T>` PKs.
3212 ///
3213 /// Captures **post-write** field state (snapshot
3214 /// audit). The legacy &PgPool [`Self::save`]
3215 /// captures BEFORE+AFTER for true diff audit;
3216 /// porting that to the &Pool path needs runtime
3217 /// per-tracked-column decoding and is deferred.
3218 ///
3219 /// # Errors
3220 /// As [`Self::save`].
3221 pub async fn save_pool(
3222 &mut self,
3223 pool: &#root::sql::Pool,
3224 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3225 let _query = #root::core::UpdateQuery {
3226 model: <Self as #root::core::Model>::SCHEMA,
3227 set: ::std::vec![ #( #assignments ),* ],
3228 where_clause: #root::core::WhereExpr::Predicate(
3229 #root::core::Filter {
3230 column: #pk_column_lit,
3231 op: #root::core::Op::Eq,
3232 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3233 ::core::clone::Clone::clone(&self.#pk_ident)
3234 ),
3235 }
3236 ),
3237 };
3238 let _audit_entry = #root::audit::PendingEntry {
3239 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3240 entity_pk: #pk_str,
3241 operation: #root::audit::AuditOp::Update,
3242 source: #root::audit::current_source(),
3243 changes: #root::audit::snapshot_changes(&[
3244 #( #pairs ),*
3245 ]),
3246 };
3247 let _affected = #root::audit::save_one_with_audit(
3248 pool, &_query, &_audit_entry,
3249 ).await?;
3250 ::core::result::Result::Ok(_affected)
3251 }
3252
3253 /// `save_pool` narrowed to a Rust-field allowlist — issue #66
3254 /// (Django `Model.save(update_fields=[...])`).
3255 /// Audit emission shrinks to the same column set so
3256 /// the audit log reflects exactly what was written.
3257 ///
3258 /// # Errors
3259 /// As [`Self::save_pool`], plus
3260 /// [`#root::core::QueryError::UnknownField`] wrapped
3261 /// in `ExecError::Query` for unknown field names.
3262 pub async fn save_partial(
3263 &mut self,
3264 fields: &[&str],
3265 pool: &#root::sql::Pool,
3266 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3267 if fields.is_empty() {
3268 #root::__tracing::warn!(
3269 target: "rustango::save_partial",
3270 model = <Self as #root::core::Model>::SCHEMA.name,
3271 "save_partial called with empty field list — no-op"
3272 );
3273 return ::core::result::Result::Ok(0);
3274 }
3275 let _schema = <Self as #root::core::Model>::SCHEMA;
3276 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3277 ::std::collections::HashSet::with_capacity(fields.len());
3278 for f in fields {
3279 match _schema.field(f) {
3280 ::core::option::Option::Some(fs) => {
3281 _wanted_cols.insert(fs.column);
3282 }
3283 ::core::option::Option::None => {
3284 return ::core::result::Result::Err(
3285 #root::sql::ExecError::Query(
3286 #root::core::QueryError::UnknownField {
3287 model: _schema.name,
3288 field: (*f).to_owned(),
3289 }
3290 )
3291 );
3292 }
3293 }
3294 }
3295 let _full: ::std::vec::Vec<#root::core::Assignment> =
3296 ::std::vec![ #( #assignments ),* ];
3297 let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3298 .into_iter()
3299 .filter(|a| _wanted_cols.contains(a.column))
3300 .collect();
3301 if _filtered.is_empty() {
3302 #root::__tracing::warn!(
3303 target: "rustango::save_partial",
3304 model = _schema.name,
3305 "save_partial: every named field maps to a non-assignable column — no-op"
3306 );
3307 return ::core::result::Result::Ok(0);
3308 }
3309 let _query = #root::core::UpdateQuery {
3310 model: _schema,
3311 set: _filtered,
3312 where_clause: #root::core::WhereExpr::Predicate(
3313 #root::core::Filter {
3314 column: #pk_column_lit,
3315 op: #root::core::Op::Eq,
3316 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3317 ::core::clone::Clone::clone(&self.#pk_ident)
3318 ),
3319 }
3320 ),
3321 };
3322 // Narrow the audit snapshot to the same column set.
3323 let _all_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3324 ::std::vec![ #( #pairs2 ),* ];
3325 let _narrowed: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3326 _all_pairs
3327 .into_iter()
3328 .filter(|(col, _)| _wanted_cols.contains(col))
3329 .collect();
3330 let _audit_entry = #root::audit::PendingEntry {
3331 entity_table: _schema.table,
3332 entity_pk: #pk_str2,
3333 operation: #root::audit::AuditOp::Update,
3334 source: #root::audit::current_source(),
3335 changes: #root::audit::snapshot_changes(&_narrowed),
3336 };
3337 let _affected = #root::audit::save_one_with_audit(
3338 pool, &_query, &_audit_entry,
3339 ).await?;
3340 ::core::result::Result::Ok(_affected)
3341 }
3342
3343 /// Typed-column counterpart of [`Self::save_partial`] —
3344 /// issue #67. `fields` is a tuple of [`Column`]
3345 /// constants whose `Model` matches `Self`; typos and
3346 /// model mismatches surface at *compile time*
3347 /// (`Author::name` inside a `Post::save_partial_typed`
3348 /// call is a type error, no runtime check).
3349 ///
3350 /// ```ignore
3351 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3352 /// ```
3353 ///
3354 /// Lowers to [`Self::save_partial`] under the hood;
3355 /// audit narrowing + every other semantic is identical.
3356 ///
3357 /// [`Column`]: #root::core::Column
3358 ///
3359 /// # Errors
3360 /// As [`Self::save_partial`].
3361 pub async fn save_partial_typed<
3362 L: #root::core::TypedFieldList<Self>,
3363 >(
3364 &mut self,
3365 fields: L,
3366 pool: &#root::sql::Pool,
3367 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3368 let _names = fields.rust_field_names();
3369 let _refs: ::std::vec::Vec<&str> =
3370 _names.iter().copied().collect();
3371 self.save_partial(&_refs, pool).await
3372 }
3373 }
3374 }
3375 } else {
3376 let dispatch_unset = if fields.pk_is_auto {
3377 quote! {
3378 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3379 return self.insert_pool(pool).await.map(|()| 1u64);
3380 }
3381 }
3382 } else {
3383 quote!()
3384 };
3385 quote! {
3386 /// Save this row to its table against either backend.
3387 /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3388 /// `UPDATE` keyed on the PK.
3389 ///
3390 /// # Errors
3391 /// As [`Self::save`].
3392 pub async fn save_pool(
3393 &mut self,
3394 pool: &#root::sql::Pool,
3395 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3396 #dispatch_unset
3397 let _query = #root::core::UpdateQuery {
3398 model: <Self as #root::core::Model>::SCHEMA,
3399 set: ::std::vec![ #( #assignments ),* ],
3400 where_clause: #root::core::WhereExpr::Predicate(
3401 #root::core::Filter {
3402 column: #pk_column_lit,
3403 op: #root::core::Op::Eq,
3404 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3405 ::core::clone::Clone::clone(&self.#pk_ident)
3406 ),
3407 }
3408 ),
3409 };
3410 let _affected = #root::sql::update_pool(pool, &_query).await?;
3411 ::core::result::Result::Ok(_affected)
3412 }
3413
3414 /// Save (UPDATE) only the listed Rust-side fields,
3415 /// leaving every other column untouched. Issue #66 —
3416 /// Django's `Model.save(update_fields=[...])` shape.
3417 ///
3418 /// `fields` are Rust-side struct field names; the macro
3419 /// resolves each to its SQL column. Unknown field
3420 /// names return [`#root::core::QueryError::UnknownField`]
3421 /// wrapped in `ExecError::Query`. An empty list is a
3422 /// no-op (returns `Ok(())` and logs a `tracing::warn!`),
3423 /// matching Django's "nothing to do" semantic.
3424 ///
3425 /// Use this when:
3426 /// * you only mutated a couple of fields on a wide row
3427 /// (avoid re-writing every column on every save), or
3428 /// * two writers diverged after their initial read and
3429 /// you want to preserve the other writer's changes to
3430 /// columns you didn't touch.
3431 ///
3432 /// Auto-PK models with an unset PK return
3433 /// [`#root::core::QueryError::UnknownField`] with
3434 /// field name `<pk>` — `save_partial` is an
3435 /// UPDATE-only path. Call [`Self::insert_pool`]
3436 /// (or [`Self::save_pool`] which dispatches based on
3437 /// PK state) for the INSERT case.
3438 ///
3439 /// # Errors
3440 /// As [`Self::save_pool`], plus `UnknownField` for
3441 /// unknown / empty / Auto-Unset cases.
3442 pub async fn save_partial(
3443 &mut self,
3444 fields: &[&str],
3445 pool: &#root::sql::Pool,
3446 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3447 if fields.is_empty() {
3448 #root::__tracing::warn!(
3449 target: "rustango::save_partial",
3450 model = <Self as #root::core::Model>::SCHEMA.name,
3451 "save_partial called with empty field list — no-op"
3452 );
3453 return ::core::result::Result::Ok(0);
3454 }
3455 let _schema = <Self as #root::core::Model>::SCHEMA;
3456 // Validate field names against the schema.
3457 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3458 ::std::collections::HashSet::with_capacity(fields.len());
3459 for f in fields {
3460 match _schema.field(f) {
3461 ::core::option::Option::Some(fs) => {
3462 _wanted_cols.insert(fs.column);
3463 }
3464 ::core::option::Option::None => {
3465 return ::core::result::Result::Err(
3466 #root::sql::ExecError::Query(
3467 #root::core::QueryError::UnknownField {
3468 model: _schema.name,
3469 field: (*f).to_owned(),
3470 }
3471 )
3472 );
3473 }
3474 }
3475 }
3476 // Build the full assignment vec, then keep only the
3477 // assignments whose column is in `_wanted_cols`.
3478 let _full: ::std::vec::Vec<#root::core::Assignment> =
3479 ::std::vec![ #( #assignments ),* ];
3480 let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3481 .into_iter()
3482 .filter(|a| _wanted_cols.contains(a.column))
3483 .collect();
3484 if _filtered.is_empty() {
3485 // All field names valid, but they all map to
3486 // non-assignable slots (PK column, computed/
3487 // virtual fields, relations without an
3488 // assignment). Same no-op semantic as Django.
3489 #root::__tracing::warn!(
3490 target: "rustango::save_partial",
3491 model = _schema.name,
3492 "save_partial: every named field maps to a non-assignable column — no-op"
3493 );
3494 return ::core::result::Result::Ok(0);
3495 }
3496 let _query = #root::core::UpdateQuery {
3497 model: _schema,
3498 set: _filtered,
3499 where_clause: #root::core::WhereExpr::Predicate(
3500 #root::core::Filter {
3501 column: #pk_column_lit,
3502 op: #root::core::Op::Eq,
3503 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3504 ::core::clone::Clone::clone(&self.#pk_ident)
3505 ),
3506 }
3507 ),
3508 };
3509 let _affected = #root::sql::update_pool(pool, &_query).await?;
3510 ::core::result::Result::Ok(_affected)
3511 }
3512
3513 /// Typed-column counterpart of [`Self::save_partial`] —
3514 /// issue #67. `fields` is a tuple of [`Column`]
3515 /// constants whose `Model` matches `Self`; typos and
3516 /// model mismatches surface at *compile time*
3517 /// (`Author::name` inside a `Post::save_partial_typed`
3518 /// call is a type error, no runtime check).
3519 ///
3520 /// ```ignore
3521 /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3522 /// ```
3523 ///
3524 /// Lowers to [`Self::save_partial`] under the hood — the
3525 /// tuple is reduced to a `&[&str]` slice of Rust-side
3526 /// field names and forwarded.
3527 ///
3528 /// [`Column`]: #root::core::Column
3529 ///
3530 /// # Errors
3531 /// As [`Self::save_partial`].
3532 pub async fn save_partial_typed<
3533 L: #root::core::TypedFieldList<Self>,
3534 >(
3535 &mut self,
3536 fields: L,
3537 pool: &#root::sql::Pool,
3538 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3539 let _names = fields.rust_field_names();
3540 let _refs: ::std::vec::Vec<&str> =
3541 _names.iter().copied().collect();
3542 self.save_partial(&_refs, pool).await
3543 }
3544 }
3545 }
3546 } else {
3547 quote!()
3548 };
3549
3550 // Audited `insert_pool` (overrides the placeholder set higher up
3551 // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3552 // audited models get insert_pool routing through
3553 // audit::insert_one_with_audit (per-backend tx wraps INSERT
3554 // + auto-PK readback + audit emit). Snapshot-style audit (the
3555 // PendingEntry's `changes` carries post-write field values).
3556 let pool_insert_method = if audited_fields.is_some() {
3557 if let Some(_) = primary_key {
3558 let pushes = if fields.has_auto {
3559 fields.insert_pushes.clone()
3560 } else {
3561 // For non-Auto-PK models, the macro normally builds
3562 // {columns, values} from fields.insert_columns +
3563 // fields.insert_values rather than insert_pushes.
3564 // Map those into the pushes shape.
3565 fields
3566 .insert_columns
3567 .iter()
3568 .zip(&fields.insert_values)
3569 .map(|(col, val)| {
3570 quote! {
3571 _columns.push(#col);
3572 _values.push(#val);
3573 }
3574 })
3575 .collect()
3576 };
3577 let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
3578 fields.returning_cols.clone()
3579 } else {
3580 // Non-Auto-PK: still need RETURNING something for the
3581 // audit helper's contract (it errors on empty
3582 // returning). Return the PK column so the audit row
3583 // can carry the assigned PK back. Some non-Auto PKs
3584 // are server-side-default (e.g. UUIDv4 default), so
3585 // RETURNING is genuinely useful.
3586 primary_key
3587 .map(|(_, col)| {
3588 let lit = col.as_str();
3589 vec![quote!(#lit)]
3590 })
3591 .unwrap_or_default()
3592 };
3593 let pairs = audit_pair_tokens.iter();
3594 let pk_str = audit_pk_to_string.clone();
3595 quote! {
3596 /// Insert this row against either backend with audit
3597 /// emission inside the same transaction. Bi-dialect
3598 /// counterpart of [`Self::insert`] for audited models.
3599 ///
3600 /// Snapshot-style audit (post-write field values).
3601 ///
3602 /// # Errors
3603 /// As [`Self::insert`].
3604 pub async fn insert_pool(
3605 &mut self,
3606 pool: &#root::sql::Pool,
3607 ) -> ::core::result::Result<(), #root::sql::ExecError> {
3608 let mut _columns: ::std::vec::Vec<&'static str> =
3609 ::std::vec::Vec::new();
3610 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3611 ::std::vec::Vec::new();
3612 #( #pushes )*
3613 let _query = #root::core::InsertQuery {
3614 model: <Self as #root::core::Model>::SCHEMA,
3615 columns: _columns,
3616 values: _values,
3617 returning: ::std::vec![ #( #returning_cols ),* ],
3618 on_conflict: ::core::option::Option::None,
3619 };
3620 let _audit_entry = #root::audit::PendingEntry {
3621 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3622 entity_pk: #pk_str,
3623 operation: #root::audit::AuditOp::Create,
3624 source: #root::audit::current_source(),
3625 changes: #root::audit::snapshot_changes(&[
3626 #( #pairs ),*
3627 ]),
3628 };
3629 let _result = #root::audit::insert_one_with_audit(
3630 pool, &_query, &_audit_entry,
3631 ).await?;
3632 #root::sql::apply_auto_pk(_result, self)
3633 }
3634 }
3635 } else {
3636 quote!()
3637 }
3638 } else {
3639 // Keep the non-audited pool_insert_method we built earlier.
3640 pool_insert_method
3641 };
3642
3643 // Update audited save_pool: now that insert_pool is wired for
3644 // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3645 // insert_pool. Non-audited save_pool already does this.
3646 // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3647 // Replaces the snapshot-only emission with a per-backend transaction
3648 // body that:
3649 // 1. SELECTs the tracked columns by PK (typed Row::try_get per
3650 // column), capturing BEFORE values
3651 // 2. compiles the UPDATE via pool.dialect() and runs it on the tx
3652 // 3. builds AFTER pairs from &self
3653 // 4. diffs BEFORE/AFTER, emits one PendingEntry with
3654 // AuditOp::Update + diff_changes(...) on the same tx connection
3655 // 5. commits
3656 //
3657 // Per-backend arms inline the SQL string + placeholder shape, then
3658 // share the `audit_before_pair_tokens` decoder block (Row::try_get
3659 // is polymorphic over Row type — the same tokens work against
3660 // PgRow and MySqlRow as long as the field's Rust type implements
3661 // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3662 // primitives + chrono/uuid/serde_json::Value all do).
3663 let pool_save_method = if let Some(tracked) = audited_fields {
3664 if let Some((pk_ident, pk_col)) = primary_key {
3665 let pk_column_lit = pk_col.as_str();
3666 // Two iterators — quote!'s `#(#var)*` consumes the
3667 // iterator, and we need to splice the same after-pairs
3668 // sequence into both per-backend arms.
3669 let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
3670 let pk_str = audit_pk_to_string.clone();
3671 // Per-tracked-column BEFORE-pair token list. Each entry
3672 // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3673 // The Row alias resolves to PgRow / MySqlRow per call site,
3674 // so the same template generates both the PG and MySQL bodies.
3675 let mk_before_pairs =
3676 |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
3677 tracked
3678 .iter()
3679 .map(|c| {
3680 let column_lit = c.column.as_str();
3681 let value_ty = &c.value_ty;
3682 quote! {
3683 (
3684 #column_lit,
3685 match #getter::<#value_ty>(
3686 _audit_before_row, #column_lit,
3687 ) {
3688 ::core::result::Result::Ok(v) => {
3689 #root::__serde_json::to_value(&v)
3690 .unwrap_or(#root::__serde_json::Value::Null)
3691 }
3692 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3693 },
3694 )
3695 }
3696 })
3697 .collect()
3698 };
3699 let before_pairs_pg: Vec<proc_macro2::TokenStream> =
3700 mk_before_pairs(quote!(#root::sql::try_get_returning));
3701 let before_pairs_my: Vec<proc_macro2::TokenStream> =
3702 mk_before_pairs(quote!(#root::sql::try_get_returning_my));
3703 let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
3704 mk_before_pairs(quote!(#root::sql::try_get_returning_sqlite));
3705 let pg_select_cols: String = tracked
3706 .iter()
3707 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
3708 .collect::<Vec<_>>()
3709 .join(", ");
3710 let my_select_cols: String = tracked
3711 .iter()
3712 .map(|c| format!("`{}`", c.column.replace('`', "``")))
3713 .collect::<Vec<_>>()
3714 .join(", ");
3715 // SQLite uses double-quote identifier quoting (same as
3716 // Postgres in default config), so the column-list shape
3717 // matches PG.
3718 let sqlite_select_cols: String = pg_select_cols.clone();
3719 let pk_value_for_bind = if fields.pk_is_auto {
3720 quote!(self.#pk_ident.get().copied().unwrap_or_default())
3721 } else {
3722 quote!(::core::clone::Clone::clone(&self.#pk_ident))
3723 };
3724 let assignments = &fields.update_assignments;
3725 let unset_dispatch = if fields.has_auto {
3726 quote! {
3727 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3728 return self.insert_pool(pool).await.map(|()| 1u64);
3729 }
3730 }
3731 } else {
3732 quote!()
3733 };
3734 quote! {
3735 /// Save this row against either backend with audit
3736 /// emission (diff-style: BEFORE+AFTER) inside the
3737 /// same transaction. Auto::Unset PK routes to
3738 /// insert_pool. Bi-dialect counterpart of
3739 /// [`Self::save`] for audited models.
3740 ///
3741 /// The audit row's `changes` JSON contains one
3742 /// `{ "field": { "before": …, "after": … } }` entry
3743 /// per tracked column whose value actually changed
3744 /// — same shape as the existing &PgPool save() emits.
3745 ///
3746 /// # Errors
3747 /// As [`Self::save`].
3748 pub async fn save_pool(
3749 &mut self,
3750 pool: &#root::sql::Pool,
3751 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3752 #unset_dispatch
3753 let _query = #root::core::UpdateQuery {
3754 model: <Self as #root::core::Model>::SCHEMA,
3755 set: ::std::vec![ #( #assignments ),* ],
3756 where_clause: #root::core::WhereExpr::Predicate(
3757 #root::core::Filter {
3758 column: #pk_column_lit,
3759 op: #root::core::Op::Eq,
3760 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3761 ::core::clone::Clone::clone(&self.#pk_ident)
3762 ),
3763 }
3764 ),
3765 };
3766 let _after_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3767 ::std::vec![ #( #after_pairs_pg ),* ];
3768 #root::audit::save_one_with_diff(
3769 pool,
3770 &_query,
3771 #pk_column_lit,
3772 ::core::convert::Into::<#root::core::SqlValue>::into(
3773 #pk_value_for_bind,
3774 ),
3775 <Self as #root::core::Model>::SCHEMA.table,
3776 #pk_str,
3777 _after_pairs,
3778 #pg_select_cols,
3779 #my_select_cols,
3780 #sqlite_select_cols,
3781 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
3782 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
3783 |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
3784 ).await
3785 }
3786 }
3787 } else {
3788 quote!()
3789 }
3790 } else {
3791 pool_save_method
3792 };
3793
3794 // `delete_pool(&Pool)` — emitted for every model with a PK. Two
3795 // body shapes:
3796 // - non-audited: simple dispatch through `sql::delete_pool`
3797 // - audited: routes through `audit::delete_one_with_audit`,
3798 // which opens a per-backend transaction wrapping DELETE +
3799 // audit emit so the data write and audit row commit atomically.
3800 let pool_delete_method = {
3801 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
3802 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
3803 if let Some(pk_ident) = pk_ident_for_pool {
3804 if audited_fields.is_some() {
3805 let pairs = audit_pair_tokens.iter();
3806 let pk_str = audit_pk_to_string.clone();
3807 quote! {
3808 /// Delete this row against either backend with audit
3809 /// emission inside the same transaction. Bi-dialect
3810 /// counterpart of [`Self::delete`] for audited models.
3811 ///
3812 /// # Errors
3813 /// As [`Self::delete`].
3814 pub async fn delete_pool(
3815 &self,
3816 pool: &#root::sql::Pool,
3817 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3818 let _query = #root::core::DeleteQuery {
3819 model: <Self as #root::core::Model>::SCHEMA,
3820 where_clause: #root::core::WhereExpr::Predicate(
3821 #root::core::Filter {
3822 column: #pk_column_lit,
3823 op: #root::core::Op::Eq,
3824 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3825 ::core::clone::Clone::clone(&self.#pk_ident)
3826 ),
3827 }
3828 ),
3829 };
3830 let _audit_entry = #root::audit::PendingEntry {
3831 entity_table: <Self as #root::core::Model>::SCHEMA.table,
3832 entity_pk: #pk_str,
3833 operation: #root::audit::AuditOp::Delete,
3834 source: #root::audit::current_source(),
3835 changes: #root::audit::snapshot_changes(&[
3836 #( #pairs ),*
3837 ]),
3838 };
3839 #root::audit::delete_one_with_audit(
3840 pool, &_query, &_audit_entry,
3841 ).await
3842 }
3843 }
3844 } else {
3845 quote! {
3846 /// Delete the row identified by this instance's primary key
3847 /// against either backend. Equivalent to [`Self::delete`] but
3848 /// takes [`#root::sql::Pool`] and dispatches per backend.
3849 ///
3850 /// # Errors
3851 /// As [`Self::delete`].
3852 pub async fn delete_pool(
3853 &self,
3854 pool: &#root::sql::Pool,
3855 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3856 let _query = #root::core::DeleteQuery {
3857 model: <Self as #root::core::Model>::SCHEMA,
3858 where_clause: #root::core::WhereExpr::Predicate(
3859 #root::core::Filter {
3860 column: #pk_column_lit,
3861 op: #root::core::Op::Eq,
3862 value: ::core::convert::Into::<#root::core::SqlValue>::into(
3863 ::core::clone::Clone::clone(&self.#pk_ident)
3864 ),
3865 }
3866 ),
3867 };
3868 #root::sql::delete_pool(pool, &_query).await
3869 }
3870 }
3871 }
3872 } else {
3873 quote!()
3874 }
3875 };
3876
3877 // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
3878 // matching this instance's PK and overwrite the in-memory state
3879 // with the freshly-fetched columns. Django's `refresh_from_db`.
3880 // Issue #825. Only emitted when the model declares a PK; non-PK
3881 // models can't address a specific row.
3882 //
3883 // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
3884 // every field from `self`; resets the PK to `Auto::Unset` when
3885 // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
3886 // a fresh autoincrement value. Non-Auto PKs preserve the source
3887 // PK — the caller must overwrite before insert. Pure-Rust, no
3888 // I/O, no dialect surface.
3889 let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
3890 let other_field_clones: Vec<TokenStream2> = fields
3891 .column_entries
3892 .iter()
3893 .filter(|c| &c.ident != pk_ident)
3894 .map(|c| {
3895 let ident = &c.ident;
3896 quote! {
3897 #ident: ::core::clone::Clone::clone(&self.#ident)
3898 }
3899 })
3900 .collect();
3901 let pk_clone_token = if fields.pk_is_auto {
3902 quote! { #pk_ident: #root::sql::Auto::Unset }
3903 } else {
3904 quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
3905 };
3906 let replicate_doc = if fields.pk_is_auto {
3907 quote! {
3908 /// Eloquent-style `replicate()` — return a clone of this
3909 /// row with the primary key reset to [`Auto::Unset`] so
3910 /// the copy is ready for `insert_pool` / `save_pool` to
3911 /// allocate a fresh autoincrement value. Every other
3912 /// field is `Clone`d verbatim. Issue #825.
3913 ///
3914 /// `auto_now_add` / `auto_now` timestamp fields are
3915 /// **not** reset (Eloquent's `replicate` doesn't reset
3916 /// them either) — pass them through the normal insert
3917 /// path if you want fresh values, or assign them
3918 /// explicitly after the call.
3919 }
3920 } else {
3921 quote! {
3922 /// Eloquent-style `replicate()` — clone this row
3923 /// verbatim. Because the primary key is **not** an
3924 /// `Auto<T>`, the clone keeps the source PK; the
3925 /// caller must overwrite `copy.<pk>` before inserting
3926 /// to avoid a unique-key violation. Issue #825.
3927 }
3928 };
3929 // 2026-06-07 — field-name / shortcut collision guard.
3930 //
3931 // The macro emits both `pub const <field>: <field>_col = ...`
3932 // (per-field typed-column const, used by the typed-builder
3933 // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
3934 // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
3935 // model has a field named e.g. `count`, both items would land
3936 // in the same inherent impl with the same name, and the
3937 // compiler rejects the derive with "duplicate definitions".
3938 //
3939 // Drop the conflicting shortcut for that model. Callers can
3940 // still reach the same behavior via
3941 // `QuerySet::<Model>::default().count(&pool)`.
3942 let column_names: ::std::collections::HashSet<String> = fields
3943 .column_entries
3944 .iter()
3945 .map(|c| c.ident.to_string())
3946 .collect();
3947 let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
3948 if column_names.contains(name) {
3949 quote! {}
3950 } else {
3951 tokens
3952 }
3953 };
3954 let count_method = emit_if_no_field_collision(
3955 "count",
3956 quote! {
3957 /// Count rows of this model — `SELECT COUNT(*) FROM
3958 /// <table>`. Eloquent `Model::count()` parity.
3959 ///
3960 /// Skipped on models that already declare a field named
3961 /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
3962 /// in that case.
3963 ///
3964 /// # Errors
3965 /// As [`CounterPool::count`].
3966 ///
3967 /// [`CounterPool::count`]: rustango::sql::CounterPool::count
3968 pub async fn count(
3969 pool: &#root::sql::Pool,
3970 ) -> ::core::result::Result<i64, #root::sql::ExecError> {
3971 use #root::sql::CounterPool as _;
3972 #root::query::QuerySet::<Self>::default()
3973 .count(pool)
3974 .await
3975 }
3976 },
3977 );
3978 let value_method = emit_if_no_field_collision(
3979 "value",
3980 quote! {
3981 /// Pluck a single scalar from the first row.
3982 /// Eloquent `Model::query()->value($col)` parity.
3983 ///
3984 /// Skipped on models that already declare a field named
3985 /// `value`. Drop into
3986 /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
3987 ///
3988 /// # Errors
3989 /// As `ValuesFlatQuerySet::first`.
3990 pub async fn value<U>(
3991 col: &str,
3992 pool: &#root::sql::Pool,
3993 ) -> ::core::result::Result<
3994 ::core::option::Option<U>,
3995 #root::sql::ExecError,
3996 >
3997 where
3998 U: #root::sql::MaybePgScalar
3999 + #root::sql::MaybeMyScalar
4000 + #root::sql::MaybeSqliteScalar
4001 + ::core::marker::Send
4002 + ::core::marker::Unpin,
4003 {
4004 let _col_static: &'static str = Self::__resolve_col(col)?;
4005 #root::query::QuerySet::<Self>::default()
4006 .values_list_flat(_col_static)
4007 .first::<U>(pool)
4008 .await
4009 }
4010 },
4011 );
4012 let sum_method = emit_if_no_field_collision(
4013 "sum",
4014 quote! {
4015 /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4016 /// Skipped on models that already declare a field named
4017 /// `sum`.
4018 ///
4019 /// # Errors
4020 /// As [`#root::sql::fetch_aggregate_pool`].
4021 pub async fn sum<U>(
4022 col: &str,
4023 pool: &#root::sql::Pool,
4024 ) -> ::core::result::Result<
4025 ::core::option::Option<U>,
4026 #root::sql::ExecError,
4027 >
4028 where
4029 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4030 + #root::sql::MaybeMyFromRow
4031 + #root::sql::MaybeSqliteFromRow
4032 + ::core::marker::Send
4033 + ::core::marker::Unpin,
4034 {
4035 Self::__aggregate_one_pool::<U>(
4036 col,
4037 |c| #root::core::AggregateExpr::Sum(c),
4038 pool,
4039 )
4040 .await
4041 }
4042 },
4043 );
4044 let avg_method = emit_if_no_field_collision(
4045 "avg",
4046 quote! {
4047 /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4048 /// models that already declare a field named `avg`.
4049 ///
4050 /// # Errors
4051 /// As [`#root::sql::fetch_aggregate_pool`].
4052 pub async fn avg<U>(
4053 col: &str,
4054 pool: &#root::sql::Pool,
4055 ) -> ::core::result::Result<
4056 ::core::option::Option<U>,
4057 #root::sql::ExecError,
4058 >
4059 where
4060 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4061 + #root::sql::MaybeMyFromRow
4062 + #root::sql::MaybeSqliteFromRow
4063 + ::core::marker::Send
4064 + ::core::marker::Unpin,
4065 {
4066 Self::__aggregate_one_pool::<U>(
4067 col,
4068 |c| #root::core::AggregateExpr::Avg(c),
4069 pool,
4070 )
4071 .await
4072 }
4073 },
4074 );
4075 let min_method = emit_if_no_field_collision(
4076 "min",
4077 quote! {
4078 /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4079 /// models that already declare a field named `min`.
4080 ///
4081 /// # Errors
4082 /// As [`#root::sql::fetch_aggregate_pool`].
4083 pub async fn min<U>(
4084 col: &str,
4085 pool: &#root::sql::Pool,
4086 ) -> ::core::result::Result<
4087 ::core::option::Option<U>,
4088 #root::sql::ExecError,
4089 >
4090 where
4091 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4092 + #root::sql::MaybeMyFromRow
4093 + #root::sql::MaybeSqliteFromRow
4094 + ::core::marker::Send
4095 + ::core::marker::Unpin,
4096 {
4097 Self::__aggregate_one_pool::<U>(
4098 col,
4099 |c| #root::core::AggregateExpr::Min(c),
4100 pool,
4101 )
4102 .await
4103 }
4104 },
4105 );
4106 let max_method = emit_if_no_field_collision(
4107 "max",
4108 quote! {
4109 /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4110 /// models that already declare a field named `max`.
4111 ///
4112 /// # Errors
4113 /// As [`#root::sql::fetch_aggregate_pool`].
4114 pub async fn max<U>(
4115 col: &str,
4116 pool: &#root::sql::Pool,
4117 ) -> ::core::result::Result<
4118 ::core::option::Option<U>,
4119 #root::sql::ExecError,
4120 >
4121 where
4122 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4123 + #root::sql::MaybeMyFromRow
4124 + #root::sql::MaybeSqliteFromRow
4125 + ::core::marker::Send
4126 + ::core::marker::Unpin,
4127 {
4128 Self::__aggregate_one_pool::<U>(
4129 col,
4130 |c| #root::core::AggregateExpr::Max(c),
4131 pool,
4132 )
4133 .await
4134 }
4135 },
4136 );
4137 let first_method = emit_if_no_field_collision(
4138 "first",
4139 quote! {
4140 /// First row of this model. Eloquent `Model::first()`.
4141 /// Skipped on models that already declare a field named
4142 /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4143 ///
4144 /// # Errors
4145 /// As `QuerySet::first`.
4146 pub async fn first(
4147 pool: &#root::sql::Pool,
4148 ) -> ::core::result::Result<
4149 ::core::option::Option<Self>,
4150 #root::sql::ExecError,
4151 > {
4152 #root::query::QuerySet::<Self>::default()
4153 .first(pool)
4154 .await
4155 }
4156 },
4157 );
4158 let last_method = emit_if_no_field_collision(
4159 "last",
4160 quote! {
4161 /// Last row of this model by primary-key DESC.
4162 /// Eloquent `Model::query()->latest('id')->first()`
4163 /// parity — fetches the highest-PK row without
4164 /// requiring the caller to spell the PK column.
4165 /// Returns `None` on an empty table.
4166 ///
4167 /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4168 /// Skipped on models that already declare a field
4169 /// named `last`.
4170 ///
4171 /// # Errors
4172 /// As `QuerySet::last`.
4173 pub async fn last(
4174 pool: &#root::sql::Pool,
4175 ) -> ::core::result::Result<
4176 ::core::option::Option<Self>,
4177 #root::sql::ExecError,
4178 > {
4179 #root::query::QuerySet::<Self>::default()
4180 .last(pool)
4181 .await
4182 }
4183 },
4184 );
4185 quote! {
4186 /// Re-SELECT this row by its primary key and overwrite
4187 /// every in-memory field with the freshly-fetched value.
4188 /// Django's [`Model.refresh_from_db`]. Issue #825.
4189 ///
4190 /// Use this when the row may have been modified by another
4191 /// process / connection / job since you read it — e.g. after
4192 /// a queued task callback, or to re-sync stale UI state
4193 /// before re-saving.
4194 ///
4195 /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4196 /// when the primary key no longer matches any row (e.g.
4197 /// the row was deleted concurrently).
4198 ///
4199 /// # Errors
4200 /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4201 /// the PK no longer exists.
4202 ///
4203 /// [`Model.refresh_from_db`]: https://docs.djangoproject.com/en/5.1/ref/models/instances/#django.db.models.Model.refresh_from_db
4204 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4205 pub async fn refresh_from_db(
4206 &mut self,
4207 pool: &#root::sql::Pool,
4208 ) -> ::core::result::Result<(), #root::sql::ExecError> {
4209 use #root::sql::FetcherPool as _;
4210 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4211 ::core::clone::Clone::clone(&self.#pk_ident),
4212 );
4213 let mut _rows: ::std::vec::Vec<Self> =
4214 #root::query::QuerySet::<Self>::default()
4215 .filter(::core::stringify!(#pk_ident), _pk_val)
4216 .limit(1)
4217 .fetch(pool)
4218 .await?;
4219 match _rows.into_iter().next() {
4220 ::core::option::Option::Some(_fresh) => {
4221 *self = _fresh;
4222 ::core::result::Result::Ok(())
4223 }
4224 ::core::option::Option::None => ::core::result::Result::Err(
4225 #root::sql::ExecError::Driver(
4226 #root::sql::sqlx::Error::RowNotFound,
4227 ),
4228 ),
4229 }
4230 }
4231
4232 /// Atomically increment the integer column `col` by
4233 /// `by` for this row. Equivalent to
4234 /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4235 /// Eloquent `Model::increment($col, $by)` / Django
4236 /// `Model.objects.filter(pk=…).update(col=F('col')+$by)`
4237 /// parity.
4238 ///
4239 /// **Doesn't mutate `self`** — the in-memory copy is now
4240 /// stale; call [`Self::refresh_from_db_pool`] /
4241 /// [`Self::fresh_pool`] to re-sync. Returns the rows-
4242 /// affected count (0 when the PK doesn't match any row,
4243 /// 1 on success).
4244 ///
4245 /// `col` is the Rust field name as a string; unknown
4246 /// fields surface as `UnknownField` at runtime. Negative
4247 /// `by` values atomically decrement (see also
4248 /// [`Self::decrement_pool`]).
4249 ///
4250 /// # Errors
4251 /// As [`UpdaterPool::execute_pool`].
4252 ///
4253 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4254 pub async fn increment(
4255 &self,
4256 col: &str,
4257 by: i64,
4258 pool: &#root::sql::Pool,
4259 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4260 Self::__increment_one(self, col, by, pool).await
4261 }
4262
4263 /// Sibling of [`Self::increment`] — atomically
4264 /// decrement this row's `col` by `by`. Eloquent
4265 /// `$model->decrement($col, $by)` parity. Equivalent to
4266 /// `self.increment(col, -by, &pool)`; the separate name
4267 /// keeps call sites readable.
4268 ///
4269 /// # Errors
4270 /// As [`Self::increment`].
4271 pub async fn decrement(
4272 &self,
4273 col: &str,
4274 by: i64,
4275 pool: &#root::sql::Pool,
4276 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4277 Self::__increment_one(self, col, -by, pool).await
4278 }
4279
4280 /// Bulk-increment: add `by` to `col` on every row of the
4281 /// table. Eloquent `Model::query()->increment($col, $by)`
4282 /// parity. Use for counters, score adjustments, view
4283 /// rollups.
4284 ///
4285 /// # Errors
4286 /// As [`UpdaterPool::execute_pool`].
4287 ///
4288 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4289 pub async fn increment_each(
4290 col: &str,
4291 by: i64,
4292 pool: &#root::sql::Pool,
4293 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4294 Self::__increment_all(col, by, pool).await
4295 }
4296
4297 /// Sibling of [`Self::increment_each`] — bulk-decrement.
4298 ///
4299 /// # Errors
4300 /// As [`Self::increment_each`].
4301 pub async fn decrement_each(
4302 col: &str,
4303 by: i64,
4304 pool: &#root::sql::Pool,
4305 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4306 Self::__increment_all(col, -by, pool).await
4307 }
4308
4309 /// Internal: forward to
4310 /// [`#root::sql::model_shortcuts::increment_one_pool`].
4311 /// One-line wrapper kept as a per-Model method so the
4312 /// macro's emitted `increment` / `decrement` instance
4313 /// calls don't have to thread `Self` through manually.
4314 #[doc(hidden)]
4315 pub async fn __increment_one(
4316 this: &Self,
4317 col: &str,
4318 by: i64,
4319 pool: &#root::sql::Pool,
4320 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4321 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4322 ::core::clone::Clone::clone(&this.#pk_ident),
4323 );
4324 #root::sql::model_shortcuts::increment_one_pool::<Self>(
4325 ::core::stringify!(#pk_ident),
4326 _pk_val,
4327 col,
4328 by,
4329 pool,
4330 )
4331 .await
4332 }
4333
4334 /// Internal: forward to
4335 /// [`#root::sql::model_shortcuts::increment_all_pool`].
4336 #[doc(hidden)]
4337 pub async fn __increment_all(
4338 col: &str,
4339 by: i64,
4340 pool: &#root::sql::Pool,
4341 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4342 #root::sql::model_shortcuts::increment_all_pool::<Self>(col, by, pool).await
4343 }
4344
4345 /// Internal: forward to
4346 /// [`#root::sql::model_shortcuts::resolve_col`].
4347 #[doc(hidden)]
4348 pub fn __resolve_col(
4349 col: &str,
4350 ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4351 #root::sql::model_shortcuts::resolve_col::<Self>(col)
4352 }
4353
4354 /// Internal: forward to
4355 /// [`#root::sql::model_shortcuts::add_signed_expr`].
4356 #[doc(hidden)]
4357 #[must_use]
4358 pub fn __add_signed_expr(
4359 col_static: &'static str,
4360 signed_by: i64,
4361 ) -> #root::core::Expr {
4362 #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4363 }
4364
4365 /// Re-SELECT this row by its primary key and return a
4366 /// **new** instance with the freshly-fetched fields.
4367 /// Eloquent `Model::fresh()` parity — non-mutating
4368 /// counterpart of [`Self::refresh_from_db_pool`].
4369 ///
4370 /// Returns `Ok(None)` when the row was deleted
4371 /// concurrently — vs [`Self::refresh_from_db_pool`]
4372 /// which surfaces that as `RowNotFound` because
4373 /// in-place mutation has nothing to write to.
4374 ///
4375 /// Useful when you want to compare the in-memory
4376 /// instance against the persisted state (audit-style
4377 /// diffs, conflict detection) without mutating the
4378 /// reference you already hold.
4379 ///
4380 /// # Errors
4381 /// As [`FetcherPool::fetch`].
4382 ///
4383 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4384 pub async fn fresh(
4385 &self,
4386 pool: &#root::sql::Pool,
4387 ) -> ::core::result::Result<
4388 ::core::option::Option<Self>,
4389 #root::sql::ExecError,
4390 > {
4391 use #root::sql::FetcherPool as _;
4392 let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4393 ::core::clone::Clone::clone(&self.#pk_ident),
4394 );
4395 let _rows: ::std::vec::Vec<Self> =
4396 #root::query::QuerySet::<Self>::default()
4397 .filter(::core::stringify!(#pk_ident), _pk_val)
4398 .limit(1)
4399 .fetch(pool)
4400 .await?;
4401 ::core::result::Result::Ok(_rows.into_iter().next())
4402 }
4403
4404 #replicate_doc
4405 #[must_use]
4406 pub fn replicate(&self) -> Self {
4407 Self {
4408 #pk_clone_token,
4409 #( #other_field_clones, )*
4410 }
4411 }
4412
4413 #first_method
4414
4415 #last_method
4416
4417 /// Throwing counterpart of [`Self::first_pool`] —
4418 /// errors with `RowNotFound` when the table is empty.
4419 /// Eloquent `Model::firstOrFail()` parity.
4420 ///
4421 /// # Errors
4422 /// As [`Self::first_pool`]; additionally
4423 /// [`sqlx::Error::RowNotFound`] on empty tables.
4424 ///
4425 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4426 pub async fn first_or_fail(
4427 pool: &#root::sql::Pool,
4428 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4429 // Route through the queryset rather than `Self::first`,
4430 // which is suppressed on models with a field named
4431 // `first` (field/shortcut collision guard).
4432 match #root::query::QuerySet::<Self>::default().first(pool).await? {
4433 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4434 ::core::option::Option::None => ::core::result::Result::Err(
4435 #root::sql::ExecError::Driver(
4436 #root::sql::sqlx::Error::RowNotFound,
4437 ),
4438 ),
4439 }
4440 }
4441
4442 /// Single-column projection — `SELECT <col> FROM
4443 /// <table>`. Returns `Vec<U>` where each element is the
4444 /// decoded value of the column. Eloquent
4445 /// `Model::pluck($column)` / Django
4446 /// `Model.objects.values_list('col', flat=True)` parity.
4447 ///
4448 /// Thin wrapper over `QuerySet::<Self>::default()
4449 /// .values_list_flat(col).fetch::<U>(pool)`. `U` must
4450 /// be decodable from the column's SQL type on every
4451 /// dialect the binary targets (common picks: `i64` /
4452 /// `i32` / `String` / `bool` / `f64`).
4453 ///
4454 /// # Errors
4455 /// As `ValuesFlatQuerySet::fetch`.
4456 pub async fn pluck<U>(
4457 col: &'static str,
4458 pool: &#root::sql::Pool,
4459 ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4460 where
4461 U: #root::sql::MaybePgScalar
4462 + #root::sql::MaybeMyScalar
4463 + #root::sql::MaybeSqliteScalar
4464 + ::core::marker::Send
4465 + ::core::marker::Unpin,
4466 {
4467 #root::query::QuerySet::<Self>::default()
4468 .values_list_flat(col)
4469 .fetch::<U>(pool)
4470 .await
4471 }
4472
4473 /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4474 /// stream every row of this model in batches of `n`,
4475 /// invoking the callback once per batch. Stable PK-ASC
4476 /// ordering so the LIMIT/OFFSET pagination is
4477 /// deterministic across drivers.
4478 ///
4479 /// The callback is async — it can do further DB work
4480 /// (writes, related-row lookups, queue dispatch) per
4481 /// batch. Return `Err(...)` from the callback to abort
4482 /// the iteration early; the error bubbles up.
4483 ///
4484 /// **When to use**: bulk processing flows that can't
4485 /// fit the whole table in memory — sending newsletters,
4486 /// running data migrations, computing summary
4487 /// statistics. For small / known-bounded tables, plain
4488 /// `Self::all(&pool)` is simpler.
4489 ///
4490 /// **Caveat**: ascending OFFSET pagination is O(N²) on
4491 /// large tables. For multi-million-row scans prefer
4492 /// keyset-by-PK (the standard "WHERE id > last_seen"
4493 /// shape) over `chunk(...)`.
4494 ///
4495 /// Skipped on models without a primary key — chunking
4496 /// needs a stable order to avoid skipping / repeating
4497 /// rows across batches.
4498 pub async fn chunk<F, Fut>(
4499 n: i64,
4500 pool: &#root::sql::Pool,
4501 mut cb: F,
4502 ) -> ::core::result::Result<(), #root::sql::ExecError>
4503 where
4504 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4505 Fut: ::core::future::Future<
4506 Output = ::core::result::Result<(), #root::sql::ExecError>,
4507 >,
4508 {
4509 use #root::sql::FetcherPool as _;
4510 let pk_col = match Self::primary_key_column() {
4511 ::core::option::Option::Some(c) => c,
4512 ::core::option::Option::None => {
4513 return ::core::result::Result::Ok(());
4514 }
4515 };
4516 let mut offset: i64 = 0;
4517 loop {
4518 let rows: ::std::vec::Vec<Self> =
4519 #root::query::QuerySet::<Self>::default()
4520 .order_by(&[(pk_col, false)])
4521 .limit(n)
4522 .offset(offset)
4523 .fetch(pool)
4524 .await?;
4525 if rows.is_empty() {
4526 return ::core::result::Result::Ok(());
4527 }
4528 let len = rows.len() as i64;
4529 cb(rows).await?;
4530 if len < n {
4531 return ::core::result::Result::Ok(());
4532 }
4533 offset += n;
4534 }
4535 }
4536
4537 /// Eloquent `Model::chunkById($n, fn (...))` — same
4538 /// per-batch callback shape as [`Self::chunk`], but uses
4539 /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4540 /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4541 /// — the right choice for multi-million-row sweeps.
4542 ///
4543 /// Requires the primary key to be a signed integer type
4544 /// (`i64` / `i32`); the keyset comparison rides on
4545 /// `__rustango_pk_value()` lowering through
4546 /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4547 /// non-integer PKs (UUID, String) — those should use
4548 /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4549 /// keyset loop.
4550 ///
4551 /// Callback errors abort iteration; the error bubbles up
4552 /// unchanged. Empty table → callback invoked zero times.
4553 pub async fn chunk_by_id<F, Fut>(
4554 n: i64,
4555 pool: &#root::sql::Pool,
4556 mut cb: F,
4557 ) -> ::core::result::Result<(), #root::sql::ExecError>
4558 where
4559 F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4560 Fut: ::core::future::Future<
4561 Output = ::core::result::Result<(), #root::sql::ExecError>,
4562 >,
4563 {
4564 use #root::sql::FetcherPool as _;
4565 let pk_col = match Self::primary_key_column() {
4566 ::core::option::Option::Some(c) => c,
4567 ::core::option::Option::None => {
4568 return ::core::result::Result::Ok(());
4569 }
4570 };
4571 // Track the largest PK seen so the next batch picks
4572 // up from there. `i64::MIN` as the sentinel — the
4573 // very first iteration's `> MIN` matches every row,
4574 // so the loop entry is uniform with subsequent
4575 // iterations.
4576 let mut last_seen: i64 = i64::MIN;
4577 loop {
4578 let key = ::std::format!("{}__gt", pk_col);
4579 let rows: ::std::vec::Vec<Self> =
4580 #root::query::QuerySet::<Self>::default()
4581 .filter(key.as_str(), last_seen)
4582 .order_by(&[(pk_col, false)])
4583 .limit(n)
4584 .fetch(pool)
4585 .await?;
4586 if rows.is_empty() {
4587 return ::core::result::Result::Ok(());
4588 }
4589 let len = rows.len() as i64;
4590 // Capture the last row's PK BEFORE moving rows
4591 // into the callback.
4592 let max_pk = match rows
4593 .last()
4594 .map(|r| r.__rustango_pk_value())
4595 {
4596 ::core::option::Option::Some(
4597 #root::core::SqlValue::I64(v),
4598 ) => v,
4599 ::core::option::Option::Some(
4600 #root::core::SqlValue::I32(v),
4601 ) => i64::from(v),
4602 _ => return ::core::result::Result::Ok(()),
4603 };
4604 cb(rows).await?;
4605 if len < n {
4606 return ::core::result::Result::Ok(());
4607 }
4608 last_seen = max_pk;
4609 }
4610 }
4611
4612 /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4613 /// per-row callback companion to [`Self::chunk`].
4614 /// Streams every row in keyset-paginated batches of
4615 /// `batch` size, calling `cb` once per row.
4616 ///
4617 /// Inherits the keyset-paginated scan of
4618 /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4619 /// non-integer PKs are silently a no-op).
4620 ///
4621 /// ```ignore
4622 /// Post::each(500, &pool, |p| async move {
4623 /// reindex(p).await?;
4624 /// Ok(())
4625 /// }).await?;
4626 /// ```
4627 ///
4628 /// Return `Err(...)` from the callback to abort
4629 /// iteration; the error bubbles up unchanged.
4630 pub async fn each<F, Fut>(
4631 batch: i64,
4632 pool: &#root::sql::Pool,
4633 mut cb: F,
4634 ) -> ::core::result::Result<(), #root::sql::ExecError>
4635 where
4636 F: ::core::ops::FnMut(Self) -> Fut,
4637 Fut: ::core::future::Future<
4638 Output = ::core::result::Result<(), #root::sql::ExecError>,
4639 >,
4640 {
4641 use #root::sql::FetcherPool as _;
4642 let pk_col = match Self::primary_key_column() {
4643 ::core::option::Option::Some(c) => c,
4644 ::core::option::Option::None => {
4645 return ::core::result::Result::Ok(());
4646 }
4647 };
4648 let mut last_seen: i64 = i64::MIN;
4649 loop {
4650 let key = ::std::format!("{}__gt", pk_col);
4651 let rows: ::std::vec::Vec<Self> =
4652 #root::query::QuerySet::<Self>::default()
4653 .filter(key.as_str(), last_seen)
4654 .order_by(&[(pk_col, false)])
4655 .limit(batch)
4656 .fetch(pool)
4657 .await?;
4658 if rows.is_empty() {
4659 return ::core::result::Result::Ok(());
4660 }
4661 let len = rows.len() as i64;
4662 let max_pk = match rows
4663 .last()
4664 .map(|r| r.__rustango_pk_value())
4665 {
4666 ::core::option::Option::Some(
4667 #root::core::SqlValue::I64(v),
4668 ) => v,
4669 ::core::option::Option::Some(
4670 #root::core::SqlValue::I32(v),
4671 ) => i64::from(v),
4672 _ => return ::core::result::Result::Ok(()),
4673 };
4674 for row in rows {
4675 cb(row).await?;
4676 }
4677 if len < batch {
4678 return ::core::result::Result::Ok(());
4679 }
4680 last_seen = max_pk;
4681 }
4682 }
4683
4684 /// Delete every row of this model — `TRUNCATE TABLE
4685 /// <table> RESTART IDENTITY CASCADE` on Postgres,
4686 /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4687 /// support `TRUNCATE` inside foreign-key constraints
4688 /// or — for SQLite — at all). Eloquent `Model::truncate()`
4689 /// / Django `Model.objects.all().delete()` parity.
4690 ///
4691 /// **Use only in tests / fixture-reset flows.** Production
4692 /// writes through this would silently bypass the
4693 /// `pre_delete` / `post_delete` signals (no per-row hooks
4694 /// fire on a TRUNCATE / bulk DELETE FROM) and lose every
4695 /// row's audit-log entry.
4696 ///
4697 /// # Errors
4698 /// As [`raw_execute_pool`].
4699 ///
4700 /// [`raw_execute_pool`]: rustango::sql::raw_execute_pool
4701 pub async fn truncate(
4702 pool: &#root::sql::Pool,
4703 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4704 let _table = <Self as #root::core::Model>::SCHEMA.table;
4705 let _dialect = pool.dialect();
4706 let _quoted = _dialect.quote_ident(_table);
4707 let _sql = if _dialect.name() == "postgres" {
4708 ::std::format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", _quoted)
4709 } else {
4710 ::std::format!("DELETE FROM {}", _quoted)
4711 };
4712 #root::sql::raw_execute_pool(pool, &_sql, ::std::vec::Vec::new()).await
4713 }
4714
4715 /// Bulk-delete every row whose primary key is in
4716 /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4717 /// Returns the affected row count.
4718 ///
4719 /// Eloquent `Model::destroy([1, 2, 3])` / Django
4720 /// `Model.objects.filter(pk__in=[...]).delete()` parity.
4721 /// Empty `pks` is a no-op (returns 0).
4722 ///
4723 /// Accepts any iterable whose elements are
4724 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4725 /// `[i64; N]`, etc.
4726 ///
4727 /// # Errors
4728 /// As `delete_pool`.
4729 pub async fn destroy<V>(
4730 pks: impl ::core::iter::IntoIterator<Item = V>,
4731 pool: &#root::sql::Pool,
4732 ) -> ::core::result::Result<u64, #root::sql::ExecError>
4733 where
4734 V: ::core::convert::Into<#root::core::SqlValue>,
4735 {
4736 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4737 pks.into_iter().map(::core::convert::Into::into).collect();
4738 if _values.is_empty() {
4739 return ::core::result::Result::Ok(0);
4740 }
4741 let _query = #root::core::DeleteQuery {
4742 model: <Self as #root::core::Model>::SCHEMA,
4743 where_clause: #root::core::WhereExpr::Predicate(
4744 #root::core::Filter {
4745 column: <Self as #root::core::Model>::SCHEMA
4746 .primary_key()
4747 .ok_or_else(|| {
4748 #root::sql::ExecError::Sql(
4749 #root::sql::SqlError::MissingPrimaryKey,
4750 )
4751 })?
4752 .column,
4753 op: #root::core::Op::In,
4754 value: #root::core::SqlValue::List(_values),
4755 },
4756 ),
4757 };
4758 #root::sql::delete_pool(pool, &_query).await
4759 }
4760
4761 /// Fetch every row where `<col> = <val>`. Eloquent
4762 /// `Model::where($col, $val)->get()` / Django
4763 /// `Model.objects.filter(col=val).all()` parity.
4764 ///
4765 /// Thin wrapper over `QuerySet::<Self>::default()
4766 /// .filter(col, val).fetch(pool)`. For one row,
4767 /// use [`Self::first_where_pool`]; for a chain that
4768 /// needs further `.filter()` / `.order_by()` /
4769 /// `.limit()`, drop down to `Self::query().filter(...)`
4770 /// directly.
4771 ///
4772 /// `val` accepts any value `Into<SqlValue>` so plain
4773 /// strings, ints, UUIDs, etc. all work.
4774 ///
4775 /// # Errors
4776 /// As [`FetcherPool::fetch`].
4777 ///
4778 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4779 pub async fn where_(
4780 col: &str,
4781 val: impl ::core::convert::Into<#root::core::SqlValue>,
4782 pool: &#root::sql::Pool,
4783 ) -> ::core::result::Result<
4784 ::std::vec::Vec<Self>,
4785 #root::sql::ExecError,
4786 > {
4787 use #root::sql::FetcherPool as _;
4788 #root::query::QuerySet::<Self>::default()
4789 .filter(col, val)
4790 .fetch(pool)
4791 .await
4792 }
4793
4794 /// Fetch every row where `<col> IN (vals)`. Eloquent
4795 /// `Model::whereIn($col, $vals)->get()` parity. Empty
4796 /// `vals` returns no rows (matches SQL's empty-IN
4797 /// semantics).
4798 ///
4799 /// `vals` accepts any iterable whose items are
4800 /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
4801 /// etc.
4802 ///
4803 /// # Errors
4804 /// As [`FetcherPool::fetch`].
4805 ///
4806 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4807 pub async fn where_in<V>(
4808 col: &str,
4809 vals: impl ::core::iter::IntoIterator<Item = V>,
4810 pool: &#root::sql::Pool,
4811 ) -> ::core::result::Result<
4812 ::std::vec::Vec<Self>,
4813 #root::sql::ExecError,
4814 >
4815 where
4816 V: ::core::convert::Into<#root::core::SqlValue>,
4817 {
4818 use #root::sql::FetcherPool as _;
4819 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4820 vals.into_iter().map(::core::convert::Into::into).collect();
4821 if _values.is_empty() {
4822 return ::core::result::Result::Ok(::std::vec::Vec::new());
4823 }
4824 let _key = ::std::format!("{}__in", col);
4825 #root::query::QuerySet::<Self>::default()
4826 .filter(&_key, #root::core::SqlValue::List(_values))
4827 .fetch(pool)
4828 .await
4829 }
4830
4831 /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
4832 /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
4833 /// `vals` returns every row (matches SQL's empty-NOT-IN
4834 /// semantics — vacuously true for every row).
4835 ///
4836 /// # Errors
4837 /// As [`FetcherPool::fetch`].
4838 ///
4839 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4840 pub async fn where_not_in<V>(
4841 col: &str,
4842 vals: impl ::core::iter::IntoIterator<Item = V>,
4843 pool: &#root::sql::Pool,
4844 ) -> ::core::result::Result<
4845 ::std::vec::Vec<Self>,
4846 #root::sql::ExecError,
4847 >
4848 where
4849 V: ::core::convert::Into<#root::core::SqlValue>,
4850 {
4851 use #root::sql::FetcherPool as _;
4852 let _values: ::std::vec::Vec<#root::core::SqlValue> =
4853 vals.into_iter().map(::core::convert::Into::into).collect();
4854 if _values.is_empty() {
4855 return #root::query::QuerySet::<Self>::default()
4856 .fetch(pool)
4857 .await;
4858 }
4859 let _key = ::std::format!("{}__not_in", col);
4860 #root::query::QuerySet::<Self>::default()
4861 .filter(&_key, #root::core::SqlValue::List(_values))
4862 .fetch(pool)
4863 .await
4864 }
4865
4866 /// Fetch every row where `<col> IS NULL`. Eloquent
4867 /// `Model::whereNull($col)->get()` parity.
4868 ///
4869 /// # Errors
4870 /// As [`FetcherPool::fetch`].
4871 ///
4872 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4873 pub async fn where_null(
4874 col: &str,
4875 pool: &#root::sql::Pool,
4876 ) -> ::core::result::Result<
4877 ::std::vec::Vec<Self>,
4878 #root::sql::ExecError,
4879 > {
4880 use #root::sql::FetcherPool as _;
4881 let _key = ::std::format!("{}__isnull", col);
4882 #root::query::QuerySet::<Self>::default()
4883 .filter(&_key, #root::core::SqlValue::Bool(true))
4884 .fetch(pool)
4885 .await
4886 }
4887
4888 /// Fetch every row where `<col> IS NOT NULL`. Eloquent
4889 /// `Model::whereNotNull($col)->get()` parity.
4890 ///
4891 /// # Errors
4892 /// As [`FetcherPool::fetch`].
4893 ///
4894 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4895 pub async fn where_not_null(
4896 col: &str,
4897 pool: &#root::sql::Pool,
4898 ) -> ::core::result::Result<
4899 ::std::vec::Vec<Self>,
4900 #root::sql::ExecError,
4901 > {
4902 use #root::sql::FetcherPool as _;
4903 let _key = ::std::format!("{}__isnull", col);
4904 #root::query::QuerySet::<Self>::default()
4905 .filter(&_key, #root::core::SqlValue::Bool(false))
4906 .fetch(pool)
4907 .await
4908 }
4909
4910 /// Fetch up to `n` rows in random order. Eloquent
4911 /// `Model::inRandomOrder()->limit($n)->get()` /
4912 /// `Model::query()->inRandomOrder()->get()->take($n)`
4913 /// parity. **Performance caveat**: random ordering
4914 /// forces a full table scan + per-row random key sort;
4915 /// the optimizer cannot use an index. Prefer a
4916 /// `pk >= rand_offset LIMIT N` walk for huge tables.
4917 ///
4918 /// # Errors
4919 /// As [`FetcherPool::fetch`].
4920 ///
4921 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4922 pub async fn random_n(
4923 n: i64,
4924 pool: &#root::sql::Pool,
4925 ) -> ::core::result::Result<
4926 ::std::vec::Vec<Self>,
4927 #root::sql::ExecError,
4928 > {
4929 use #root::sql::FetcherPool as _;
4930 #root::query::QuerySet::<Self>::default()
4931 .order_random()
4932 .limit(n)
4933 .fetch(pool)
4934 .await
4935 }
4936
4937 /// Fetch one row in random order. Eloquent
4938 /// `Model::inRandomOrder()->first()` parity. Same
4939 /// performance caveat as [`Self::random_n_pool`].
4940 ///
4941 /// # Errors
4942 /// As [`FetcherPool::fetch`].
4943 ///
4944 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4945 pub async fn random(
4946 pool: &#root::sql::Pool,
4947 ) -> ::core::result::Result<
4948 ::core::option::Option<Self>,
4949 #root::sql::ExecError,
4950 > {
4951 ::core::result::Result::Ok(
4952 Self::random_n(1, pool).await?.into_iter().next(),
4953 )
4954 }
4955
4956 /// Fetch every row ordered ASC by `field`. Eloquent
4957 /// `Model::oldest($field)->get()` parity — the multi-row
4958 /// counterpart of [`Self::earliest_pool`].
4959 ///
4960 /// # Errors
4961 /// As [`FetcherPool::fetch`].
4962 ///
4963 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4964 pub async fn oldest(
4965 field: &str,
4966 pool: &#root::sql::Pool,
4967 ) -> ::core::result::Result<
4968 ::std::vec::Vec<Self>,
4969 #root::sql::ExecError,
4970 > {
4971 use #root::sql::FetcherPool as _;
4972 #root::query::QuerySet::<Self>::default()
4973 .order_by(&[(field, false)])
4974 .fetch(pool)
4975 .await
4976 }
4977
4978 /// Fetch every row ordered DESC by `field`. Eloquent
4979 /// `Model::latest($field)->get()` parity — the multi-row
4980 /// counterpart of [`Self::latest_pool`].
4981 ///
4982 /// # Errors
4983 /// As [`FetcherPool::fetch`].
4984 ///
4985 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4986 pub async fn newest(
4987 field: &str,
4988 pool: &#root::sql::Pool,
4989 ) -> ::core::result::Result<
4990 ::std::vec::Vec<Self>,
4991 #root::sql::ExecError,
4992 > {
4993 use #root::sql::FetcherPool as _;
4994 #root::query::QuerySet::<Self>::default()
4995 .order_by(&[(field, true)])
4996 .fetch(pool)
4997 .await
4998 }
4999
5000 /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5001 /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5002 /// Routes through the existing `__year` lookup suffix
5003 /// (issue #829).
5004 ///
5005 /// # Errors
5006 /// As [`FetcherPool::fetch`].
5007 ///
5008 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5009 pub async fn where_year(
5010 col: &str,
5011 year: i64,
5012 pool: &#root::sql::Pool,
5013 ) -> ::core::result::Result<
5014 ::std::vec::Vec<Self>,
5015 #root::sql::ExecError,
5016 > {
5017 use #root::sql::FetcherPool as _;
5018 let _key = ::std::format!("{}__year", col);
5019 #root::query::QuerySet::<Self>::default()
5020 .filter(&_key, #root::core::SqlValue::I64(year))
5021 .fetch(pool)
5022 .await
5023 }
5024
5025 /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5026 /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5027 /// `month` is 1–12.
5028 ///
5029 /// # Errors
5030 /// As [`FetcherPool::fetch`].
5031 ///
5032 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5033 pub async fn where_month(
5034 col: &str,
5035 month: i64,
5036 pool: &#root::sql::Pool,
5037 ) -> ::core::result::Result<
5038 ::std::vec::Vec<Self>,
5039 #root::sql::ExecError,
5040 > {
5041 use #root::sql::FetcherPool as _;
5042 let _key = ::std::format!("{}__month", col);
5043 #root::query::QuerySet::<Self>::default()
5044 .filter(&_key, #root::core::SqlValue::I64(month))
5045 .fetch(pool)
5046 .await
5047 }
5048
5049 /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5050 /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5051 /// `day` is 1–31.
5052 ///
5053 /// # Errors
5054 /// As [`FetcherPool::fetch`].
5055 ///
5056 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5057 pub async fn where_day(
5058 col: &str,
5059 day: i64,
5060 pool: &#root::sql::Pool,
5061 ) -> ::core::result::Result<
5062 ::std::vec::Vec<Self>,
5063 #root::sql::ExecError,
5064 > {
5065 use #root::sql::FetcherPool as _;
5066 let _key = ::std::format!("{}__day", col);
5067 #root::query::QuerySet::<Self>::default()
5068 .filter(&_key, #root::core::SqlValue::I64(day))
5069 .fetch(pool)
5070 .await
5071 }
5072
5073 /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5074 /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5075 /// `hour` is 0–23.
5076 ///
5077 /// # Errors
5078 /// As [`FetcherPool::fetch`].
5079 ///
5080 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5081 pub async fn where_hour(
5082 col: &str,
5083 hour: i64,
5084 pool: &#root::sql::Pool,
5085 ) -> ::core::result::Result<
5086 ::std::vec::Vec<Self>,
5087 #root::sql::ExecError,
5088 > {
5089 use #root::sql::FetcherPool as _;
5090 let _key = ::std::format!("{}__hour", col);
5091 #root::query::QuerySet::<Self>::default()
5092 .filter(&_key, #root::core::SqlValue::I64(hour))
5093 .fetch(pool)
5094 .await
5095 }
5096
5097 /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5098 /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5099 /// `minute` is 0–59.
5100 ///
5101 /// # Errors
5102 /// As [`FetcherPool::fetch`].
5103 ///
5104 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5105 pub async fn where_minute(
5106 col: &str,
5107 minute: i64,
5108 pool: &#root::sql::Pool,
5109 ) -> ::core::result::Result<
5110 ::std::vec::Vec<Self>,
5111 #root::sql::ExecError,
5112 > {
5113 use #root::sql::FetcherPool as _;
5114 let _key = ::std::format!("{}__minute", col);
5115 #root::query::QuerySet::<Self>::default()
5116 .filter(&_key, #root::core::SqlValue::I64(minute))
5117 .fetch(pool)
5118 .await
5119 }
5120
5121 /// Fetch every row where `<col> LIKE <pattern>` —
5122 /// caller-supplied pattern (must include `%` / `_`
5123 /// wildcards manually). Eloquent `Model::whereLike`
5124 /// parity.
5125 ///
5126 /// # Errors
5127 /// As [`FetcherPool::fetch`].
5128 ///
5129 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5130 pub async fn where_like(
5131 col: &str,
5132 pattern: impl ::core::convert::Into<::std::string::String>,
5133 pool: &#root::sql::Pool,
5134 ) -> ::core::result::Result<
5135 ::std::vec::Vec<Self>,
5136 #root::sql::ExecError,
5137 > {
5138 use #root::sql::FetcherPool as _;
5139 let _key = ::std::format!("{}__like", col);
5140 #root::query::QuerySet::<Self>::default()
5141 .filter(
5142 &_key,
5143 #root::core::SqlValue::String(pattern.into()),
5144 )
5145 .fetch(pool)
5146 .await
5147 }
5148
5149 /// Fetch every row where `<col> ILIKE <pattern>` —
5150 /// case-insensitive LIKE (PG native, MySQL/SQLite
5151 /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5152 ///
5153 /// # Errors
5154 /// As [`FetcherPool::fetch`].
5155 ///
5156 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5157 pub async fn where_ilike(
5158 col: &str,
5159 pattern: impl ::core::convert::Into<::std::string::String>,
5160 pool: &#root::sql::Pool,
5161 ) -> ::core::result::Result<
5162 ::std::vec::Vec<Self>,
5163 #root::sql::ExecError,
5164 > {
5165 use #root::sql::FetcherPool as _;
5166 let _key = ::std::format!("{}__ilike", col);
5167 #root::query::QuerySet::<Self>::default()
5168 .filter(
5169 &_key,
5170 #root::core::SqlValue::String(pattern.into()),
5171 )
5172 .fetch(pool)
5173 .await
5174 }
5175
5176 /// Fetch every row where `<col>` starts with `prefix`
5177 /// (auto-appends `%`). Django `__startswith` / Eloquent
5178 /// `whereLike("col", "$prefix%")` parity.
5179 ///
5180 /// # Errors
5181 /// As [`FetcherPool::fetch`].
5182 ///
5183 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5184 pub async fn where_starts_with(
5185 col: &str,
5186 prefix: impl ::core::convert::Into<::std::string::String>,
5187 pool: &#root::sql::Pool,
5188 ) -> ::core::result::Result<
5189 ::std::vec::Vec<Self>,
5190 #root::sql::ExecError,
5191 > {
5192 use #root::sql::FetcherPool as _;
5193 let _key = ::std::format!("{}__startswith", col);
5194 #root::query::QuerySet::<Self>::default()
5195 .filter(
5196 &_key,
5197 #root::core::SqlValue::String(prefix.into()),
5198 )
5199 .fetch(pool)
5200 .await
5201 }
5202
5203 /// Fetch every row where `<col>` ends with `suffix`
5204 /// (auto-prepends `%`). Django `__endswith` / Eloquent
5205 /// `whereLike("col", "%$suffix")` parity.
5206 ///
5207 /// # Errors
5208 /// As [`FetcherPool::fetch`].
5209 ///
5210 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5211 pub async fn where_ends_with(
5212 col: &str,
5213 suffix: impl ::core::convert::Into<::std::string::String>,
5214 pool: &#root::sql::Pool,
5215 ) -> ::core::result::Result<
5216 ::std::vec::Vec<Self>,
5217 #root::sql::ExecError,
5218 > {
5219 use #root::sql::FetcherPool as _;
5220 let _key = ::std::format!("{}__endswith", col);
5221 #root::query::QuerySet::<Self>::default()
5222 .filter(
5223 &_key,
5224 #root::core::SqlValue::String(suffix.into()),
5225 )
5226 .fetch(pool)
5227 .await
5228 }
5229
5230 /// Fetch every row where `<col>` contains `substr`
5231 /// (auto-wraps with `%`). Django `__contains` /
5232 /// Eloquent `whereLike("col", "%$substr%")` parity.
5233 ///
5234 /// # Errors
5235 /// As [`FetcherPool::fetch`].
5236 ///
5237 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5238 pub async fn where_contains(
5239 col: &str,
5240 substr: impl ::core::convert::Into<::std::string::String>,
5241 pool: &#root::sql::Pool,
5242 ) -> ::core::result::Result<
5243 ::std::vec::Vec<Self>,
5244 #root::sql::ExecError,
5245 > {
5246 use #root::sql::FetcherPool as _;
5247 let _key = ::std::format!("{}__contains", col);
5248 #root::query::QuerySet::<Self>::default()
5249 .filter(
5250 &_key,
5251 #root::core::SqlValue::String(substr.into()),
5252 )
5253 .fetch(pool)
5254 .await
5255 }
5256
5257 /// Fetch every row where `<col> > val`. Eloquent
5258 /// `Model::where($col, ">", $val)->get()` parity.
5259 ///
5260 /// # Errors
5261 /// As [`FetcherPool::fetch`].
5262 ///
5263 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5264 pub async fn where_gt(
5265 col: &str,
5266 val: impl ::core::convert::Into<#root::core::SqlValue>,
5267 pool: &#root::sql::Pool,
5268 ) -> ::core::result::Result<
5269 ::std::vec::Vec<Self>,
5270 #root::sql::ExecError,
5271 > {
5272 use #root::sql::FetcherPool as _;
5273 let _key = ::std::format!("{}__gt", col);
5274 #root::query::QuerySet::<Self>::default()
5275 .filter(&_key, val)
5276 .fetch(pool)
5277 .await
5278 }
5279
5280 /// Fetch every row where `<col> >= val`. Eloquent
5281 /// `Model::where($col, ">=", $val)->get()` parity.
5282 ///
5283 /// # Errors
5284 /// As [`FetcherPool::fetch`].
5285 ///
5286 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5287 pub async fn where_gte(
5288 col: &str,
5289 val: impl ::core::convert::Into<#root::core::SqlValue>,
5290 pool: &#root::sql::Pool,
5291 ) -> ::core::result::Result<
5292 ::std::vec::Vec<Self>,
5293 #root::sql::ExecError,
5294 > {
5295 use #root::sql::FetcherPool as _;
5296 let _key = ::std::format!("{}__gte", col);
5297 #root::query::QuerySet::<Self>::default()
5298 .filter(&_key, val)
5299 .fetch(pool)
5300 .await
5301 }
5302
5303 /// Fetch every row where `<col> < val`. Eloquent
5304 /// `Model::where($col, "<", $val)->get()` parity.
5305 ///
5306 /// # Errors
5307 /// As [`FetcherPool::fetch`].
5308 ///
5309 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5310 pub async fn where_lt(
5311 col: &str,
5312 val: impl ::core::convert::Into<#root::core::SqlValue>,
5313 pool: &#root::sql::Pool,
5314 ) -> ::core::result::Result<
5315 ::std::vec::Vec<Self>,
5316 #root::sql::ExecError,
5317 > {
5318 use #root::sql::FetcherPool as _;
5319 let _key = ::std::format!("{}__lt", col);
5320 #root::query::QuerySet::<Self>::default()
5321 .filter(&_key, val)
5322 .fetch(pool)
5323 .await
5324 }
5325
5326 /// Fetch every row where `<col> <= val`. Eloquent
5327 /// `Model::where($col, "<=", $val)->get()` parity.
5328 ///
5329 /// # Errors
5330 /// As [`FetcherPool::fetch`].
5331 ///
5332 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5333 pub async fn where_lte(
5334 col: &str,
5335 val: impl ::core::convert::Into<#root::core::SqlValue>,
5336 pool: &#root::sql::Pool,
5337 ) -> ::core::result::Result<
5338 ::std::vec::Vec<Self>,
5339 #root::sql::ExecError,
5340 > {
5341 use #root::sql::FetcherPool as _;
5342 let _key = ::std::format!("{}__lte", col);
5343 #root::query::QuerySet::<Self>::default()
5344 .filter(&_key, val)
5345 .fetch(pool)
5346 .await
5347 }
5348
5349 /// Fetch every row where `<col> <> val`. Eloquent
5350 /// `Model::where($col, "!=", $val)->get()` parity.
5351 ///
5352 /// # Errors
5353 /// As [`FetcherPool::fetch`].
5354 ///
5355 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5356 pub async fn where_ne(
5357 col: &str,
5358 val: impl ::core::convert::Into<#root::core::SqlValue>,
5359 pool: &#root::sql::Pool,
5360 ) -> ::core::result::Result<
5361 ::std::vec::Vec<Self>,
5362 #root::sql::ExecError,
5363 > {
5364 use #root::sql::FetcherPool as _;
5365 let _key = ::std::format!("{}__ne", col);
5366 #root::query::QuerySet::<Self>::default()
5367 .filter(&_key, val)
5368 .fetch(pool)
5369 .await
5370 }
5371
5372 /// Fetch every row matching `<col> = val` for ANY of the
5373 /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5374 /// parity. Empty `cols` returns no rows.
5375 ///
5376 /// Resolves each `&str` column to its SCHEMA-registered
5377 /// `&'static str` once and builds a single OR-composed
5378 /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5379 ///
5380 /// # Errors
5381 /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5382 /// when any column is not declared on the model.
5383 ///
5384 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5385 pub async fn where_any(
5386 cols: &[&str],
5387 val: impl ::core::convert::Into<#root::core::SqlValue>,
5388 pool: &#root::sql::Pool,
5389 ) -> ::core::result::Result<
5390 ::std::vec::Vec<Self>,
5391 #root::sql::ExecError,
5392 > {
5393 Self::__where_multi(cols, val, false, pool).await
5394 }
5395
5396 /// Fetch every row matching `<col> = val` for ALL listed
5397 /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5398 /// Empty `cols` returns every row (vacuous AND).
5399 ///
5400 /// # Errors
5401 /// As [`Self::where_any`].
5402 pub async fn where_all(
5403 cols: &[&str],
5404 val: impl ::core::convert::Into<#root::core::SqlValue>,
5405 pool: &#root::sql::Pool,
5406 ) -> ::core::result::Result<
5407 ::std::vec::Vec<Self>,
5408 #root::sql::ExecError,
5409 > {
5410 Self::__where_multi(cols, val, true, pool).await
5411 }
5412
5413 /// Internal: build a Q expression composing `cols` via
5414 /// AND (`all`) or OR (`!all`), then fetch. Backs
5415 /// `where_any` / `where_all`.
5416 #[doc(hidden)]
5417 pub async fn __where_multi(
5418 cols: &[&str],
5419 val: impl ::core::convert::Into<#root::core::SqlValue>,
5420 all: bool,
5421 pool: &#root::sql::Pool,
5422 ) -> ::core::result::Result<
5423 ::std::vec::Vec<Self>,
5424 #root::sql::ExecError,
5425 > {
5426 #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5427 .await
5428 }
5429
5430 /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5431 /// parity / Django `Model.objects.all()[:n]`. PK-ordered
5432 /// is NOT guaranteed without an explicit `order_by` —
5433 /// drop into `Self::query()` for that.
5434 ///
5435 /// # Errors
5436 /// As [`FetcherPool::fetch`].
5437 ///
5438 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5439 pub async fn take(
5440 n: i64,
5441 pool: &#root::sql::Pool,
5442 ) -> ::core::result::Result<
5443 ::std::vec::Vec<Self>,
5444 #root::sql::ExecError,
5445 > {
5446 use #root::sql::FetcherPool as _;
5447 #root::query::QuerySet::<Self>::default()
5448 .limit(n)
5449 .fetch(pool)
5450 .await
5451 }
5452
5453 /// Fetch the page-th window of `per_page` rows
5454 /// (1-indexed). Eloquent
5455 /// `Model::query()->forPage($page, $perPage)->get()`
5456 /// parity. The DB scans an `OFFSET (page - 1) * per_page
5457 /// LIMIT per_page`; for large offsets this is O(N) —
5458 /// prefer keyset pagination via PK on hot paths.
5459 ///
5460 /// # Errors
5461 /// As [`FetcherPool::fetch`].
5462 ///
5463 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5464 pub async fn for_page(
5465 page: i64,
5466 per_page: i64,
5467 pool: &#root::sql::Pool,
5468 ) -> ::core::result::Result<
5469 ::std::vec::Vec<Self>,
5470 #root::sql::ExecError,
5471 > {
5472 use #root::sql::FetcherPool as _;
5473 let _offset = if page > 1 { (page - 1) * per_page } else { 0 };
5474 #root::query::QuerySet::<Self>::default()
5475 .limit(per_page)
5476 .offset(_offset)
5477 .fetch(pool)
5478 .await
5479 }
5480
5481 /// Eloquent `Model::paginate($per_page, $page)` — fetch
5482 /// one page of rows AND the total row count in one
5483 /// call. Returns `(rows, total)`. Two queries: a
5484 /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5485 /// for the total.
5486 ///
5487 /// Useful for paginated UIs that need both the visible
5488 /// rows AND a "Page X of Y" / total-count widget. For
5489 /// large tables prefer keyset pagination + cached count;
5490 /// every call to `paginate` re-counts the full table.
5491 ///
5492 /// Same 1-indexed `page` convention as [`Self::for_page`].
5493 ///
5494 /// # Errors
5495 /// As [`Self::for_page`] and [`Self::count`].
5496 pub async fn paginate(
5497 page: i64,
5498 per_page: i64,
5499 pool: &#root::sql::Pool,
5500 ) -> ::core::result::Result<
5501 (::std::vec::Vec<Self>, i64),
5502 #root::sql::ExecError,
5503 > {
5504 // Route through the queryset rather than `Self::count`:
5505 // the `count` inherent method is suppressed on models
5506 // that declare a field named `count` (field/shortcut
5507 // collision guard), and `paginate` must still compile
5508 // for those models.
5509 let total = {
5510 use #root::sql::CounterPool as _;
5511 #root::query::QuerySet::<Self>::default()
5512 .count(pool)
5513 .await?
5514 };
5515 let rows = Self::for_page(page, per_page, pool).await?;
5516 ::core::result::Result::Ok((rows, total))
5517 }
5518
5519 /// Bulk-update — set `set_col = set_val` on every row
5520 /// matching `where_col = where_val`. Returns affected row
5521 /// count. Eloquent
5522 /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5523 /// parity.
5524 ///
5525 /// For multi-column updates drop into the queryset
5526 /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5527 ///
5528 /// # Errors
5529 /// As [`UpdaterPool::execute_pool`].
5530 ///
5531 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5532 pub async fn update_where(
5533 where_col: &str,
5534 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5535 set_col: &str,
5536 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5537 pool: &#root::sql::Pool,
5538 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5539 use #root::sql::UpdaterPool as _;
5540 #root::query::QuerySet::<Self>::default()
5541 .filter(where_col, where_val)
5542 .update()
5543 .set(set_col, set_val)
5544 .execute_pool(pool)
5545 .await
5546 }
5547
5548 /// Bulk-delete — remove every row matching
5549 /// `where_col = where_val`. Returns affected row count.
5550 /// Eloquent
5551 /// `Model::where($where_col, $where_val)->delete()` parity.
5552 ///
5553 /// For more complex filters drop into the queryset
5554 /// builder + `Self::query().filter(...).delete().execute_pool(&pool)`.
5555 ///
5556 /// # Errors
5557 /// As [`#root::sql::delete_pool`].
5558 pub async fn delete_where(
5559 where_col: &str,
5560 where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5561 pool: &#root::sql::Pool,
5562 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5563 let _query = #root::core::DeleteQuery {
5564 model: <Self as #root::core::Model>::SCHEMA,
5565 where_clause: #root::core::WhereExpr::Predicate(
5566 #root::core::Filter {
5567 column: <Self as #root::core::Model>::SCHEMA
5568 .field(where_col)
5569 .ok_or_else(|| {
5570 #root::sql::ExecError::Query(
5571 #root::core::QueryError::UnknownField {
5572 model: <Self as #root::core::Model>::SCHEMA.name,
5573 field: ::std::string::ToString::to_string(where_col),
5574 },
5575 )
5576 })?
5577 .column,
5578 op: #root::core::Op::Eq,
5579 value: ::core::convert::Into::into(where_val),
5580 },
5581 ),
5582 };
5583 #root::sql::delete_pool(pool, &_query).await
5584 }
5585
5586 /// Bulk-update — set `set_col = set_val` on EVERY row of
5587 /// this model's table. **No WHERE clause** — use with
5588 /// care. Eloquent `Model::query()->update([$col => $val])`
5589 /// parity.
5590 ///
5591 /// Use for backfills, one-shot reset flows, etc. The
5592 /// returned count is rows affected.
5593 ///
5594 /// # Errors
5595 /// As [`UpdaterPool::execute_pool`].
5596 ///
5597 /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5598 pub async fn update_all(
5599 set_col: &str,
5600 set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5601 pool: &#root::sql::Pool,
5602 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5603 use #root::sql::UpdaterPool as _;
5604 #root::query::QuerySet::<Self>::default()
5605 .update()
5606 .set(set_col, set_val)
5607 .execute_pool(pool)
5608 .await
5609 }
5610
5611 /// Fetch every row where `<col> NOT LIKE <pattern>`.
5612 /// Eloquent `Model::whereNotLike` parity. Pattern is
5613 /// passed verbatim — caller controls `%` / `_`.
5614 ///
5615 /// # Errors
5616 /// As [`FetcherPool::fetch`].
5617 ///
5618 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5619 pub async fn where_not_like(
5620 col: &str,
5621 pattern: impl ::core::convert::Into<::std::string::String>,
5622 pool: &#root::sql::Pool,
5623 ) -> ::core::result::Result<
5624 ::std::vec::Vec<Self>,
5625 #root::sql::ExecError,
5626 > {
5627 use #root::sql::FetcherPool as _;
5628 let _key = ::std::format!("{}__not_like", col);
5629 #root::query::QuerySet::<Self>::default()
5630 .filter(
5631 &_key,
5632 #root::core::SqlValue::String(pattern.into()),
5633 )
5634 .fetch(pool)
5635 .await
5636 }
5637
5638 /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5639 /// case-insensitive `NOT LIKE` (PG native, MySQL /
5640 /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5641 ///
5642 /// # Errors
5643 /// As [`FetcherPool::fetch`].
5644 ///
5645 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5646 pub async fn where_not_ilike(
5647 col: &str,
5648 pattern: impl ::core::convert::Into<::std::string::String>,
5649 pool: &#root::sql::Pool,
5650 ) -> ::core::result::Result<
5651 ::std::vec::Vec<Self>,
5652 #root::sql::ExecError,
5653 > {
5654 use #root::sql::FetcherPool as _;
5655 let _key = ::std::format!("{}__not_ilike", col);
5656 #root::query::QuerySet::<Self>::default()
5657 .filter(
5658 &_key,
5659 #root::core::SqlValue::String(pattern.into()),
5660 )
5661 .fetch(pool)
5662 .await
5663 }
5664
5665 /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5666 /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5667 /// parity.
5668 ///
5669 /// # Errors
5670 /// As [`FetcherPool::fetch`].
5671 ///
5672 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5673 pub async fn where_not_between(
5674 col: &str,
5675 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5676 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5677 pool: &#root::sql::Pool,
5678 ) -> ::core::result::Result<
5679 ::std::vec::Vec<Self>,
5680 #root::sql::ExecError,
5681 > {
5682 use #root::sql::FetcherPool as _;
5683 let _key = ::std::format!("{}__not_between", col);
5684 let _vals = #root::core::SqlValue::List(::std::vec![
5685 ::core::convert::Into::into(lo),
5686 ::core::convert::Into::into(hi),
5687 ]);
5688 #root::query::QuerySet::<Self>::default()
5689 .filter(&_key, _vals)
5690 .fetch(pool)
5691 .await
5692 }
5693
5694 /// Returns the SQL table name for this model. Eloquent
5695 /// `$model->getTable()` parity.
5696 #[must_use]
5697 pub fn table_name() -> &'static str {
5698 <Self as #root::core::Model>::SCHEMA.table
5699 }
5700
5701 /// Returns the SQL column name of this model's primary
5702 /// key, or `None` when the model has no
5703 /// `#[rustango(primary_key)]`. Eloquent
5704 /// `$model->getKeyName()` parity.
5705 #[must_use]
5706 pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5707 <Self as #root::core::Model>::SCHEMA
5708 .primary_key()
5709 .map(|f| f.column)
5710 }
5711
5712 /// Fetch every row where `<col> BETWEEN lo AND hi`
5713 /// (inclusive on both ends — same as SQL). Eloquent
5714 /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5715 ///
5716 /// # Errors
5717 /// As [`FetcherPool::fetch`].
5718 ///
5719 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5720 pub async fn where_between(
5721 col: &str,
5722 lo: impl ::core::convert::Into<#root::core::SqlValue>,
5723 hi: impl ::core::convert::Into<#root::core::SqlValue>,
5724 pool: &#root::sql::Pool,
5725 ) -> ::core::result::Result<
5726 ::std::vec::Vec<Self>,
5727 #root::sql::ExecError,
5728 > {
5729 use #root::sql::FetcherPool as _;
5730 let _key = ::std::format!("{}__between", col);
5731 let _vals = #root::core::SqlValue::List(::std::vec![
5732 ::core::convert::Into::into(lo),
5733 ::core::convert::Into::into(hi),
5734 ]);
5735 #root::query::QuerySet::<Self>::default()
5736 .filter(&_key, _vals)
5737 .fetch(pool)
5738 .await
5739 }
5740
5741 /// Fetch the first row where `<col> = <val>`. Returns
5742 /// `Ok(None)` when no row matches. Eloquent
5743 /// `Model::firstWhere($col, $val)` / Django
5744 /// `Model.objects.filter(col=val).first()` parity.
5745 ///
5746 /// Thin wrapper over `QuerySet::<Self>::default()
5747 /// .filter(col, val).first(pool)`. Use this when you
5748 /// want one row identified by a non-PK column (e.g.
5749 /// `User::first_where_pool("email", "x@y.com", &pool)`).
5750 ///
5751 /// `val` accepts any value `Into<SqlValue>` so plain
5752 /// strings, ints, UUIDs, etc. all work.
5753 ///
5754 /// # Errors
5755 /// As `QuerySet::first`.
5756 pub async fn first_where(
5757 col: &str,
5758 val: impl ::core::convert::Into<#root::core::SqlValue>,
5759 pool: &#root::sql::Pool,
5760 ) -> ::core::result::Result<
5761 ::core::option::Option<Self>,
5762 #root::sql::ExecError,
5763 > {
5764 #root::query::QuerySet::<Self>::default()
5765 .filter(col, val)
5766 .first(pool)
5767 .await
5768 }
5769
5770 #value_method
5771
5772 /// Fetch the row with the largest `field` value —
5773 /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
5774 /// `Ok(None)` for an empty table. Eloquent
5775 /// `Model::latest($field)->first()` / Django
5776 /// `Model.objects.latest(field)` (non-throwing) parity.
5777 /// Thin wrapper over `QuerySet::<Self>::default()
5778 /// .latest(field, pool)`.
5779 ///
5780 /// **Field name** is the Rust field ident as a string
5781 /// (not the SQL column). Unknown fields surface as
5782 /// `ExecError::Query(QueryError::UnknownField)` at
5783 /// compile time.
5784 ///
5785 /// # Errors
5786 /// As `QuerySet::latest`.
5787 pub async fn latest(
5788 field: &str,
5789 pool: &#root::sql::Pool,
5790 ) -> ::core::result::Result<
5791 ::core::option::Option<Self>,
5792 #root::sql::ExecError,
5793 > {
5794 #root::query::QuerySet::<Self>::default()
5795 .latest(field, pool)
5796 .await
5797 }
5798
5799 /// Sibling of [`Self::latest_pool`] — fetches the row
5800 /// with the smallest `field` value (`ORDER BY <field>
5801 /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
5802 /// ->first()` / Django `Model.objects.earliest(field)`
5803 /// parity.
5804 ///
5805 /// # Errors
5806 /// As [`Self::latest_pool`].
5807 pub async fn earliest(
5808 field: &str,
5809 pool: &#root::sql::Pool,
5810 ) -> ::core::result::Result<
5811 ::core::option::Option<Self>,
5812 #root::sql::ExecError,
5813 > {
5814 #root::query::QuerySet::<Self>::default()
5815 .earliest(field, pool)
5816 .await
5817 }
5818
5819 #count_method
5820
5821 /// `true` when the table contains at least one row.
5822 /// Eloquent `Model::query()->exists()` / Django
5823 /// `Model.objects.exists()` parity. Thin wrapper over
5824 /// `QuerySet::<Self>::default().exists(pool)`.
5825 ///
5826 /// # Errors
5827 /// As [`ExistsPool::exists`].
5828 ///
5829 /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
5830 pub async fn exists(
5831 pool: &#root::sql::Pool,
5832 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5833 use #root::sql::ExistsPool as _;
5834 #root::query::QuerySet::<Self>::default()
5835 .exists(pool)
5836 .await
5837 }
5838
5839 /// Inverse of [`Self::exists`] — returns `true` when
5840 /// the table has zero rows. Eloquent
5841 /// `Model::doesntExist()` parity.
5842 ///
5843 /// # Errors
5844 /// As [`Self::exists`].
5845 pub async fn doesnt_exist(
5846 pool: &#root::sql::Pool,
5847 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5848 Self::exists(pool).await.map(|e| !e)
5849 }
5850
5851 /// Eloquent `Model::query()->whereKey($pk)->exists()` —
5852 /// `true` when a row with primary key `pk` exists in
5853 /// the table. Sugar over
5854 /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
5855 ///
5856 /// Differs from [`Self::exists`] (which checks "any row
5857 /// in the table") by checking a specific PK existence.
5858 /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
5859 /// because the row is never materialized — the SQL is
5860 /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
5861 ///
5862 /// # Errors
5863 /// As [`#root::sql::ExistsPool::contains_pk`].
5864 pub async fn contains_pk(
5865 pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
5866 pool: &#root::sql::Pool,
5867 ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5868 use #root::sql::ExistsPool as _;
5869 #root::query::QuerySet::<Self>::default()
5870 .contains_pk(pool, pk)
5871 .await
5872 }
5873
5874 #sum_method
5875 #avg_method
5876 #min_method
5877 #max_method
5878
5879 /// Internal: forward to
5880 /// [`#root::sql::model_shortcuts::aggregate_one_pool`].
5881 /// Backs `sum` / `avg` / `min` / `max`.
5882 #[doc(hidden)]
5883 pub async fn __aggregate_one_pool<U>(
5884 col: &str,
5885 build: fn(&'static str) -> #root::core::AggregateExpr,
5886 pool: &#root::sql::Pool,
5887 ) -> ::core::result::Result<
5888 ::core::option::Option<U>,
5889 #root::sql::ExecError,
5890 >
5891 where
5892 (::core::option::Option<U>,): #root::sql::MaybePgFromRow
5893 + #root::sql::MaybeMyFromRow
5894 + #root::sql::MaybeSqliteFromRow
5895 + ::core::marker::Send
5896 + ::core::marker::Unpin,
5897 {
5898 #root::sql::model_shortcuts::aggregate_one_pool::<Self, U>(col, build, pool)
5899 .await
5900 }
5901
5902 /// Fetch every row of this model from `pool`. Eloquent
5903 /// `Model::all()` parity — a thin wrapper over
5904 /// `QuerySet::<Self>::default().fetch(pool)`.
5905 ///
5906 /// **Use with care on large tables** — there's no
5907 /// pagination or limit; the entire table is materialized
5908 /// into memory. For anything beyond fixture / lookup
5909 /// tables, page through `QuerySet::<Self>::default()
5910 /// .order_by(...).limit(N).offset(M).fetch(pool)`
5911 /// or stream via `.iterator(chunk_size)`.
5912 ///
5913 /// # Errors
5914 /// As [`FetcherPool::fetch`].
5915 ///
5916 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5917 pub async fn all(
5918 pool: &#root::sql::Pool,
5919 ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
5920 {
5921 use #root::sql::FetcherPool as _;
5922 #root::query::QuerySet::<Self>::default()
5923 .fetch(pool)
5924 .await
5925 }
5926
5927 /// Look up every row whose primary key is in `pks`.
5928 /// Returns the matching rows in **inventory** order — NOT
5929 /// the order of `pks`. Empty `pks` returns an empty
5930 /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
5931 /// with a list) / Django `Model.objects.filter(pk__in=[...])`
5932 /// parity.
5933 ///
5934 /// Thin wrapper over `QuerySet::<Self>::default()
5935 /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
5936 /// Caller-supplied PKs that don't match a row are
5937 /// silently skipped (the returned vec is shorter than
5938 /// the input list). For an order-preserving / "fail
5939 /// when any missing" variant, build the queryset
5940 /// explicitly with `in_bulk` instead.
5941 ///
5942 /// Accepts any iterable whose elements are
5943 /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
5944 /// `Vec<Uuid>`, etc.
5945 ///
5946 /// # Errors
5947 /// As [`FetcherPool::fetch`].
5948 ///
5949 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5950 pub async fn find_many<V>(
5951 pks: impl ::core::iter::IntoIterator<Item = V>,
5952 pool: &#root::sql::Pool,
5953 ) -> ::core::result::Result<
5954 ::std::vec::Vec<Self>,
5955 #root::sql::ExecError,
5956 >
5957 where
5958 V: ::core::convert::Into<#root::core::SqlValue>,
5959 {
5960 use #root::sql::FetcherPool as _;
5961 let _values: ::std::vec::Vec<#root::core::SqlValue> =
5962 pks.into_iter().map(::core::convert::Into::into).collect();
5963 if _values.is_empty() {
5964 return ::core::result::Result::Ok(::std::vec::Vec::new());
5965 }
5966 let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
5967 #root::query::QuerySet::<Self>::default()
5968 .filter(&_key, #root::core::SqlValue::List(_values))
5969 .fetch(pool)
5970 .await
5971 }
5972
5973 /// Look up the row whose primary key equals `pk`. Returns
5974 /// `Ok(None)` when no row matches; this is the
5975 /// non-throwing counterpart of Django's `.get(pk=…)`
5976 /// (which raises `DoesNotExist`). Eloquent `Model::find`
5977 /// shape — accepts any value `Into<SqlValue>`.
5978 ///
5979 /// One-liner shortcut for the common
5980 /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
5981 /// .limit(1).fetch(pool).await?.into_iter().next()`
5982 /// dance.
5983 ///
5984 /// # Errors
5985 /// As [`FetcherPool::fetch`].
5986 ///
5987 /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5988 pub async fn find(
5989 pk: impl ::core::convert::Into<#root::core::SqlValue>,
5990 pool: &#root::sql::Pool,
5991 ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
5992 {
5993 use #root::sql::FetcherPool as _;
5994 let _pk_val: #root::core::SqlValue = pk.into();
5995 let mut _rows: ::std::vec::Vec<Self> =
5996 #root::query::QuerySet::<Self>::default()
5997 .filter(::core::stringify!(#pk_ident), _pk_val)
5998 .limit(1)
5999 .fetch(pool)
6000 .await?;
6001 ::core::result::Result::Ok(_rows.into_iter().next())
6002 }
6003
6004 /// Look up the row whose primary key equals `pk`. Errors
6005 /// when no row matches — the throwing counterpart of
6006 /// [`Self::find_pool`]. Eloquent `Model::findOrFail` /
6007 /// Django `Model.objects.get(pk=…)` (which raises
6008 /// `DoesNotExist`) parity.
6009 ///
6010 /// Translates the miss into
6011 /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6012 /// so callers can `?`-bubble straight through the typical
6013 /// `ExecError` error chain.
6014 ///
6015 /// # Errors
6016 /// As [`Self::find_pool`]; additionally
6017 /// [`sqlx::Error::RowNotFound`] when no row matches.
6018 ///
6019 /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6020 /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6021 pub async fn find_or_fail(
6022 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6023 pool: &#root::sql::Pool,
6024 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6025 match Self::find(pk, pool).await? {
6026 ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6027 ::core::option::Option::None => ::core::result::Result::Err(
6028 #root::sql::ExecError::Driver(
6029 #root::sql::sqlx::Error::RowNotFound,
6030 ),
6031 ),
6032 }
6033 }
6034
6035 /// Look up multiple rows by primary key, **failing** if
6036 /// any requested PK is missing. Eloquent
6037 /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6038 /// inventory order (NOT request order — same as
6039 /// [`Self::find_many`]).
6040 ///
6041 /// Empty `pks` returns an empty `Vec` (no rows requested
6042 /// → nothing to fail on).
6043 ///
6044 /// Differs from [`Self::find_many`] in that this method
6045 /// requires every PK to resolve. If even one is missing,
6046 /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6047 /// in `ExecError::Driver`). Rows are deduped at the SQL
6048 /// layer, so passing the same PK twice in `pks` counts as
6049 /// one expected row.
6050 ///
6051 /// # Errors
6052 /// As [`Self::find_many`], plus `RowNotFound` when the
6053 /// returned row count is less than the count of distinct
6054 /// requested PKs.
6055 pub async fn find_many_or_fail<V>(
6056 pks: impl ::core::iter::IntoIterator<Item = V>,
6057 pool: &#root::sql::Pool,
6058 ) -> ::core::result::Result<
6059 ::std::vec::Vec<Self>,
6060 #root::sql::ExecError,
6061 >
6062 where
6063 V: ::core::convert::Into<#root::core::SqlValue>,
6064 {
6065 let _values: ::std::vec::Vec<#root::core::SqlValue> =
6066 pks.into_iter().map(::core::convert::Into::into).collect();
6067 if _values.is_empty() {
6068 return ::core::result::Result::Ok(::std::vec::Vec::new());
6069 }
6070 // Dedup the requested PK list before counting so
6071 // duplicate-PK requests don't false-fail (the SQL
6072 // `IN (...)` clause naturally dedups too).
6073 let mut _seen: ::std::collections::HashSet<
6074 ::std::string::String,
6075 > = ::std::collections::HashSet::new();
6076 for v in &_values {
6077 _seen.insert(v.to_display_string());
6078 }
6079 let _expected = _seen.len();
6080 let _rows = Self::find_many(_values, pool).await?;
6081 if _rows.len() < _expected {
6082 return ::core::result::Result::Err(
6083 #root::sql::ExecError::Driver(
6084 #root::sql::sqlx::Error::RowNotFound,
6085 ),
6086 );
6087 }
6088 ::core::result::Result::Ok(_rows)
6089 }
6090
6091 /// Find by primary key or run `fallback` to produce a
6092 /// default row to return. Eloquent
6093 /// `Model::findOr($pk, fn() => …)` parity.
6094 ///
6095 /// Unlike [`Self::find_or_fail_pool`] (which raises on
6096 /// miss), this is the "give me something sensible"
6097 /// branch: typical use is "fetch the user's row, else
6098 /// fall back to an anonymous/guest stub".
6099 ///
6100 /// `fallback` runs only when no row matches — the DB
6101 /// round-trip happens unconditionally.
6102 ///
6103 /// # Errors
6104 /// As [`Self::find_pool`].
6105 pub async fn find_or<F>(
6106 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6107 pool: &#root::sql::Pool,
6108 fallback: F,
6109 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6110 where
6111 F: ::core::ops::FnOnce() -> Self,
6112 {
6113 ::core::result::Result::Ok(
6114 Self::find(pk, pool).await?.unwrap_or_else(fallback),
6115 )
6116 }
6117
6118 /// Same as [`Self::find_or`] but also returns a `bool`
6119 /// indicating whether the row was found in the DB
6120 /// (`true`) or freshly built from `fallback` (`false`).
6121 /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6122 /// in PHP the returned model also exposes `->exists`;
6123 /// here that's surfaced as the second tuple element.
6124 ///
6125 /// Useful for edit-or-create form handlers where the
6126 /// caller needs to know whether to PATCH or POST when
6127 /// the user submits the form.
6128 ///
6129 /// # Errors
6130 /// As [`Self::find`].
6131 pub async fn find_or_new<F>(
6132 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6133 pool: &#root::sql::Pool,
6134 fallback: F,
6135 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6136 where
6137 F: ::core::ops::FnOnce() -> Self,
6138 {
6139 match Self::find(pk, pool).await? {
6140 ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6141 ::core::option::Option::None => {
6142 ::core::result::Result::Ok((fallback(), false))
6143 }
6144 }
6145 }
6146
6147 /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6148 /// [`Self::find_or_new`] but **persists** the new row
6149 /// when the PK isn't found. Returns `(row, exists: bool)`
6150 /// — `exists=true` when the PK was found,
6151 /// `false` when a fresh row was inserted.
6152 ///
6153 /// ```ignore
6154 /// let (post, found) = Post::find_or_insert(
6155 /// pk,
6156 /// &pool,
6157 /// || Post { id: Auto::default(), title: "new".into() },
6158 /// ).await?;
6159 /// // `found` true → returned existing row;
6160 /// // `found` false → fallback was inserted, `post.id` populated.
6161 /// ```
6162 ///
6163 /// **Caveat**: two concurrent calls that both miss the
6164 /// find can both INSERT, violating uniqueness. Wrap in a
6165 /// transaction or rely on a UNIQUE constraint + handle
6166 /// the conflict error if you need race-free semantics.
6167 ///
6168 /// # Errors
6169 /// As [`Self::find`] and [`Self::save_pool`].
6170 pub async fn find_or_insert<F>(
6171 pk: impl ::core::convert::Into<#root::core::SqlValue>,
6172 pool: &#root::sql::Pool,
6173 fallback: F,
6174 ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6175 where
6176 F: ::core::ops::FnOnce() -> Self,
6177 {
6178 if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6179 return ::core::result::Result::Ok((_row, true));
6180 }
6181 let mut _new = fallback();
6182 _new.save_pool(pool).await?;
6183 ::core::result::Result::Ok((_new, false))
6184 }
6185
6186 /// Fetch the first row of the table, or run `fallback`
6187 /// when the table is empty. Eloquent
6188 /// `Model::firstOr(fn() => …)` parity.
6189 ///
6190 /// # Errors
6191 /// As [`Self::first_pool`].
6192 pub async fn first_or<F>(
6193 pool: &#root::sql::Pool,
6194 fallback: F,
6195 ) -> ::core::result::Result<Self, #root::sql::ExecError>
6196 where
6197 F: ::core::ops::FnOnce() -> Self,
6198 {
6199 // Route through the queryset rather than `Self::first`,
6200 // which is suppressed on models with a field named
6201 // `first` (field/shortcut collision guard).
6202 ::core::result::Result::Ok(
6203 #root::query::QuerySet::<Self>::default()
6204 .first(pool)
6205 .await?
6206 .unwrap_or_else(fallback),
6207 )
6208 }
6209
6210 /// Fetch exactly one row matching `<col> = <val>`. Errors
6211 /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6212 /// or more than one matches
6213 /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6214 /// Eloquent `Model::sole($col, $val)` parity.
6215 ///
6216 /// # Errors
6217 /// As [`Self::where_pool`] plus the explicit
6218 /// `RowNotFound` / `MultipleRowsReturned` cases above.
6219 pub async fn sole(
6220 col: &str,
6221 val: impl ::core::convert::Into<#root::core::SqlValue>,
6222 pool: &#root::sql::Pool,
6223 ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6224 let mut _rows = Self::where_(col, val, pool).await?;
6225 match _rows.len() {
6226 0 => ::core::result::Result::Err(
6227 #root::sql::ExecError::Driver(
6228 #root::sql::sqlx::Error::RowNotFound,
6229 ),
6230 ),
6231 1 => ::core::result::Result::Ok(_rows.remove(0)),
6232 n => ::core::result::Result::Err(
6233 #root::sql::ExecError::MultipleRowsReturned {
6234 op: "sole",
6235 table: <Self as #root::core::Model>::SCHEMA.name,
6236 count: n,
6237 },
6238 ),
6239 }
6240 }
6241 }
6242 } else {
6243 quote!()
6244 };
6245
6246 // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6247 // the non-audited `_pool` methods but execute against an open
6248 // `PoolTx` so the writes participate in the caller's transaction.
6249 // Auditing inside TX is deferred; these always use the plain
6250 // executor primitives regardless of whether the model is audited.
6251 let tx_insert_method = if fields.has_auto {
6252 let pushes = &fields.insert_pushes;
6253 let returning_cols = &fields.returning_cols;
6254 quote! {
6255 /// Insert this row inside an open transaction, populating
6256 /// any `Auto<T>` PK from the auto-assigned value. Works
6257 /// against any backend that `tx` wraps.
6258 ///
6259 /// # Errors
6260 /// As [`Self::insert_pool`].
6261 pub async fn insert_tx(
6262 &mut self,
6263 tx: &mut #root::sql::PoolTx<'_>,
6264 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6265 let mut _columns: ::std::vec::Vec<&'static str> =
6266 ::std::vec::Vec::new();
6267 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6268 ::std::vec::Vec::new();
6269 #( #pushes )*
6270 let _query = #root::core::InsertQuery {
6271 model: <Self as #root::core::Model>::SCHEMA,
6272 columns: _columns,
6273 values: _values,
6274 returning: ::std::vec![ #( #returning_cols ),* ],
6275 on_conflict: ::core::option::Option::None,
6276 };
6277 let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6278 #root::sql::apply_auto_pk(_result, self)
6279 }
6280 }
6281 } else {
6282 let insert_columns = &fields.insert_columns;
6283 let insert_values = &fields.insert_values;
6284 quote! {
6285 /// Insert this row inside an open transaction.
6286 ///
6287 /// # Errors
6288 /// As [`Self::insert_pool`].
6289 pub async fn insert_tx(
6290 &self,
6291 tx: &mut #root::sql::PoolTx<'_>,
6292 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6293 let _query = #root::core::InsertQuery {
6294 model: <Self as #root::core::Model>::SCHEMA,
6295 columns: ::std::vec![ #( #insert_columns ),* ],
6296 values: ::std::vec![ #( #insert_values ),* ],
6297 returning: ::std::vec::Vec::new(),
6298 on_conflict: ::core::option::Option::None,
6299 };
6300 #root::sql::insert_tx(tx, &_query).await
6301 }
6302 }
6303 };
6304
6305 let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6306 let pk_column_lit = pk_col.as_str();
6307 let assignments = &fields.update_assignments;
6308 let dispatch_unset = if fields.pk_is_auto {
6309 quote! {
6310 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6311 return self.insert_tx(tx).await.map(|()| 1u64);
6312 }
6313 }
6314 } else {
6315 quote!()
6316 };
6317 quote! {
6318 /// Save this row inside an open transaction. `INSERT` when
6319 /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6320 /// PK. Works against any backend that `tx` wraps.
6321 ///
6322 /// # Errors
6323 /// As [`Self::save_pool`].
6324 pub async fn save_tx(
6325 &mut self,
6326 tx: &mut #root::sql::PoolTx<'_>,
6327 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6328 #dispatch_unset
6329 let _query = #root::core::UpdateQuery {
6330 model: <Self as #root::core::Model>::SCHEMA,
6331 set: ::std::vec![ #( #assignments ),* ],
6332 where_clause: #root::core::WhereExpr::Predicate(
6333 #root::core::Filter {
6334 column: #pk_column_lit,
6335 op: #root::core::Op::Eq,
6336 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6337 ::core::clone::Clone::clone(&self.#pk_ident)
6338 ),
6339 }
6340 ),
6341 };
6342 let _affected = #root::sql::update_tx(tx, &_query).await?;
6343 ::core::result::Result::Ok(_affected)
6344 }
6345 }
6346 } else {
6347 quote!()
6348 };
6349
6350 let tx_delete_method = {
6351 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6352 let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6353 if let Some(pk_ident) = pk_ident_for_tx {
6354 quote! {
6355 /// Delete the row identified by this instance's PK
6356 /// inside an open transaction. Works against any backend
6357 /// that `tx` wraps.
6358 ///
6359 /// # Errors
6360 /// As [`Self::delete_pool`].
6361 pub async fn delete_tx(
6362 &self,
6363 tx: &mut #root::sql::PoolTx<'_>,
6364 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6365 let _query = #root::core::DeleteQuery {
6366 model: <Self as #root::core::Model>::SCHEMA,
6367 where_clause: #root::core::WhereExpr::Predicate(
6368 #root::core::Filter {
6369 column: #pk_column_lit,
6370 op: #root::core::Op::Eq,
6371 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6372 ::core::clone::Clone::clone(&self.#pk_ident)
6373 ),
6374 }
6375 ),
6376 };
6377 #root::sql::delete_tx(tx, &_query).await
6378 }
6379 }
6380 } else {
6381 quote!()
6382 }
6383 };
6384
6385 // Update emission captures both BEFORE and AFTER state — runs an
6386 // extra SELECT against `_executor` BEFORE the UPDATE, captures
6387 // each tracked field's prior value, then after the UPDATE diffs
6388 // against the in-memory `&self`. `diff_changes` drops unchanged
6389 // columns so the JSON only contains the actual delta.
6390 //
6391 // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6392 // and binds `_audit_before_pairs`; `audit_update_post` runs
6393 // after the UPDATE and emits the PendingEntry.
6394 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6395 audited_fields
6396 {
6397 if tracked.is_empty() {
6398 (quote!(), quote!())
6399 } else {
6400 let select_cols: String = tracked
6401 .iter()
6402 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
6403 .collect::<Vec<_>>()
6404 .join(", ");
6405 let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6406 let select_cols_lit = select_cols;
6407 let pk_column_lit_for_select = pk_column_for_select;
6408 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6409 if fields.pk_is_auto {
6410 quote!(self.#pk_ident.get().copied().unwrap_or_default())
6411 } else {
6412 quote!(::core::clone::Clone::clone(&self.#pk_ident))
6413 }
6414 } else {
6415 quote!(0_i64)
6416 };
6417 let before_pairs = tracked.iter().map(|c| {
6418 let column_lit = c.column.as_str();
6419 let value_ty = &c.value_ty;
6420 quote! {
6421 (
6422 #column_lit,
6423 match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6424 &_audit_before_row, #column_lit,
6425 ) {
6426 ::core::result::Result::Ok(v) => {
6427 #root::__serde_json::to_value(&v)
6428 .unwrap_or(#root::__serde_json::Value::Null)
6429 }
6430 ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6431 },
6432 )
6433 }
6434 });
6435 let after_pairs = tracked.iter().map(|c| {
6436 let column_lit = c.column.as_str();
6437 let ident = &c.ident;
6438 quote! {
6439 (
6440 #column_lit,
6441 #root::__serde_json::to_value(&self.#ident)
6442 .unwrap_or(#root::__serde_json::Value::Null),
6443 )
6444 }
6445 });
6446 let pk_str = audit_pk_to_string.clone();
6447 let pre = quote! {
6448 let _audit_select_sql = ::std::format!(
6449 r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
6450 #select_cols_lit,
6451 <Self as #root::core::Model>::SCHEMA.table,
6452 #pk_column_lit_for_select,
6453 );
6454 let _audit_before_pairs:
6455 ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6456 match #root::sql::sqlx::query(&_audit_select_sql)
6457 .bind(#pk_value_for_bind)
6458 .fetch_optional(&mut *_executor)
6459 .await
6460 {
6461 ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
6462 ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
6463 }
6464 _ => ::core::option::Option::None,
6465 };
6466 };
6467 let post = quote! {
6468 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6469 let _audit_after:
6470 ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
6471 ::std::vec![ #( #after_pairs ),* ];
6472 let _audit_entry = #root::audit::PendingEntry {
6473 entity_table: <Self as #root::core::Model>::SCHEMA.table,
6474 entity_pk: #pk_str,
6475 operation: #root::audit::AuditOp::Update,
6476 source: #root::audit::current_source(),
6477 changes: #root::audit::diff_changes(
6478 &_audit_before,
6479 &_audit_after,
6480 ),
6481 };
6482 #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6483 }
6484 };
6485 (pre, post)
6486 }
6487 } else {
6488 (quote!(), quote!())
6489 };
6490
6491 // Bulk-insert audit: capture every row's tracked fields after the
6492 // RETURNING populates each PK, then push one batched INSERT INTO
6493 // audit_log via `emit_many`. One round-trip regardless of N rows.
6494 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6495 let row_pk_str = if let Some((pk_ident, _)) = primary_key {
6496 if fields.pk_is_auto {
6497 quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
6498 } else {
6499 quote!(::std::format!("{}", &_row.#pk_ident))
6500 }
6501 } else {
6502 quote!(::std::string::String::new())
6503 };
6504 let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
6505 let column_lit = c.column.as_str();
6506 let ident = &c.ident;
6507 quote! {
6508 (
6509 #column_lit,
6510 #root::__serde_json::to_value(&_row.#ident)
6511 .unwrap_or(#root::__serde_json::Value::Null),
6512 )
6513 }
6514 });
6515 quote! {
6516 let _audit_source = #root::audit::current_source();
6517 let mut _audit_entries:
6518 ::std::vec::Vec<#root::audit::PendingEntry> =
6519 ::std::vec::Vec::with_capacity(rows.len());
6520 for _row in rows.iter() {
6521 _audit_entries.push(#root::audit::PendingEntry {
6522 entity_table: <Self as #root::core::Model>::SCHEMA.table,
6523 entity_pk: #row_pk_str,
6524 operation: #root::audit::AuditOp::Create,
6525 source: _audit_source.clone(),
6526 changes: #root::audit::snapshot_changes(&[
6527 #( #row_pairs ),*
6528 ]),
6529 });
6530 }
6531 #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6532 }
6533 } else {
6534 quote!()
6535 };
6536
6537 let save_method = if fields.pk_is_auto {
6538 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6539 let pk_column_lit = pk_column.as_str();
6540 let assignments = &fields.update_assignments;
6541 let upsert_cols = &fields.upsert_update_columns;
6542 let upsert_pushes = &fields.insert_pushes;
6543 let upsert_returning = &fields.returning_cols;
6544 let upsert_auto_assigns = &fields.auto_assigns;
6545 // Conflict target: prefer the first declared `unique_together`
6546 // when it exists. Plain `Auto<T>` PKs are server-assigned via
6547 // `BIGSERIAL` and never collide on insert, so a PK-only target
6548 // would silently turn `upsert()` into "always-insert" for
6549 // surrogate-PK models with composite UNIQUE constraints — see
6550 // `RolePermission` / `UserRole` / `UserPermission` in the
6551 // tenancy permission engine. When no `unique_together` is
6552 // declared we keep the PK target (the original behaviour).
6553 let upsert_target_columns: Vec<String> = indexes
6554 .iter()
6555 .find(|i| i.unique && !i.columns.is_empty())
6556 .map(|i| i.columns.clone())
6557 .unwrap_or_else(|| vec![pk_column.clone()]);
6558 let upsert_target_lits = upsert_target_columns
6559 .iter()
6560 .map(String::as_str)
6561 .collect::<Vec<_>>();
6562 let conflict_clause = if fields.upsert_update_columns.is_empty() {
6563 quote!(#root::core::ConflictClause::DoNothing)
6564 } else {
6565 quote!(#root::core::ConflictClause::DoUpdate {
6566 target: ::std::vec![ #( #upsert_target_lits ),* ],
6567 update_columns: ::std::vec![ #( #upsert_cols ),* ],
6568 })
6569 };
6570 Some(quote! {
6571 /// Insert this row if its `Auto<T>` primary key is
6572 /// `Unset`, otherwise update the existing row matching the
6573 /// PK. Mirrors Django's `save()` — caller doesn't need to
6574 /// pick `insert` vs the bulk-update path manually.
6575 ///
6576 /// On the insert branch, populates the PK from `RETURNING`
6577 /// (same behavior as `insert`). On the update branch,
6578 /// writes every non-PK column back; if no row matches the
6579 /// PK, returns `Ok(())` silently.
6580 ///
6581 /// Only generated when the primary key is declared as
6582 /// `Auto<T>`. Models with a manually-managed PK must use
6583 /// `insert` or the QuerySet update builder.
6584 ///
6585 /// # Errors
6586 /// Returns [`#root::sql::ExecError`] for SQL-writing
6587 /// or driver failures.
6588 #[cfg(feature = "postgres")]
6589 pub async fn save(
6590 &mut self,
6591 pool: &#root::sql::sqlx::PgPool,
6592 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6593 #pool_to_save_on
6594 }
6595
6596 /// Like [`Self::save`] but accepts any sqlx executor —
6597 /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6598 /// escape hatch for tenant-scoped writes: schema-mode
6599 /// tenants share the registry pool but rely on a per-
6600 /// checkout `SET search_path`, so passing `&PgPool` would
6601 /// silently hit the wrong schema. Acquire a connection
6602 /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6603 ///
6604 /// # Errors
6605 /// As [`Self::save`].
6606 #[cfg(feature = "postgres")]
6607 pub async fn save_on #executor_generics (
6608 &mut self,
6609 #executor_param,
6610 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6611 #executor_where
6612 {
6613 // #1029 — INSERT writes exactly one row → 1; UPDATE returns
6614 // the rows-affected count (0 when the PK no longer exists,
6615 // the Django 6.0 `Model.NotUpdated` signal).
6616 if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6617 return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
6618 }
6619 #audit_update_pre
6620 let _query = #root::core::UpdateQuery {
6621 model: <Self as #root::core::Model>::SCHEMA,
6622 set: ::std::vec![ #( #assignments ),* ],
6623 where_clause: #root::core::WhereExpr::Predicate(
6624 #root::core::Filter {
6625 column: #pk_column_lit,
6626 op: #root::core::Op::Eq,
6627 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6628 ::core::clone::Clone::clone(&self.#pk_ident)
6629 ),
6630 }
6631 ),
6632 };
6633 let _affected = #root::sql::__macro_internals::update_on(
6634 #executor_passes_to_data_write,
6635 &_query,
6636 ).await?;
6637 #audit_update_post
6638 ::core::result::Result::Ok(_affected)
6639 }
6640
6641 /// Per-call override for the audit source. Runs
6642 /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6643 /// scope so the resulting audit entry records `source`
6644 /// instead of the task-local default. Useful for seed
6645 /// scripts and one-off CLI tools that don't sit inside an
6646 /// admin handler. The override applies only to this call;
6647 /// no global state changes.
6648 ///
6649 /// # Errors
6650 /// As [`Self::save_on`].
6651 #[cfg(feature = "postgres")]
6652 pub async fn save_on_with #executor_generics (
6653 &mut self,
6654 #executor_param,
6655 source: #root::audit::AuditSource,
6656 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6657 #executor_where
6658 {
6659 #root::audit::with_source(source, self.save_on(_executor)).await
6660 }
6661
6662 /// Insert this row or update it in-place if the primary key already
6663 /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6664 ///
6665 /// With `Auto::Unset` PK the server assigns a new key and no conflict
6666 /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6667 /// inserted if absent or all non-PK columns are overwritten if present.
6668 ///
6669 /// # Errors
6670 /// As [`Self::insert_on`].
6671 #[cfg(feature = "postgres")]
6672 pub async fn upsert(
6673 &mut self,
6674 pool: &#root::sql::sqlx::PgPool,
6675 ) -> ::core::result::Result<(), #root::sql::ExecError> {
6676 #pool_to_upsert_on
6677 }
6678
6679 /// Like [`Self::upsert`] but accepts any sqlx executor.
6680 /// See [`Self::save_on`] for tenancy-scoped rationale.
6681 ///
6682 /// # Errors
6683 /// As [`Self::upsert`].
6684 #[cfg(feature = "postgres")]
6685 pub async fn upsert_on #executor_generics (
6686 &mut self,
6687 #executor_param,
6688 ) -> ::core::result::Result<(), #root::sql::ExecError>
6689 #executor_where
6690 {
6691 let mut _columns: ::std::vec::Vec<&'static str> =
6692 ::std::vec::Vec::new();
6693 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6694 ::std::vec::Vec::new();
6695 #( #upsert_pushes )*
6696 let query = #root::core::InsertQuery {
6697 model: <Self as #root::core::Model>::SCHEMA,
6698 columns: _columns,
6699 values: _values,
6700 returning: ::std::vec![ #( #upsert_returning ),* ],
6701 on_conflict: ::core::option::Option::Some(#conflict_clause),
6702 };
6703 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
6704 #executor_passes_to_data_write,
6705 &query,
6706 ).await?;
6707 let _returning_row = &_returning_row_v;
6708 #( #upsert_auto_assigns )*
6709 ::core::result::Result::Ok(())
6710 }
6711 })
6712 } else {
6713 None
6714 };
6715
6716 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6717 let pk_column_lit = pk_column.as_str();
6718 // Optional `soft_delete_on` / `restore_on` companions when the
6719 // model has a `#[rustango(soft_delete)]` column. They land
6720 // alongside the regular `delete_on` so callers have both
6721 // options — a hard delete (audit-tracked as a real DELETE) and
6722 // a logical delete (audit-tracked as an UPDATE setting the
6723 // deleted_at column to NOW()).
6724 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6725 let col_lit = col;
6726 let sd_field_ident = fields
6727 .soft_delete_field_ident
6728 .clone()
6729 .expect("soft_delete_column without ident");
6730 quote! {
6731 /// Soft-delete this row by setting its
6732 /// `#[rustango(soft_delete)]` column to `NOW()`.
6733 /// Mirrors Django's `SoftDeleteModel.delete()` shape:
6734 /// the row stays in the table; query helpers can
6735 /// filter it out by checking the column for `IS NOT
6736 /// NULL`.
6737 ///
6738 /// # Errors
6739 /// As [`Self::delete`].
6740 #[cfg(feature = "postgres")]
6741 pub async fn soft_delete_on #executor_generics (
6742 &self,
6743 #executor_param,
6744 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6745 #executor_where
6746 {
6747 let _query = #root::core::UpdateQuery {
6748 model: <Self as #root::core::Model>::SCHEMA,
6749 set: ::std::vec![
6750 #root::core::Assignment {
6751 column: #col_lit,
6752 value: ::core::convert::Into::<#root::core::Expr>::into(
6753 ::core::convert::Into::<#root::core::SqlValue>::into(
6754 #root::__chrono::Utc::now()
6755 )
6756 ),
6757 },
6758 ],
6759 where_clause: #root::core::WhereExpr::Predicate(
6760 #root::core::Filter {
6761 column: #pk_column_lit,
6762 op: #root::core::Op::Eq,
6763 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6764 ::core::clone::Clone::clone(&self.#pk_ident)
6765 ),
6766 }
6767 ),
6768 };
6769 let _affected = #root::sql::__macro_internals::update_on(
6770 #executor_passes_to_data_write,
6771 &_query,
6772 ).await?;
6773 #audit_softdelete_emit
6774 ::core::result::Result::Ok(_affected)
6775 }
6776
6777 /// Inverse of [`Self::soft_delete_on`] — clears the
6778 /// soft-delete column back to NULL so the row is
6779 /// considered live again.
6780 ///
6781 /// # Errors
6782 /// As [`Self::delete`].
6783 #[cfg(feature = "postgres")]
6784 pub async fn restore_on #executor_generics (
6785 &self,
6786 #executor_param,
6787 ) -> ::core::result::Result<u64, #root::sql::ExecError>
6788 #executor_where
6789 {
6790 let _query = #root::core::UpdateQuery {
6791 model: <Self as #root::core::Model>::SCHEMA,
6792 set: ::std::vec![
6793 #root::core::Assignment {
6794 column: #col_lit,
6795 value: ::core::convert::Into::<#root::core::Expr>::into(
6796 #root::core::SqlValue::Null
6797 ),
6798 },
6799 ],
6800 where_clause: #root::core::WhereExpr::Predicate(
6801 #root::core::Filter {
6802 column: #pk_column_lit,
6803 op: #root::core::Op::Eq,
6804 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6805 ::core::clone::Clone::clone(&self.#pk_ident)
6806 ),
6807 }
6808 ),
6809 };
6810 let _affected = #root::sql::__macro_internals::update_on(
6811 #executor_passes_to_data_write,
6812 &_query,
6813 ).await?;
6814 #audit_restore_emit
6815 ::core::result::Result::Ok(_affected)
6816 }
6817
6818 /// Tri-dialect counterpart of [`Self::soft_delete_on`]
6819 /// — takes [`#root::sql::Pool`] and dispatches per
6820 /// backend. Eloquent `Model::delete()` semantics on
6821 /// soft-delete-enabled models (closes #821 partial).
6822 ///
6823 /// Sets the `#[rustango(soft_delete)]` column to
6824 /// `NOW()` on every backend. Query helpers
6825 /// (`QuerySet::active()` / `only_trashed()`,
6826 /// `soft_delete::active_filter` /
6827 /// `compose_with_active`) filter trashed rows out by
6828 /// reading `IS NULL` on the same column.
6829 ///
6830 /// # Errors
6831 /// As [`#root::sql::update_pool`].
6832 pub async fn soft_delete(
6833 &self,
6834 pool: &#root::sql::Pool,
6835 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6836 let _query = #root::core::UpdateQuery {
6837 model: <Self as #root::core::Model>::SCHEMA,
6838 set: ::std::vec![
6839 #root::core::Assignment {
6840 column: #col_lit,
6841 value: ::core::convert::Into::<#root::core::Expr>::into(
6842 ::core::convert::Into::<#root::core::SqlValue>::into(
6843 #root::__chrono::Utc::now()
6844 )
6845 ),
6846 },
6847 ],
6848 where_clause: #root::core::WhereExpr::Predicate(
6849 #root::core::Filter {
6850 column: #pk_column_lit,
6851 op: #root::core::Op::Eq,
6852 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6853 ::core::clone::Clone::clone(&self.#pk_ident)
6854 ),
6855 }
6856 ),
6857 };
6858 #root::sql::update_pool(pool, &_query).await
6859 }
6860
6861 /// Tri-dialect counterpart of [`Self::restore_on`].
6862 /// Clears the `#[rustango(soft_delete)]` column back
6863 /// to `NULL`, marking the row live again. Eloquent
6864 /// `Model::restore()` parity.
6865 ///
6866 /// # Errors
6867 /// As [`#root::sql::update_pool`].
6868 pub async fn restore(
6869 &self,
6870 pool: &#root::sql::Pool,
6871 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6872 let _query = #root::core::UpdateQuery {
6873 model: <Self as #root::core::Model>::SCHEMA,
6874 set: ::std::vec![
6875 #root::core::Assignment {
6876 column: #col_lit,
6877 value: ::core::convert::Into::<#root::core::Expr>::into(
6878 #root::core::SqlValue::Null
6879 ),
6880 },
6881 ],
6882 where_clause: #root::core::WhereExpr::Predicate(
6883 #root::core::Filter {
6884 column: #pk_column_lit,
6885 op: #root::core::Op::Eq,
6886 value: ::core::convert::Into::<#root::core::SqlValue>::into(
6887 ::core::clone::Clone::clone(&self.#pk_ident)
6888 ),
6889 }
6890 ),
6891 };
6892 #root::sql::update_pool(pool, &_query).await
6893 }
6894
6895 /// Hard-delete this row, ignoring the soft-delete
6896 /// column. Eloquent `Model::forceDelete()` parity —
6897 /// the escape hatch when you need to actually purge
6898 /// data (GDPR, fixture cleanup, etc.).
6899 ///
6900 /// Equivalent to [`Self::delete_pool`] (the framework's
6901 /// non-soft delete) — exposed under the Eloquent name
6902 /// for muscle-memory + so soft-delete-enabled models
6903 /// have all three operations (soft / restore / force)
6904 /// in one place.
6905 ///
6906 /// # Errors
6907 /// As [`Self::delete_pool`].
6908 pub async fn force_delete(
6909 &self,
6910 pool: &#root::sql::Pool,
6911 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6912 Self::delete_pool(self, pool).await
6913 }
6914
6915 /// Returns `true` when this row is soft-deleted (its
6916 /// `#[rustango(soft_delete)]` column is currently
6917 /// set — Eloquent `$model->trashed()` parity).
6918 ///
6919 /// Pure in-memory predicate over `&self`; does not
6920 /// hit the database. Useful in admin/template code
6921 /// like `{% if post.trashed() %}…{% endif %}` and in
6922 /// guard clauses on restore/force-delete flows.
6923 pub fn trashed(&self) -> bool {
6924 ::core::option::Option::is_some(&self.#sd_field_ident)
6925 }
6926
6927 /// Fetch every row whose `#[rustango(soft_delete)]`
6928 /// column is `NULL` (a.k.a. the live, non-trashed
6929 /// rows). Eloquent's default `Model::all()` behavior on
6930 /// a soft-delete model (Eloquent auto-scopes trashed
6931 /// rows out; rustango doesn't have auto-scoping yet —
6932 /// see issue #820 — so this is the explicit shortcut).
6933 ///
6934 /// One-liner over `QuerySet::<Self>::default()
6935 /// .active().fetch(pool)`. Closes #821 partial.
6936 ///
6937 /// # Errors
6938 /// As [`#root::sql::FetcherPool::fetch`].
6939 pub async fn active(
6940 pool: &#root::sql::Pool,
6941 ) -> ::core::result::Result<
6942 ::std::vec::Vec<Self>,
6943 #root::sql::ExecError,
6944 > {
6945 use #root::sql::FetcherPool as _;
6946 #root::query::QuerySet::<Self>::default()
6947 .active()
6948 .fetch(pool)
6949 .await
6950 }
6951
6952 /// Fetch ONLY soft-deleted rows. Eloquent
6953 /// `Model::onlyTrashed()->get()` parity — drives the
6954 /// admin "Trash" page, restore flows, GDPR purge
6955 /// scans, etc.
6956 ///
6957 /// One-liner over `QuerySet::<Self>::default()
6958 /// .only_trashed().fetch(pool)`. Closes #821
6959 /// partial.
6960 ///
6961 /// # Errors
6962 /// As [`#root::sql::FetcherPool::fetch`].
6963 pub async fn only_trashed(
6964 pool: &#root::sql::Pool,
6965 ) -> ::core::result::Result<
6966 ::std::vec::Vec<Self>,
6967 #root::sql::ExecError,
6968 > {
6969 use #root::sql::FetcherPool as _;
6970 #root::query::QuerySet::<Self>::default()
6971 .only_trashed()
6972 .fetch(pool)
6973 .await
6974 }
6975
6976 /// Fetch every row, both live and soft-deleted.
6977 /// Eloquent `Model::withTrashed()->get()` parity.
6978 ///
6979 /// Today every queryset already includes trashed rows
6980 /// (rustango has no global-scope tracking yet — issue
6981 /// #820), so this is functionally equivalent to
6982 /// [`Self::all_pool`]. Exposed as a named shortcut so
6983 /// soft-delete-aware code reads `Model::with_trashed_pool`
6984 /// rather than `Model::all_pool` — keeps intent visible
6985 /// in callers and stays correct when auto-scoping lands.
6986 ///
6987 /// Closes #821 partial.
6988 ///
6989 /// # Errors
6990 /// As [`#root::sql::FetcherPool::fetch`].
6991 pub async fn with_trashed(
6992 pool: &#root::sql::Pool,
6993 ) -> ::core::result::Result<
6994 ::std::vec::Vec<Self>,
6995 #root::sql::ExecError,
6996 > {
6997 use #root::sql::FetcherPool as _;
6998 #root::query::QuerySet::<Self>::default()
6999 .with_trashed()
7000 .fetch(pool)
7001 .await
7002 }
7003
7004 }
7005 } else {
7006 quote!()
7007 };
7008 quote! {
7009 /// Delete the row identified by this instance's primary key.
7010 ///
7011 /// Returns the number of rows affected (0 or 1).
7012 ///
7013 /// # Errors
7014 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7015 /// driver failures.
7016 #[cfg(feature = "postgres")]
7017 pub async fn delete(
7018 &self,
7019 pool: &#root::sql::sqlx::PgPool,
7020 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7021 #pool_to_delete_on
7022 }
7023
7024 /// Like [`Self::delete`] but accepts any sqlx executor —
7025 /// for tenant-scoped deletes against an explicitly-acquired
7026 /// connection. See [`Self::save_on`] for the rationale.
7027 ///
7028 /// # Errors
7029 /// As [`Self::delete`].
7030 #[cfg(feature = "postgres")]
7031 pub async fn delete_on #executor_generics (
7032 &self,
7033 #executor_param,
7034 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7035 #executor_where
7036 {
7037 let query = #root::core::DeleteQuery {
7038 model: <Self as #root::core::Model>::SCHEMA,
7039 where_clause: #root::core::WhereExpr::Predicate(
7040 #root::core::Filter {
7041 column: #pk_column_lit,
7042 op: #root::core::Op::Eq,
7043 value: ::core::convert::Into::<#root::core::SqlValue>::into(
7044 ::core::clone::Clone::clone(&self.#pk_ident)
7045 ),
7046 }
7047 ),
7048 };
7049 let _affected = #root::sql::__macro_internals::delete_on(
7050 #executor_passes_to_data_write,
7051 &query,
7052 ).await?;
7053 #audit_delete_emit
7054 ::core::result::Result::Ok(_affected)
7055 }
7056
7057 /// Per-call audit-source override for [`Self::delete_on`].
7058 /// See [`Self::save_on_with`] for shape rationale.
7059 ///
7060 /// # Errors
7061 /// As [`Self::delete_on`].
7062 #[cfg(feature = "postgres")]
7063 pub async fn delete_on_with #executor_generics (
7064 &self,
7065 #executor_param,
7066 source: #root::audit::AuditSource,
7067 ) -> ::core::result::Result<u64, #root::sql::ExecError>
7068 #executor_where
7069 {
7070 #root::audit::with_source(source, self.delete_on(_executor)).await
7071 }
7072 #pool_delete_method
7073 #pool_insert_method
7074 #pool_save_method
7075 #refresh_replicate_methods
7076 #tx_delete_method
7077 #tx_insert_method
7078 #tx_save_method
7079 #soft_delete_methods
7080
7081 /// Returns `true` when `other` represents the same DB
7082 /// row as `self` — i.e. their primary keys compare
7083 /// equal. Eloquent `$model->is($other)` parity.
7084 ///
7085 /// Because both arguments are typed `&Self`, the
7086 /// model/table check is automatic — `Post::is` cannot
7087 /// be invoked against a `Comment` at compile time. Only
7088 /// the PK has to be compared at runtime.
7089 pub fn is(&self, other: &Self) -> bool {
7090 self.#pk_ident == other.#pk_ident
7091 }
7092
7093 /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7094 /// parity.
7095 pub fn is_not(&self, other: &Self) -> bool {
7096 self.#pk_ident != other.#pk_ident
7097 }
7098
7099 /// Returns this row's primary-key value as an
7100 /// [`#root::core::SqlValue`]. Eloquent
7101 /// `$model->getKey()` parity.
7102 ///
7103 /// Useful when you need to thread the PK through a
7104 /// generic `Into<SqlValue>`-bound API without knowing the
7105 /// concrete PK type (`i64` vs `Uuid` vs `String`).
7106 #[must_use]
7107 pub fn get_key(&self) -> #root::core::SqlValue {
7108 ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7109 }
7110
7111 }
7112 });
7113
7114 let insert_method = if fields.has_auto {
7115 let pushes = &fields.insert_pushes;
7116 let returning_cols = &fields.returning_cols;
7117 let auto_assigns = &fields.auto_assigns;
7118 quote! {
7119 /// Insert this row into its table. Skips columns whose
7120 /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7121 /// sequence fills them in, then reads each `Auto` column
7122 /// back via `RETURNING` and stores it on `self`.
7123 ///
7124 /// # Errors
7125 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7126 /// driver failures.
7127 #[cfg(feature = "postgres")]
7128 pub async fn insert(
7129 &mut self,
7130 pool: &#root::sql::sqlx::PgPool,
7131 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7132 #pool_to_insert_on
7133 }
7134
7135 /// Like [`Self::insert`] but accepts any sqlx executor.
7136 /// See [`Self::save_on`] for tenancy-scoped rationale.
7137 ///
7138 /// # Errors
7139 /// As [`Self::insert`].
7140 #[cfg(feature = "postgres")]
7141 pub async fn insert_on #executor_generics (
7142 &mut self,
7143 #executor_param,
7144 ) -> ::core::result::Result<(), #root::sql::ExecError>
7145 #executor_where
7146 {
7147 let mut _columns: ::std::vec::Vec<&'static str> =
7148 ::std::vec::Vec::new();
7149 let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7150 ::std::vec::Vec::new();
7151 #( #pushes )*
7152 let query = #root::core::InsertQuery {
7153 model: <Self as #root::core::Model>::SCHEMA,
7154 columns: _columns,
7155 values: _values,
7156 returning: ::std::vec![ #( #returning_cols ),* ],
7157 on_conflict: ::core::option::Option::None,
7158 };
7159 let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7160 #executor_passes_to_data_write,
7161 &query,
7162 ).await?;
7163 let _returning_row = &_returning_row_v;
7164 #( #auto_assigns )*
7165 #audit_insert_emit
7166 ::core::result::Result::Ok(())
7167 }
7168
7169 /// Per-call audit-source override for [`Self::insert_on`].
7170 /// See [`Self::save_on_with`] for shape rationale.
7171 ///
7172 /// # Errors
7173 /// As [`Self::insert_on`].
7174 #[cfg(feature = "postgres")]
7175 pub async fn insert_on_with #executor_generics (
7176 &mut self,
7177 #executor_param,
7178 source: #root::audit::AuditSource,
7179 ) -> ::core::result::Result<(), #root::sql::ExecError>
7180 #executor_where
7181 {
7182 #root::audit::with_source(source, self.insert_on(_executor)).await
7183 }
7184 }
7185 } else {
7186 let insert_columns = &fields.insert_columns;
7187 let insert_values = &fields.insert_values;
7188 quote! {
7189 /// Insert this row into its table.
7190 ///
7191 /// # Errors
7192 /// Returns [`#root::sql::ExecError`] for SQL-writing or
7193 /// driver failures.
7194 #[cfg(feature = "postgres")]
7195 pub async fn insert(
7196 &self,
7197 pool: &#root::sql::sqlx::PgPool,
7198 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7199 self.insert_on(pool).await
7200 }
7201
7202 /// Like [`Self::insert`] but accepts any sqlx executor.
7203 /// See [`Self::save_on`] for tenancy-scoped rationale.
7204 ///
7205 /// # Errors
7206 /// As [`Self::insert`].
7207 #[cfg(feature = "postgres")]
7208 pub async fn insert_on<'_c, _E>(
7209 &self,
7210 _executor: _E,
7211 ) -> ::core::result::Result<(), #root::sql::ExecError>
7212 where
7213 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7214 {
7215 let query = #root::core::InsertQuery {
7216 model: <Self as #root::core::Model>::SCHEMA,
7217 columns: ::std::vec![ #( #insert_columns ),* ],
7218 values: ::std::vec![ #( #insert_values ),* ],
7219 returning: ::std::vec::Vec::new(),
7220 on_conflict: ::core::option::Option::None,
7221 };
7222 #root::sql::__macro_internals::insert_on(_executor, &query).await
7223 }
7224 }
7225 };
7226
7227 let bulk_insert_method = if fields.has_auto {
7228 let cols_no_auto = &fields.bulk_columns_no_auto;
7229 let cols_all = &fields.bulk_columns_all;
7230 let pushes_no_auto = &fields.bulk_pushes_no_auto;
7231 let pushes_all = &fields.bulk_pushes_all;
7232 let returning_cols = &fields.returning_cols;
7233 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7234 let uniformity = &fields.bulk_auto_uniformity;
7235 let first_auto_ident = fields
7236 .first_auto_ident
7237 .as_ref()
7238 .expect("has_auto implies first_auto_ident is Some");
7239 quote! {
7240 /// Bulk-insert `rows` in a single round-trip. Every row's
7241 /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7242 /// (sequence fills them in) or uniformly `Auto::Set(_)`
7243 /// (caller-supplied values). Mixed Set/Unset is rejected
7244 /// — call `insert` per row for that case.
7245 ///
7246 /// Empty slice is a no-op. Each row's `Auto` fields are
7247 /// populated from the `RETURNING` clause in input order
7248 /// before this returns.
7249 ///
7250 /// # Errors
7251 /// Returns [`#root::sql::ExecError`] for validation,
7252 /// SQL-writing, mixed-Auto rejection, or driver failures.
7253 #[cfg(feature = "postgres")]
7254 pub async fn bulk_insert(
7255 rows: &mut [Self],
7256 pool: &#root::sql::sqlx::PgPool,
7257 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7258 #pool_to_bulk_insert_on
7259 }
7260
7261 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7262 /// See [`Self::save_on`] for tenancy-scoped rationale.
7263 ///
7264 /// # Errors
7265 /// As [`Self::bulk_insert`].
7266 #[cfg(feature = "postgres")]
7267 pub async fn bulk_insert_on #executor_generics (
7268 rows: &mut [Self],
7269 #executor_param,
7270 ) -> ::core::result::Result<(), #root::sql::ExecError>
7271 #executor_where
7272 {
7273 if rows.is_empty() {
7274 return ::core::result::Result::Ok(());
7275 }
7276 let _first_unset = matches!(
7277 rows[0].#first_auto_ident,
7278 #root::sql::Auto::Unset
7279 );
7280 #( #uniformity )*
7281
7282 let mut _all_rows: ::std::vec::Vec<
7283 ::std::vec::Vec<#root::core::SqlValue>,
7284 > = ::std::vec::Vec::with_capacity(rows.len());
7285 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7286 for _row in rows.iter() {
7287 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7288 ::std::vec::Vec::new();
7289 #( #pushes_no_auto )*
7290 _all_rows.push(_row_vals);
7291 }
7292 ::std::vec![ #( #cols_no_auto ),* ]
7293 } else {
7294 for _row in rows.iter() {
7295 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7296 ::std::vec::Vec::new();
7297 #( #pushes_all )*
7298 _all_rows.push(_row_vals);
7299 }
7300 ::std::vec![ #( #cols_all ),* ]
7301 };
7302
7303 let _query = #root::core::BulkInsertQuery {
7304 model: <Self as #root::core::Model>::SCHEMA,
7305 columns: _columns,
7306 rows: _all_rows,
7307 returning: ::std::vec![ #( #returning_cols ),* ],
7308 on_conflict: ::core::option::Option::None,
7309 };
7310 let _returned = #root::sql::__macro_internals::bulk_insert_on(
7311 #executor_passes_to_data_write,
7312 &_query,
7313 ).await?;
7314 if _returned.len() != rows.len() {
7315 return ::core::result::Result::Err(
7316 #root::sql::ExecError::Sql(
7317 #root::sql::SqlError::BulkInsertReturningMismatch {
7318 expected: rows.len(),
7319 actual: _returned.len(),
7320 }
7321 )
7322 );
7323 }
7324 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7325 #auto_assigns_for_row
7326 }
7327 #audit_bulk_insert_emit
7328 ::core::result::Result::Ok(())
7329 }
7330 }
7331 } else {
7332 let cols_all = &fields.bulk_columns_all;
7333 let pushes_all = &fields.bulk_pushes_all;
7334 quote! {
7335 /// Bulk-insert `rows` in a single round-trip. Every row's
7336 /// fields are written verbatim — there are no `Auto<T>`
7337 /// fields on this model.
7338 ///
7339 /// Empty slice is a no-op.
7340 ///
7341 /// # Errors
7342 /// Returns [`#root::sql::ExecError`] for validation,
7343 /// SQL-writing, or driver failures.
7344 #[cfg(feature = "postgres")]
7345 pub async fn bulk_insert(
7346 rows: &[Self],
7347 pool: &#root::sql::sqlx::PgPool,
7348 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7349 Self::bulk_insert_on(rows, pool).await
7350 }
7351
7352 /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7353 /// See [`Self::save_on`] for tenancy-scoped rationale.
7354 ///
7355 /// # Errors
7356 /// As [`Self::bulk_insert`].
7357 #[cfg(feature = "postgres")]
7358 pub async fn bulk_insert_on<'_c, _E>(
7359 rows: &[Self],
7360 _executor: _E,
7361 ) -> ::core::result::Result<(), #root::sql::ExecError>
7362 where
7363 _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7364 {
7365 if rows.is_empty() {
7366 return ::core::result::Result::Ok(());
7367 }
7368 let mut _all_rows: ::std::vec::Vec<
7369 ::std::vec::Vec<#root::core::SqlValue>,
7370 > = ::std::vec::Vec::with_capacity(rows.len());
7371 for _row in rows.iter() {
7372 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7373 ::std::vec::Vec::new();
7374 #( #pushes_all )*
7375 _all_rows.push(_row_vals);
7376 }
7377 let _query = #root::core::BulkInsertQuery {
7378 model: <Self as #root::core::Model>::SCHEMA,
7379 columns: ::std::vec![ #( #cols_all ),* ],
7380 rows: _all_rows,
7381 returning: ::std::vec::Vec::new(),
7382 on_conflict: ::core::option::Option::None,
7383 };
7384 let _ = #root::sql::__macro_internals::bulk_insert_on(_executor, &_query).await?;
7385 ::core::result::Result::Ok(())
7386 }
7387 }
7388 };
7389
7390 // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7391 // (no postgres-feature gate); routes through the existing
7392 // `bulk_insert_pool` + per-dialect conflict writer.
7393 //
7394 // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7395 // sequence picks the PK for fresh inserts; the UPDATE branch never
7396 // touches the Auto column.
7397 let bulk_upsert_pool_method = {
7398 // Pick the "no Auto" columns when the model has Auto fields,
7399 // else every column.
7400 let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7401 (
7402 fields.bulk_columns_no_auto.clone(),
7403 fields.bulk_pushes_no_auto.clone(),
7404 )
7405 } else {
7406 (
7407 fields.bulk_columns_all.clone(),
7408 fields.bulk_pushes_all.clone(),
7409 )
7410 };
7411 quote! {
7412 /// Tri-dialect `bulk_create(update_conflicts=True)` — Django's
7413 /// canonical "import a batch idempotently" shape. Issue #267
7414 /// / T1.5.
7415 ///
7416 /// Per-row values are extracted and lowered into a
7417 /// [`#root::core::BulkInsertQuery`] with
7418 /// `on_conflict = DoUpdate { target, update_columns }`. The
7419 /// writer dispatches per-dialect:
7420 /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7421 /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)` (target ignored — MySQL matches every UNIQUE index)
7422 ///
7423 /// `target` names the column(s) whose unique constraint
7424 /// defines the conflict (typically a `unique` or
7425 /// `unique_together` natural-key column, NOT the `Auto<T>`
7426 /// PK). `update_cols` names the columns to overwrite on
7427 /// conflict — every other column is left untouched on the
7428 /// existing row.
7429 ///
7430 /// Auto-PK rows must all have `Auto::Unset` (the sequence
7431 /// picks the PK on insert; the update path never touches
7432 /// the Auto column). Auto-set rows trigger a hard error.
7433 /// Empty slice is a no-op.
7434 ///
7435 /// # Errors
7436 /// Returns [`#root::sql::ExecError`] for validation,
7437 /// SQL-writing, or driver failures.
7438 pub async fn bulk_upsert_pool(
7439 rows: &[Self],
7440 target: &[&'static str],
7441 update_cols: &[&'static str],
7442 pool: &#root::sql::Pool,
7443 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7444 if rows.is_empty() {
7445 return ::core::result::Result::Ok(());
7446 }
7447 let mut _all_rows: ::std::vec::Vec<
7448 ::std::vec::Vec<#root::core::SqlValue>,
7449 > = ::std::vec::Vec::with_capacity(rows.len());
7450 for _row in rows.iter() {
7451 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7452 ::std::vec::Vec::new();
7453 #( #upsert_pushes )*
7454 _all_rows.push(_row_vals);
7455 }
7456 let _query = #root::core::BulkInsertQuery {
7457 model: <Self as #root::core::Model>::SCHEMA,
7458 columns: ::std::vec![ #( #upsert_cols ),* ],
7459 rows: _all_rows,
7460 returning: ::std::vec::Vec::new(),
7461 on_conflict: ::core::option::Option::Some(
7462 #root::core::ConflictClause::DoUpdate {
7463 target: target.to_vec(),
7464 update_columns: update_cols.to_vec(),
7465 }
7466 ),
7467 };
7468 #root::sql::bulk_insert_pool(pool, &_query).await
7469 }
7470
7471 /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7472 /// skip rows that would violate a unique constraint. Issue
7473 /// #267 / T1.5. Same per-dialect dispatch as
7474 /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7475 /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7476 /// <pivot> = <pivot>` (MySQL no-op write).
7477 ///
7478 /// # Errors
7479 /// As [`Self::bulk_upsert_pool`].
7480 pub async fn bulk_insert_or_ignore_pool(
7481 rows: &[Self],
7482 pool: &#root::sql::Pool,
7483 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7484 if rows.is_empty() {
7485 return ::core::result::Result::Ok(());
7486 }
7487 let mut _all_rows: ::std::vec::Vec<
7488 ::std::vec::Vec<#root::core::SqlValue>,
7489 > = ::std::vec::Vec::with_capacity(rows.len());
7490 for _row in rows.iter() {
7491 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7492 ::std::vec::Vec::new();
7493 #( #upsert_pushes )*
7494 _all_rows.push(_row_vals);
7495 }
7496 let _query = #root::core::BulkInsertQuery {
7497 model: <Self as #root::core::Model>::SCHEMA,
7498 columns: ::std::vec![ #( #upsert_cols ),* ],
7499 rows: _all_rows,
7500 returning: ::std::vec::Vec::new(),
7501 on_conflict: ::core::option::Option::Some(
7502 #root::core::ConflictClause::DoNothing
7503 ),
7504 };
7505 #root::sql::bulk_insert_pool(pool, &_query).await
7506 }
7507 }
7508 };
7509
7510 // Ergonomic `Model::bulk_update(objs, fields)` — Django's
7511 // `QuerySet.bulk_update`. The SQL/IR/executor stack
7512 // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7513 // `write_bulk_update_*` writers) already existed; what was missing
7514 // was the per-model constructor that maps `&[Self]` + a runtime
7515 // column list into rows of `[pk, col_vals…]` so callers don't
7516 // hand-build the IR. Emitted only when the model has a primary key
7517 // (the PK is the join key and can't itself be updated).
7518 let bulk_update_method = match &fields.primary_key {
7519 None => quote! {},
7520 Some((pk_ident, pk_col)) => {
7521 // One pair of match arms per non-PK column: a validation arm
7522 // resolving the runtime name to its `&'static str` column,
7523 // and a value arm pushing that field off the row.
7524 let mut col_arms: Vec<TokenStream2> = Vec::new();
7525 let mut val_arms: Vec<TokenStream2> = Vec::new();
7526 for entry in &fields.column_entries {
7527 if &entry.column == pk_col {
7528 continue;
7529 }
7530 let col = &entry.column;
7531 let ident = &entry.ident;
7532 col_arms.push(quote! { #col => #col, });
7533 val_arms.push(quote! {
7534 #col => _row_vals.push(
7535 ::core::convert::Into::<#root::core::SqlValue>::into(
7536 ::core::clone::Clone::clone(&_o.#ident)
7537 )
7538 ),
7539 });
7540 }
7541 quote! {
7542 /// Django's `QuerySet.bulk_update(objs, fields)` — write
7543 /// per-row-different values for the named `fields` across
7544 /// every object in `objs` in a single statement, matched
7545 /// by primary key.
7546 ///
7547 /// `fields` names the **columns** to update. The primary
7548 /// key identifies each row and cannot itself be updated
7549 /// (pass it and you get
7550 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7551 /// Empty `objs` or `fields` is a no-op returning `0`.
7552 /// Objects whose PK matches no row are simply not updated.
7553 /// Returns the number of rows affected.
7554 ///
7555 /// Tri-dialect: lowers to one
7556 /// [`#root::core::BulkUpdateQuery`] and dispatches
7557 /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7558 /// a CTE + correlated subquery on SQLite, an inner
7559 /// `JOIN (VALUES …)` on MySQL.
7560 ///
7561 /// # Errors
7562 /// [`#root::core::QueryError::UnknownField`] for a name
7563 /// that isn't a column on this model,
7564 /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7565 /// `fields` names the PK, or [`#root::sql::ExecError`] for
7566 /// SQL-writing / driver failures.
7567 pub async fn bulk_update(
7568 objs: &[Self],
7569 fields: &[&str],
7570 pool: &#root::sql::Pool,
7571 ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7572 if objs.is_empty() || fields.is_empty() {
7573 return ::core::result::Result::Ok(0);
7574 }
7575 let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7576 let mut _update_columns: ::std::vec::Vec<&'static str> =
7577 ::std::vec::Vec::with_capacity(fields.len());
7578 for &_f in fields {
7579 let _col: &'static str = match _f {
7580 #pk_col => {
7581 return ::core::result::Result::Err(
7582 ::core::convert::Into::into(
7583 #root::core::QueryError::BulkUpdatePrimaryKey {
7584 model: _model_name,
7585 field: ::std::string::ToString::to_string(_f),
7586 }
7587 )
7588 );
7589 }
7590 #( #col_arms )*
7591 _ => {
7592 return ::core::result::Result::Err(
7593 ::core::convert::Into::into(
7594 #root::core::QueryError::UnknownField {
7595 model: _model_name,
7596 field: ::std::string::ToString::to_string(_f),
7597 }
7598 )
7599 );
7600 }
7601 };
7602 _update_columns.push(_col);
7603 }
7604 let mut _rows: ::std::vec::Vec<
7605 ::std::vec::Vec<#root::core::SqlValue>,
7606 > = ::std::vec::Vec::with_capacity(objs.len());
7607 for _o in objs.iter() {
7608 let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7609 ::std::vec::Vec::with_capacity(fields.len() + 1);
7610 // PK first — the writers expect `[pk, …update cols]`.
7611 _row_vals.push(
7612 ::core::convert::Into::<#root::core::SqlValue>::into(
7613 ::core::clone::Clone::clone(&_o.#pk_ident)
7614 )
7615 );
7616 for &_f in fields {
7617 match _f {
7618 #( #val_arms )*
7619 // Unreachable: every name was validated
7620 // against the same arm set above.
7621 _ => {}
7622 }
7623 }
7624 _rows.push(_row_vals);
7625 }
7626 let _query = #root::core::BulkUpdateQuery {
7627 model: <Self as #root::core::Model>::SCHEMA,
7628 update_columns: _update_columns,
7629 rows: _rows,
7630 };
7631 #root::sql::bulk_update_pool(pool, &_query).await
7632 }
7633 }
7634 }
7635 };
7636
7637 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7638 quote! {
7639 /// Hidden runtime accessor for the primary-key value as a
7640 /// [`SqlValue`]. Used by reverse-relation helpers
7641 /// (`<parent>::<child>_set`) emitted from sibling models'
7642 /// FK fields. Not part of the public API.
7643 #[doc(hidden)]
7644 pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7645 ::core::convert::Into::<#root::core::SqlValue>::into(
7646 ::core::clone::Clone::clone(&self.#pk_ident)
7647 )
7648 }
7649 }
7650 });
7651
7652 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7653 quote! {
7654 impl #root::sql::HasPkValue for #struct_name {
7655 fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7656 ::core::convert::Into::<#root::core::SqlValue>::into(
7657 ::core::clone::Clone::clone(&self.#pk_ident)
7658 )
7659 }
7660 }
7661 }
7662 });
7663
7664 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7665
7666 // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7667 // dispatch to the right per-backend body without the macro emitting
7668 // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7669 // so audited models with non-Auto PKs (which still go through
7670 // `insert_one_with_audit` → `apply_auto_pk`) link.
7671 let assign_auto_pk_pool_impl = {
7672 let auto_assigns = &fields.auto_assigns;
7673 // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7674 // so the body is structurally identical to `auto_assigns` —
7675 // only the helper name swaps from `try_get_returning` to
7676 // `try_get_returning_sqlite` so the closure typechecks against
7677 // a `SqliteRow` instead of a `PgRow`.
7678 let auto_assigns_sqlite: Vec<TokenStream2> = fields
7679 .auto_field_idents
7680 .iter()
7681 .map(|(ident, column)| {
7682 quote! {
7683 self.#ident = #root::sql::try_get_returning_sqlite(
7684 _returning_row, #column
7685 )?;
7686 }
7687 })
7688 .collect();
7689 // #1028 — decode each `generated_as` column from the same
7690 // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7691 // own type drives the decode (no `Auto<T>` wrapper).
7692 let generated_assigns: Vec<TokenStream2> = fields
7693 .generated_field_idents
7694 .iter()
7695 .map(|(ident, column)| {
7696 quote! {
7697 self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7698 }
7699 })
7700 .collect();
7701 let generated_assigns_sqlite: Vec<TokenStream2> = fields
7702 .generated_field_idents
7703 .iter()
7704 .map(|(ident, column)| {
7705 quote! {
7706 self.#ident = #root::sql::try_get_returning_sqlite(
7707 _returning_row, #column
7708 )?;
7709 }
7710 })
7711 .collect();
7712 let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
7713 // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7714 // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7715 // Auto<Uuid>/etc. fail to link against MySQL (intended —
7716 // those models can't use AUTO_INCREMENT). The trait is only
7717 // touched on the MySQL arm at runtime, so PG-only consumers
7718 // never see the bound failure.
7719 //
7720 // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7721 // Auto<i64> PK + auto_now_add timestamp) errored hard at
7722 // runtime with "multi-column RETURNING". MySQL has no
7723 // multi-column RETURNING semantic and a follow-up SELECT
7724 // would need cross-trait plumbing. Pragmatic shape: succeed
7725 // with the FIRST Auto field populated from LAST_INSERT_ID();
7726 // any other Auto fields stay `Auto::Unset`. Callers that
7727 // need the DB-defaulted timestamp / UUID can re-fetch the
7728 // row by PK after `save_pool`. Fixes the cookbook chapter
7729 // 12 dialect divergence.
7730 let value_ty = fields
7731 .first_auto_value_ty
7732 .as_ref()
7733 .expect("first_auto_value_ty set whenever first_auto_ident is");
7734 quote! {
7735 let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7736 ::rustango_from_mysql_auto_id(_id)?;
7737 self.#first = #root::sql::Auto::Set(_converted);
7738 ::core::result::Result::Ok(())
7739 }
7740 } else {
7741 quote! {
7742 let _ = _id;
7743 ::core::result::Result::Ok(())
7744 }
7745 };
7746 quote! {
7747 impl #root::sql::AssignAutoPkPool for #struct_name {
7748 fn __rustango_assign_from_pg_row(
7749 &mut self,
7750 _returning_row: &#root::sql::PgReturningRow,
7751 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7752 #( #auto_assigns )*
7753 #( #generated_assigns )*
7754 ::core::result::Result::Ok(())
7755 }
7756 fn __rustango_assign_from_mysql_id(
7757 &mut self,
7758 _id: i64,
7759 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7760 #mysql_body
7761 }
7762 fn __rustango_assign_from_sqlite_row(
7763 &mut self,
7764 _returning_row: &#root::sql::SqliteReturningRow,
7765 ) -> ::core::result::Result<(), #root::sql::ExecError> {
7766 #( #auto_assigns_sqlite )*
7767 #( #generated_assigns_sqlite )*
7768 ::core::result::Result::Ok(())
7769 }
7770 }
7771 }
7772 };
7773
7774 let from_aliased_row_inits = &fields.from_aliased_row_inits;
7775 let aliased_row_helper = quote! {
7776 /// Decode a row's aliased target columns (produced by
7777 /// `select_related`'s LEFT JOIN) into a fresh instance of
7778 /// this model. Reads each column via
7779 /// `format!("{prefix}__{col}")`, matching the alias the
7780 /// SELECT writer emitted. Slice 9.0d.
7781 #[doc(hidden)]
7782 #[cfg(feature = "postgres")]
7783 pub fn __rustango_from_aliased_row(
7784 row: &#root::sql::sqlx::postgres::PgRow,
7785 prefix: &str,
7786 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7787 ::core::result::Result::Ok(Self {
7788 #( #from_aliased_row_inits ),*
7789 })
7790 }
7791 };
7792 // v0.23.0-batch8 — MySQL counterpart, gated through the
7793 // cfg-aware macro_rules so PG-only builds expand to nothing.
7794 let aliased_row_helper_my = quote! {
7795 #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7796 #( #from_aliased_row_inits ),*
7797 });
7798 };
7799
7800 // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7801 // pattern + cfg gate on the `sqlite` feature.
7802 let aliased_row_helper_sqlite = quote! {
7803 #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7804 #( #from_aliased_row_inits ),*
7805 });
7806 };
7807
7808 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7809 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7810 let load_related_impl_sqlite =
7811 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7812
7813 // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7814 // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7815 // default `Self::objects()`. Each accessor returns a fresh
7816 // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7817 // methods the user defined.
7818 let extra_manager_fns: Vec<TokenStream2> = manager_fns
7819 .iter()
7820 .map(|fn_ident| {
7821 let model_name_str = struct_name.to_string();
7822 let fn_name_str = fn_ident.to_string();
7823 let doc = format!(
7824 "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7825 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7826 equivalent to `Self::objects()`. Chains with any \
7827 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7828 extension methods."
7829 );
7830 quote! {
7831 #[doc = #doc]
7832 #[must_use]
7833 pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7834 #root::query::QuerySet::new()
7835 }
7836 }
7837 })
7838 .collect();
7839
7840 quote! {
7841 impl #struct_name {
7842 /// Start a new `QuerySet` over this model. Django shape.
7843 #[must_use]
7844 pub fn objects() -> #root::query::QuerySet<#struct_name> {
7845 #root::query::QuerySet::new()
7846 }
7847
7848 /// Eloquent-shape alias of [`Self::objects`]. Returns
7849 /// a fresh `QuerySet<Self>` ready for `.filter()` /
7850 /// `.where_()` / etc. Matches Laravel muscle-memory:
7851 ///
7852 /// ```ignore
7853 /// // Eloquent: Post::query()->where('published', true)
7854 /// // Django: Post.objects.filter(published=True)
7855 /// // rustango: Post::query().filter("published", true)
7856 /// // or: Post::objects().filter("published", true)
7857 /// ```
7858 ///
7859 /// Both names point at the same underlying constructor;
7860 /// neither is preferred.
7861 #[must_use]
7862 pub fn query() -> #root::query::QuerySet<#struct_name> {
7863 #root::query::QuerySet::new()
7864 }
7865
7866 #( #extra_manager_fns )*
7867
7868 #insert_method
7869
7870 #bulk_insert_method
7871
7872 #bulk_upsert_pool_method
7873
7874 #bulk_update_method
7875
7876 #save_method
7877
7878 #pk_methods
7879
7880 #pk_value_helper
7881
7882 #aliased_row_helper
7883
7884 #column_consts
7885 }
7886
7887 #aliased_row_helper_my
7888
7889 #aliased_row_helper_sqlite
7890
7891 #load_related_impl
7892
7893 #load_related_impl_my
7894
7895 #load_related_impl_sqlite
7896
7897 #has_pk_value_impl
7898
7899 #fk_pk_access_impl
7900
7901 #assign_auto_pk_pool_impl
7902 }
7903}
7904
7905/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
7906/// `auto_assigns` but reading from `_returning_row` and writing to
7907/// `_row_mut` instead of `self`.
7908fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
7909 let root = rustango_root();
7910 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
7911 let col_lit = column.as_str();
7912 quote! {
7913 _row_mut.#ident = #root::sql::sqlx::Row::try_get(
7914 _returning_row,
7915 #col_lit,
7916 )?;
7917 }
7918 });
7919 quote! { #( #lines )* }
7920}
7921
7922/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
7923fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
7924 let lines = entries.iter().map(|e| {
7925 let ident = &e.ident;
7926 let col_ty = column_type_ident(ident);
7927 quote! {
7928 #[allow(non_upper_case_globals)]
7929 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
7930 }
7931 });
7932 quote! { #(#lines)* }
7933}
7934
7935/// Emit a hidden per-model module carrying one zero-sized type per field,
7936/// each with a `Column` impl pointing back at the model.
7937fn column_module_tokens(
7938 module_ident: &syn::Ident,
7939 struct_name: &syn::Ident,
7940 entries: &[ColumnEntry],
7941) -> TokenStream2 {
7942 let root = rustango_root();
7943 let items = entries.iter().map(|e| {
7944 let col_ty = column_type_ident(&e.ident);
7945 let value_ty = &e.value_ty;
7946 let name = &e.name;
7947 let column = &e.column;
7948 let field_type_tokens = &e.field_type_tokens;
7949 quote! {
7950 #[derive(::core::clone::Clone, ::core::marker::Copy)]
7951 pub struct #col_ty;
7952
7953 impl #root::core::Column for #col_ty {
7954 type Model = super::#struct_name;
7955 type Value = #value_ty;
7956 const NAME: &'static str = #name;
7957 const COLUMN: &'static str = #column;
7958 const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
7959 }
7960 }
7961 });
7962 quote! {
7963 #[doc(hidden)]
7964 #[allow(non_camel_case_types, non_snake_case)]
7965 pub mod #module_ident {
7966 // Re-import the parent scope so field types referencing
7967 // sibling models (e.g. `ForeignKey<Author>`) resolve
7968 // inside this submodule. Without this we'd hit
7969 // `proc_macro_derive_resolution_fallback` warnings.
7970 #[allow(unused_imports)]
7971 use super::*;
7972 #(#items)*
7973 }
7974 }
7975}
7976
7977fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
7978 syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
7979}
7980
7981fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
7982 syn::Ident::new(
7983 &format!("__rustango_cols_{struct_name}"),
7984 struct_name.span(),
7985 )
7986}
7987
7988fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
7989 let root = rustango_root();
7990 // The Postgres impl is always emitted — every rustango build pulls in
7991 // sqlx-postgres via the default `postgres` feature. The MySQL impl is
7992 // routed through `#root::__impl_my_from_row!`, a cfg-gated
7993 // macro_rules whose body collapses to nothing when rustango is built
7994 // without the `mysql` feature. No user-facing feature shim required.
7995 //
7996 // The macro_rules pattern expects `[ field: expr, … ]` — we need to
7997 // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
7998 // back into a comma-separated list inside square brackets. Since each
7999 // entry is already in `field: expr` shape, the existing tokens slot in.
8000 quote! {
8001 #[cfg(feature = "postgres")]
8002 impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8003 for #struct_name
8004 {
8005 fn from_row(
8006 row: &'r #root::sql::sqlx::postgres::PgRow,
8007 ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8008 ::core::result::Result::Ok(Self {
8009 #( #from_row_inits ),*
8010 })
8011 }
8012 }
8013
8014 #root::__impl_my_from_row!(#struct_name, |row| {
8015 #( #from_row_inits ),*
8016 });
8017
8018 #root::__impl_sqlite_from_row!(#struct_name, |row| {
8019 #( #from_row_inits ),*
8020 });
8021 }
8022}
8023
8024struct ContainerAttrs {
8025 table: Option<String>,
8026 display: Option<(String, proc_macro2::Span)>,
8027 /// Explicit Django-style app label from `#[rustango(app = "blog")]`.
8028 /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8029 /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8030 /// at runtime — this attribute is the override for cases where
8031 /// the inference is wrong (e.g. a model that conceptually belongs
8032 /// to one app but is physically in another module).
8033 app: Option<String>,
8034 /// Django ModelAdmin-shape per-model knobs from
8035 /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8036 /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8037 /// admin code falls back to `AdminConfig::DEFAULT`.
8038 admin: Option<AdminAttrs>,
8039 /// Per-model audit configuration from `#[rustango(audit(...))]`.
8040 /// `None` when the model isn't audited — write paths emit no
8041 /// audit entries. When present, single-row writes capture
8042 /// before/after for the listed fields and bulk writes batch
8043 /// snapshots into one INSERT into `rustango_audit_log`.
8044 audit: Option<AuditAttrs>,
8045 /// `true` when `#[rustango(permissions)]` is present. Signals that
8046 /// `auto_create_permissions` should seed the four CRUD codenames for
8047 /// this model.
8048 permissions: bool,
8049 /// Many-to-many relations declared via
8050 /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8051 /// src = "post_id", dst = "tag_id"))]`.
8052 m2m: Vec<M2MAttr>,
8053 /// Polymorphic M2M relations declared via
8054 /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8055 /// pk_column = "taggable_id", ct_column = "taggable_type",
8056 /// related_column = "tag_id"))]` (issue #818).
8057 generic_m2m: Vec<GenericM2MAttr>,
8058 /// Composite indexes declared via
8059 /// `#[rustango(index("col1, col2"))]` or
8060 /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8061 /// Single-column indexes from `#[rustango(index)]` on fields are
8062 /// accumulated here during field collection.
8063 indexes: Vec<IndexAttr>,
8064 /// Table-level CHECK constraints declared via
8065 /// `#[rustango(check(name = "…", expr = "…"))]`.
8066 checks: Vec<CheckAttr>,
8067 /// Table-level PG `EXCLUDE` constraints declared via
8068 /// `#[rustango(exclude(name = "…", using = "gist", elements =
8069 /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8070 /// migration writer renders them on Postgres and skips with a
8071 /// warning on MySQL/SQLite. Issue #319.
8072 excludes: Vec<ExcludeAttr>,
8073 /// Composite (multi-column) FKs declared via
8074 /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8075 /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8076 composite_fks: Vec<CompositeFkAttr>,
8077 /// Generic ("any model") FKs declared via
8078 /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8079 /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8080 generic_fks: Vec<GenericFkAttr>,
8081 /// Where this model lives in a tenancy deployment, declared via
8082 /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8083 /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8084 /// to partition diff output between registry-scoped and
8085 /// tenant-scoped migration files.
8086 scope: Option<String>,
8087 /// Custom-Manager extension-trait name from
8088 /// `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8089 /// When set, the macro emits an empty `pub trait <name>: Sized {}`
8090 /// adjacent to the model so users can write
8091 /// `impl FooManagerExt for QuerySet<Foo> { fn published(self) -> Self ... }`
8092 /// and discover the convention from the model definition.
8093 manager_ext: Option<syn::Ident>,
8094 /// Extra QuerySet accessor names from
8095 /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8096 /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8097 /// the default `Self::objects()`. Multiple attributes allowed.
8098 manager_fns: Vec<syn::Ident>,
8099 /// Default ordering declared via `#[rustango(default_order =
8100 /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8101 /// `(column_name, desc_flag, span_for_error_reporting)` — the
8102 /// `-` prefix means descending; the `+` prefix or no prefix means
8103 /// ascending.
8104 default_order: Vec<(String, bool, proc_macro2::Span)>,
8105 /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8106 /// Routes the emitted schema's `is_view = true` so the migration
8107 /// snapshot skips this model (its underlying SQL view is operator-
8108 /// managed, not rustango-managed).
8109 is_view: bool,
8110 /// Django-shape `Meta.managed` from `#[rustango(managed = false)]`.
8111 /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8112 /// the migration snapshot skips this model so `makemigrations` /
8113 /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8114 /// TABLE` against it (operator-managed schema).
8115 managed: bool,
8116 /// Django-shape `Meta.base_manager_name` from
8117 /// `#[rustango(base_manager_name = "...")]`. Threaded into
8118 /// `ModelSchema::base_manager_name`. Declarative-only today.
8119 base_manager_name: Option<String>,
8120 /// Django-shape `Meta.order_with_respect_to = "parent_fk"` from
8121 /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8122 /// field this model's instances are ordered relative to.
8123 /// Declarative-only today; threaded onto
8124 /// `ModelSchema::order_with_respect_to`.
8125 order_with_respect_to: Option<String>,
8126 /// Django-shape `Meta.proxy = True` from `#[rustango(proxy)]` /
8127 /// `#[rustango(proxy = true)]`. Marks the model as a proxy that
8128 /// shares its DB table with another struct. Threaded into
8129 /// `ModelSchema::proxy` so future codegen can skip table-owning
8130 /// behavior for proxies.
8131 proxy: bool,
8132 /// Django-shape `Meta.required_db_features` from
8133 /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8134 /// Each comma-separated capability token surfaces on
8135 /// `ModelSchema::required_db_features` so `manage check --deploy`
8136 /// can warn when the active dialect lacks one.
8137 required_db_features: Vec<String>,
8138 /// Django-shape `Meta.required_db_vendor` from
8139 /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8140 /// Normalized to the dialect name `manage check --deploy`
8141 /// compares against `Settings.database.backend`. Aliases
8142 /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8143 /// stored under the canonical name.
8144 required_db_vendor: Option<String>,
8145 /// Django-shape `Meta.default_related_name` from
8146 /// `#[rustango(default_related_name = "...")]`. Threaded into
8147 /// `ModelSchema::default_related_name`. Reverse-relation accessor
8148 /// name to use when an FK / M2M field doesn't override it.
8149 /// Today rustango doesn't auto-emit reverse managers; the
8150 /// metadata is the foundation for that work.
8151 default_related_name: Option<String>,
8152 /// Django-shape `Meta.db_table_comment` (4.2+) from
8153 /// `#[rustango(db_table_comment = "...")]`. Threaded into
8154 /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8155 /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8156 /// inline `COMMENT='...'`, SQLite: no-op).
8157 db_table_comment: Option<String>,
8158 /// Django-shape `Meta.get_latest_by` from
8159 /// `#[rustango(get_latest_by = "created_at")]` /
8160 /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8161 /// `(column, descending)` where `descending = true` when the
8162 /// attribute value starts with `-`. Threaded into
8163 /// `ModelSchema::get_latest_by`.
8164 get_latest_by: Option<(String, bool)>,
8165 /// Django-shape `Meta.permissions = [(codename, name), ...]`
8166 /// from `#[rustango(extra_permissions = "approve:Can approve,
8167 /// archive:Can archive")]`. Comma-separated `codename:label`
8168 /// pairs. Threaded into `ModelSchema::extra_permissions`.
8169 extra_permissions: Vec<(String, String)>,
8170 /// Django-shape `Meta.default_permissions` — which CRUD codenames
8171 /// (`"add"` / `"change"` / `"delete"` / `"view"`) the framework
8172 /// auto-creates. Empty `Vec` (default) means **all four** — matches
8173 /// Django's behavior when the operator omits the option. Set via
8174 /// `#[rustango(default_permissions = "view,change")]` to opt out.
8175 /// Validated at parse time; unknown actions fail with a span-pointing
8176 /// error.
8177 default_permissions: Vec<String>,
8178 /// `#[rustango(verbose_name = "blog post")]` — Django-shape
8179 /// human-readable singular label for the model. Threaded into
8180 /// `ModelSchema::verbose_name` so admin section headers /
8181 /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8182 /// over the Rust struct identifier.
8183 verbose_name: Option<String>,
8184 /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8185 /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8186 /// When unset, `display_label_plural()` auto-suffixes `s`.
8187 verbose_name_plural: Option<String>,
8188 /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8189 /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8190 /// a name (used by `QuerySet::without_global_scope`) with a
8191 /// `fn() -> WhereExpr` path that the macro emits into
8192 /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8193 global_scopes: Vec<GlobalScopeAttr>,
8194 /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8195 /// `#[rustango(through(name, far, far_fk_column, intermediate,
8196 /// intermediate_fk_column, intermediate_pk_column))]` — issue
8197 /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8198 /// entry emits an inherent `<name>_through(&self)` accessor that
8199 /// returns a `QuerySet<Far>` filtered via a correlated subquery
8200 /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8201 /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8202 through_relations: Vec<ThroughAttr>,
8203 /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8204 /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8205 /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8206 /// Each entry emits two associated functions on the parent —
8207 /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8208 /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8209 /// over a correlated subquery against the child table. Users
8210 /// drop the result into `QuerySet::where_raw(...)`.
8211 reverse_has_relations: Vec<ReverseHasAttr>,
8212 /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8213 /// issue #830. Each emits a `Model::generic_reverse_relations()`
8214 /// entry so the relation-existence family resolves polymorphic
8215 /// children by name.
8216 generic_has_relations: Vec<GenericHasAttr>,
8217}
8218
8219/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8220/// declaration. Each entry becomes one `core::GlobalScope` in the
8221/// emitted schema literal; `apply` resolves at macro-expand time
8222/// against the consumer's scope so the function must be in scope at
8223/// the use site. Issue #820.
8224struct GlobalScopeAttr {
8225 name: String,
8226 apply: syn::Path,
8227}
8228
8229/// Parsed `#[rustango(through(...))]` declaration. Issue
8230/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8231/// `hasManyThrough` / `hasOneThrough` parity.
8232///
8233/// `Country hasManyThrough Post via User` declares as:
8234///
8235/// ```ignore
8236/// #[rustango(through(
8237/// name = "posts",
8238/// far = "Post",
8239/// far_fk_column = "author_id",
8240/// intermediate = "User",
8241/// intermediate_fk_column = "country_id",
8242/// ))]
8243/// struct Country { ... }
8244/// ```
8245///
8246/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8247/// which returns a queryset filtered via a correlated subquery —
8248/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8249/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8250/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8251/// because the subquery lives inside a `WhereExpr::InSubquery` node
8252/// and the outer queryset's pending list stays empty.
8253///
8254/// All four required arguments use **SQL column / table names**
8255/// (not Rust field names) to sidestep the multi-hop-filter substrate
8256/// gap. Once that substrate lands, a higher-level Rust-field-name
8257/// shorthand can be added without breaking this surface.
8258struct ThroughAttr {
8259 /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8260 name: String,
8261 /// Far model type identifier. `far = "Post"` → returns
8262 /// `QuerySet<Post>`. Resolved verbatim against the scope where
8263 /// the derive expands.
8264 far: syn::Ident,
8265 /// SQL column on the far model's table that references the
8266 /// intermediate model's primary key. For `Post`'s
8267 /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8268 /// default `<field>_id` convention) or whatever the user
8269 /// declared via `#[rustango(db_column = "...")]`.
8270 far_fk_column: String,
8271 /// Intermediate model type identifier. Needed to look up its
8272 /// `SCHEMA` so the subquery's `FROM` clause points at the
8273 /// intermediate table. `intermediate = "User"`.
8274 intermediate: syn::Ident,
8275 /// SQL column on the intermediate's table that references the
8276 /// source (this) model's primary key. For `User`'s
8277 /// `country: ForeignKey<Country>` the column is `"country_id"`.
8278 intermediate_fk_column: String,
8279 /// SQL primary-key column on the intermediate's table — the
8280 /// column the subquery projects. Optional; defaults to `"id"`
8281 /// (rustango's default PK column name). Override when the
8282 /// intermediate declares a custom PK column.
8283 intermediate_pk_column: String,
8284}
8285
8286/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8287/// child_fk_column = "..."))]` declaration. Issue
8288/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8289/// `whereHas` / `whereDoesntHave` parity.
8290///
8291/// `Post hasMany Comment` declares as:
8292///
8293/// ```ignore
8294/// #[rustango(reverse_has(
8295/// name = "comments",
8296/// child = "Comment",
8297/// child_fk_column = "post_id",
8298/// ))]
8299/// struct Post { ... }
8300/// ```
8301///
8302/// The macro emits two associated functions on `Post`:
8303///
8304/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8305/// `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8306/// <outer>.<self_pk_column>)`.
8307/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8308/// but `NOT EXISTS`, the `whereDoesntHave` analog.
8309///
8310/// User code:
8311///
8312/// ```ignore
8313/// // Posts with at least one comment:
8314/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8315/// // Posts with no comments:
8316/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8317/// ```
8318///
8319/// As with #817, all column / table identifiers are **SQL names**
8320/// (not Rust field names) so the substrate is independent of the
8321/// outstanding multi-hop filter resolver gap. The emitted
8322/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8323/// SQL-emit time via the writer's scope stack.
8324struct ReverseHasAttr {
8325 /// Accessor name. `name = "comments"` → emits
8326 /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8327 name: String,
8328 /// Child model type identifier. `child = "Comment"` — needed to
8329 /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8330 /// points at the child table.
8331 child: syn::Ident,
8332 /// SQL column on the child's table that references this model's
8333 /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8334 /// column is `"post_id"`.
8335 child_fk_column: String,
8336 /// SQL primary-key column on **this** model's table — the column
8337 /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8338 /// (rustango's default PK column name).
8339 self_pk_column: String,
8340}
8341
8342/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8343/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8344/// relation-existence family (issue #830). The child is a polymorphic,
8345/// content-type-discriminated model; emits a `Model::
8346/// generic_reverse_relations()` entry the queryset resolves by name.
8347struct GenericHasAttr {
8348 /// Accessor name, e.g. `name = "tags"`.
8349 name: String,
8350 /// Child model type identifier (`child = "Tag"`) — looked up for its
8351 /// `SCHEMA` so the subquery's `FROM` points at the child table.
8352 child: syn::Ident,
8353 /// Column on the child table holding the parent's content-type id.
8354 /// Optional; defaults to `"content_type_id"`.
8355 ct_column: String,
8356 /// Column on the child table holding the parent's PK value.
8357 /// Optional; defaults to `"object_pk"`.
8358 pk_column: String,
8359 /// SQL primary-key column on **this** (parent) model's table.
8360 /// Optional; defaults to `"id"`.
8361 self_pk_column: String,
8362}
8363
8364/// Parsed form of one index declaration (field-level or container-level).
8365struct IndexAttr {
8366 /// Index name; auto-derived when `None` at parse time.
8367 name: Option<String>,
8368 /// Column names in the index.
8369 columns: Vec<String>,
8370 /// `true` for `CREATE UNIQUE INDEX`.
8371 unique: bool,
8372 /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8373 /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8374 /// `"btree"` when the attribute is absent — the DDL writer omits
8375 /// the `USING` clause and the backend uses its own default
8376 /// (btree on every supported dialect).
8377 method: String,
8378 /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8379 /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8380 /// condition = "...", name = "..."))]`. `None` for plain indexes.
8381 where_clause: Option<String>,
8382 /// Django `Index(fields=..., include=[...])` covering-index
8383 /// columns (PG 11+ `INCLUDE (...)` clause). Empty `Vec` (the
8384 /// default) means "no covering columns".
8385 include: Vec<String>,
8386}
8387
8388/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8389struct CheckAttr {
8390 name: String,
8391 expr: String,
8392}
8393
8394/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8395/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8396/// PG-only — surfaced on every backend in the macro emit; the migration
8397/// writer skips the op on MySQL/SQLite. Issue #319.
8398struct ExcludeAttr {
8399 /// Constraint name (free-form Rust identifier).
8400 name: String,
8401 /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8402 using: String,
8403 /// Comma-separated `(column, operator)` pairs, in declaration
8404 /// order. Parsed from `"col WITH op, col WITH op"`.
8405 elements: Vec<(String, String)>,
8406 /// Optional `WHERE` predicate (raw SQL).
8407 where_clause: Option<String>,
8408}
8409
8410/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8411/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8412/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8413/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8414/// the model, not the field.
8415struct CompositeFkAttr {
8416 /// Logical relation name (free-form Rust identifier).
8417 name: String,
8418 /// SQL table name of the target.
8419 to: String,
8420 /// Source-side column names, in declaration order.
8421 from: Vec<String>,
8422 /// Target-side column names, same length / order as `from`.
8423 on: Vec<String>,
8424}
8425
8426/// Parsed form of one `#[rustango(generic_fk(name = "target",
8427/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8428/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8429/// generic ("any model") FKs live on the model, not the field.
8430struct GenericFkAttr {
8431 /// Logical relation name (free-form Rust identifier).
8432 name: String,
8433 /// Source-side column carrying the `content_type_id` value.
8434 ct_column: String,
8435 /// Source-side column carrying the target row's primary key.
8436 pk_column: String,
8437}
8438
8439/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8440struct M2MAttr {
8441 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8442 name: String,
8443 /// Target table (e.g. `"app_tags"`).
8444 to: String,
8445 /// Junction table (e.g. `"post_tags"`).
8446 through: String,
8447 /// Source FK column in the junction table (e.g. `"post_id"`).
8448 src: String,
8449 /// Destination FK column in the junction table (e.g. `"tag_id"`).
8450 dst: String,
8451 /// Whether the migration writer should auto-create the junction
8452 /// table. Default `true`. Set `auto_create = false` (#324) when
8453 /// the operator declares the through table as its own
8454 /// `#[derive(Model)]` struct with extra columns.
8455 auto_create: bool,
8456}
8457
8458/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8459/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8460/// junction carries a ContentType discriminator so unrelated models
8461/// share one pivot + related set.
8462struct GenericM2MAttr {
8463 /// Accessor suffix: `tags` → generates `tags_m2m()`.
8464 name: String,
8465 /// Polymorphic junction table (e.g. `"taggables"`).
8466 through: String,
8467 /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8468 pk_column: String,
8469 /// Junction column holding the owning model's `content_type_id`
8470 /// discriminator (e.g. `"taggable_type"`).
8471 ct_column: String,
8472 /// Junction column holding the related model PK (e.g. `"tag_id"`).
8473 related_column: String,
8474}
8475
8476/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8477/// "user"))]`. `track` is a comma-separated list of field names whose
8478/// before/after values land in the JSONB `changes` column. `source`
8479/// is informational only — it pins a default source when the model
8480/// is written outside any `audit::with_source(...)` scope (rare).
8481#[derive(Default)]
8482struct AuditAttrs {
8483 /// Field names to capture in the `changes` JSONB. Validated
8484 /// against declared scalar fields at compile time. Empty means
8485 /// "track every scalar field" — Django's audit-everything default.
8486 track: Option<(Vec<String>, proc_macro2::Span)>,
8487}
8488
8489/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8490/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8491/// comma-separated strings; we validate each ident against the model's
8492/// declared fields at compile time.
8493#[derive(Default)]
8494struct AdminAttrs {
8495 list_display: Option<(Vec<String>, proc_macro2::Span)>,
8496 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8497 list_per_page: Option<usize>,
8498 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8499 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8500 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8501 /// Bulk action names. No field-validation against model fields —
8502 /// these are action handlers, not column references.
8503 actions: Option<(Vec<String>, proc_macro2::Span)>,
8504 /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8505 /// sections, comma-separated fields per section, optional
8506 /// `Title:` prefix. Empty title omits the `<legend>`.
8507 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8508 /// `admin(list_display_links = "title")` — Django-shape. Names
8509 /// from `list_display` whose cells should link to detail/edit.
8510 /// Issue #350.
8511 list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8512 /// `admin(search_help_text = "...")` — Django-shape. Short
8513 /// caption rendered beside the admin list view's search box.
8514 /// Issue #353.
8515 search_help_text: Option<String>,
8516 /// `admin(actions_on_top = false)` — Django-shape. Hides the
8517 /// action-bar above the table. Default `true`. Issue #354.
8518 actions_on_top: Option<bool>,
8519 /// `admin(actions_on_bottom = true)` — Django-shape. Renders an
8520 /// additional action-bar below the table. Default `false`.
8521 /// Issue #354.
8522 actions_on_bottom: Option<bool>,
8523 /// `admin(date_hierarchy = "created_at")` — Django-shape. Name of
8524 /// a date / datetime field whose values render as a clickable
8525 /// year / month / day drill-down strip above the list table.
8526 /// Empty / unset disables the strip. Issue #355.
8527 date_hierarchy: Option<String>,
8528 /// `admin(prepopulated_fields = "slug:title")` — Django-shape.
8529 /// Each entry is `target:source[+source2]`; multiple entries are
8530 /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8531 /// The admin change-form emits JS that slugifies the source values
8532 /// into the target field on every keystroke. Issue #356.
8533 prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8534 /// `admin(raw_id_fields = "parent, owner")` — Django-shape. Names
8535 /// of FK fields whose change-form widget renders a lookup link
8536 /// next to the input. Issue #357.
8537 raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8538 /// `admin(autocomplete_fields = "author_id")` — Django-shape.
8539 /// Names of FK fields whose change-form widget renders an
8540 /// Ajax-driven typeahead populated from a `__autocomplete`
8541 /// endpoint on the target model. Issue #358.
8542 autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8543 /// `admin(list_select_related = "all" | "none" | "author, …")`
8544 /// — Django-shape. Tunes the admin list view's FK auto-JOIN
8545 /// policy. Default `"all"` matches rustango's join-everything
8546 /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8547 list_select_related: Option<String>,
8548 /// `admin(formfield_overrides = "field:widget, field2:widget2")` —
8549 /// Django-shape. Each entry is `field_name:widget_name`; multiple
8550 /// entries comma-separated. Empty / unset → no overrides. The
8551 /// list of widget names supported is documented on
8552 /// `AdminConfig::formfield_overrides`. Issue #359.
8553 formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8554}
8555
8556fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8557 let mut out = ContainerAttrs {
8558 table: None,
8559 display: None,
8560 app: None,
8561 admin: None,
8562 audit: None,
8563 // Default `permissions = true` so every `#[derive(Model)]`
8564 // gets the four CRUD codenames seeded by `auto_create_permissions`
8565 // and is visible to non-superusers in the tenant admin without
8566 // manual per-model annotation. Models that intentionally don't
8567 // want permission rows (registry-internal types, framework
8568 // tables operators shouldn't manage directly) opt out via
8569 // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8570 // out-of-the-box admin invisibility regression (#62).
8571 permissions: true,
8572 m2m: Vec::new(),
8573 generic_m2m: Vec::new(),
8574 indexes: Vec::new(),
8575 checks: Vec::new(),
8576 excludes: Vec::new(),
8577 composite_fks: Vec::new(),
8578 generic_fks: Vec::new(),
8579 scope: None,
8580 manager_ext: None,
8581 manager_fns: Vec::new(),
8582 default_order: Vec::new(),
8583 is_view: false,
8584 managed: true,
8585 verbose_name: None,
8586 verbose_name_plural: None,
8587 base_manager_name: None,
8588 order_with_respect_to: None,
8589 proxy: false,
8590 required_db_features: Vec::new(),
8591 required_db_vendor: None,
8592 default_related_name: None,
8593 db_table_comment: None,
8594 get_latest_by: None,
8595 extra_permissions: Vec::new(),
8596 default_permissions: Vec::new(),
8597 global_scopes: Vec::new(),
8598 through_relations: Vec::new(),
8599 reverse_has_relations: Vec::new(),
8600 generic_has_relations: Vec::new(),
8601 };
8602 for attr in &input.attrs {
8603 if !attr.path().is_ident("rustango") {
8604 continue;
8605 }
8606 attr.parse_nested_meta(|meta| {
8607 if meta.path.is_ident("table") {
8608 let s: LitStr = meta.value()?.parse()?;
8609 let name = s.value();
8610 // v0.27.3 (#65) — macro-time guard against table names
8611 // that compile but break SQL downstream. Hyphens are
8612 // the common footgun: PostgreSQL accepts a quoted
8613 // `"intermediate-region"` in CREATE TABLE, but the
8614 // FK / index name derivation in `migrate::ddl`
8615 // emits `intermediate-region_field_fkey` unquoted,
8616 // which then fails the SQL parser. Same shape rule
8617 // as Postgres regular identifiers so the safe path
8618 // is the only path.
8619 validate_table_name(&name, s.span())?;
8620 out.table = Some(name);
8621 return Ok(());
8622 }
8623 if meta.path.is_ident("display") {
8624 let s: LitStr = meta.value()?.parse()?;
8625 out.display = Some((s.value(), s.span()));
8626 return Ok(());
8627 }
8628 if meta.path.is_ident("app") {
8629 let s: LitStr = meta.value()?.parse()?;
8630 out.app = Some(s.value());
8631 return Ok(());
8632 }
8633 if meta.path.is_ident("scope") {
8634 let s: LitStr = meta.value()?.parse()?;
8635 let val = s.value();
8636 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8637 return Err(meta.error(format!(
8638 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8639 )));
8640 }
8641 out.scope = Some(val);
8642 return Ok(());
8643 }
8644 if meta.path.is_ident("admin") {
8645 let mut admin = AdminAttrs::default();
8646 meta.parse_nested_meta(|inner| {
8647 if inner.path.is_ident("list_display") {
8648 let s: LitStr = inner.value()?.parse()?;
8649 admin.list_display =
8650 Some((split_field_list(&s.value()), s.span()));
8651 return Ok(());
8652 }
8653 if inner.path.is_ident("search_fields") {
8654 let s: LitStr = inner.value()?.parse()?;
8655 admin.search_fields =
8656 Some((split_field_list(&s.value()), s.span()));
8657 return Ok(());
8658 }
8659 if inner.path.is_ident("readonly_fields") {
8660 let s: LitStr = inner.value()?.parse()?;
8661 admin.readonly_fields =
8662 Some((split_field_list(&s.value()), s.span()));
8663 return Ok(());
8664 }
8665 if inner.path.is_ident("list_per_page") {
8666 let lit: syn::LitInt = inner.value()?.parse()?;
8667 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8668 return Ok(());
8669 }
8670 if inner.path.is_ident("ordering") {
8671 let s: LitStr = inner.value()?.parse()?;
8672 admin.ordering = Some((
8673 parse_ordering_list(&s.value()),
8674 s.span(),
8675 ));
8676 return Ok(());
8677 }
8678 if inner.path.is_ident("list_filter") {
8679 let s: LitStr = inner.value()?.parse()?;
8680 admin.list_filter =
8681 Some((split_field_list(&s.value()), s.span()));
8682 return Ok(());
8683 }
8684 if inner.path.is_ident("actions") {
8685 let s: LitStr = inner.value()?.parse()?;
8686 admin.actions =
8687 Some((split_field_list(&s.value()), s.span()));
8688 return Ok(());
8689 }
8690 if inner.path.is_ident("fieldsets") {
8691 let s: LitStr = inner.value()?.parse()?;
8692 admin.fieldsets =
8693 Some((parse_fieldset_list(&s.value()), s.span()));
8694 return Ok(());
8695 }
8696 if inner.path.is_ident("list_display_links") {
8697 let s: LitStr = inner.value()?.parse()?;
8698 admin.list_display_links =
8699 Some((split_field_list(&s.value()), s.span()));
8700 return Ok(());
8701 }
8702 if inner.path.is_ident("search_help_text") {
8703 let s: LitStr = inner.value()?.parse()?;
8704 admin.search_help_text = Some(s.value());
8705 return Ok(());
8706 }
8707 if inner.path.is_ident("actions_on_top") {
8708 let lit: syn::LitBool = inner.value()?.parse()?;
8709 admin.actions_on_top = Some(lit.value);
8710 return Ok(());
8711 }
8712 if inner.path.is_ident("actions_on_bottom") {
8713 let lit: syn::LitBool = inner.value()?.parse()?;
8714 admin.actions_on_bottom = Some(lit.value);
8715 return Ok(());
8716 }
8717 if inner.path.is_ident("date_hierarchy") {
8718 let s: LitStr = inner.value()?.parse()?;
8719 admin.date_hierarchy = Some(s.value());
8720 return Ok(());
8721 }
8722 if inner.path.is_ident("prepopulated_fields") {
8723 let s: LitStr = inner.value()?.parse()?;
8724 admin.prepopulated_fields =
8725 Some((parse_prepopulated_list(&s.value()), s.span()));
8726 return Ok(());
8727 }
8728 if inner.path.is_ident("raw_id_fields") {
8729 let s: LitStr = inner.value()?.parse()?;
8730 admin.raw_id_fields =
8731 Some((split_field_list(&s.value()), s.span()));
8732 return Ok(());
8733 }
8734 if inner.path.is_ident("autocomplete_fields") {
8735 let s: LitStr = inner.value()?.parse()?;
8736 admin.autocomplete_fields =
8737 Some((split_field_list(&s.value()), s.span()));
8738 return Ok(());
8739 }
8740 if inner.path.is_ident("list_select_related") {
8741 let s: LitStr = inner.value()?.parse()?;
8742 admin.list_select_related = Some(s.value());
8743 return Ok(());
8744 }
8745 if inner.path.is_ident("formfield_overrides") {
8746 let s: LitStr = inner.value()?.parse()?;
8747 admin.formfield_overrides =
8748 Some((parse_formfield_overrides(&s.value()), s.span()));
8749 return Ok(());
8750 }
8751 Err(inner.error(
8752 "unknown admin attribute (supported: \
8753 `list_display`, `list_display_links`, \
8754 `search_fields`, `search_help_text`, \
8755 `readonly_fields`, \
8756 `list_filter`, `list_per_page`, `ordering`, `actions`, \
8757 `actions_on_top`, `actions_on_bottom`, \
8758 `date_hierarchy`, \
8759 `prepopulated_fields`, \
8760 `raw_id_fields`, \
8761 `autocomplete_fields`, \
8762 `list_select_related`, \
8763 `formfield_overrides`, \
8764 `fieldsets`)",
8765 ))
8766 })?;
8767 out.admin = Some(admin);
8768 return Ok(());
8769 }
8770 if meta.path.is_ident("manager") {
8771 // `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8772 // Stretch `from_queryset = "..."` (Django Manager.from_queryset
8773 // shape) is left as a follow-up — the issue's primary
8774 // acceptance is the `ext = ...` trait emission.
8775 meta.parse_nested_meta(|inner| {
8776 if inner.path.is_ident("ext") {
8777 let s: LitStr = inner.value()?.parse()?;
8778 let name = s.value();
8779 if name.is_empty() {
8780 return Err(inner.error("manager(ext = \"...\") cannot be empty"));
8781 }
8782 out.manager_ext =
8783 Some(syn::Ident::new(&name, s.span()));
8784 return Ok(());
8785 }
8786 Err(inner.error(
8787 "unknown manager attribute (supported: `ext = \"TraitName\"`)",
8788 ))
8789 })?;
8790 return Ok(());
8791 }
8792 if meta.path.is_ident("manager_fn") {
8793 // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8794 // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8795 // next to the default `Self::objects()`. Multiple
8796 // attributes accumulate.
8797 let s: LitStr = meta.value()?.parse()?;
8798 let name = s.value();
8799 if name.is_empty() {
8800 return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8801 }
8802 if name == "objects" {
8803 return Err(meta.error(
8804 "`manager_fn = \"objects\"` collides with the default \
8805 accessor — pick a different name",
8806 ));
8807 }
8808 let ident = syn::Ident::new(&name, s.span());
8809 if out.manager_fns.iter().any(|prev| *prev == ident) {
8810 return Err(meta.error(format!(
8811 "duplicate `manager_fn = \"{name}\"`"
8812 )));
8813 }
8814 out.manager_fns.push(ident);
8815 return Ok(());
8816 }
8817 if meta.path.is_ident("default_order") {
8818 // `#[rustango(default_order = "-created_at, status")]`
8819 // — issue #291 / T2.5. Comma-separated list; `-prefix`
8820 // means descending, `+prefix` or bare name means ascending.
8821 // Per-query opt-in via `QuerySet::with_default_order()`.
8822 let s: LitStr = meta.value()?.parse()?;
8823 let raw = s.value();
8824 let span = s.span();
8825 let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
8826 Vec::new();
8827 for entry in raw.split(',') {
8828 let trimmed = entry.trim();
8829 if trimmed.is_empty() {
8830 return Err(syn::Error::new(
8831 span,
8832 "`default_order = \"...\"` has an empty entry — \
8833 check for a stray comma",
8834 ));
8835 }
8836 let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
8837 (true, rest.trim().to_owned())
8838 } else if let Some(rest) = trimmed.strip_prefix('+') {
8839 (false, rest.trim().to_owned())
8840 } else {
8841 (false, trimmed.to_owned())
8842 };
8843 if name.is_empty() {
8844 return Err(syn::Error::new(
8845 span,
8846 "`default_order` entry has no column name after the prefix",
8847 ));
8848 }
8849 if parsed.iter().any(|(n, _, _)| *n == name) {
8850 return Err(syn::Error::new(
8851 span,
8852 format!("duplicate column `{name}` in `default_order`"),
8853 ));
8854 }
8855 parsed.push((name, desc, span));
8856 }
8857 if parsed.is_empty() {
8858 return Err(syn::Error::new(
8859 span,
8860 "`default_order = \"...\"` cannot be empty",
8861 ));
8862 }
8863 out.default_order = parsed;
8864 return Ok(());
8865 }
8866 if meta.path.is_ident("global_scope") {
8867 // `#[rustango(global_scope(name = "active", apply =
8868 // path::to::fn))]` — issue #820. The apply function
8869 // path resolves at macro-expand time in the consumer's
8870 // scope; the macro re-emits it verbatim into the
8871 // `ModelSchema::global_scopes` slice literal.
8872 let span = meta.path.span();
8873 let mut scope_name: Option<String> = None;
8874 let mut apply_path: Option<syn::Path> = None;
8875 meta.parse_nested_meta(|inner| {
8876 if inner.path.is_ident("name") {
8877 let s: LitStr = inner.value()?.parse()?;
8878 let raw = s.value();
8879 if raw.trim().is_empty() {
8880 return Err(syn::Error::new(
8881 s.span(),
8882 "`global_scope(name = \"...\")` must not be empty",
8883 ));
8884 }
8885 scope_name = Some(raw);
8886 return Ok(());
8887 }
8888 if inner.path.is_ident("apply") {
8889 let p: syn::Path = inner.value()?.parse()?;
8890 apply_path = Some(p);
8891 return Ok(());
8892 }
8893 Err(inner.error(
8894 "unknown `global_scope` attribute (supported: \
8895 `name`, `apply`)",
8896 ))
8897 })?;
8898 let Some(name) = scope_name else {
8899 return Err(syn::Error::new(
8900 span,
8901 "`global_scope` requires `name = \"...\"`",
8902 ));
8903 };
8904 let Some(apply) = apply_path else {
8905 return Err(syn::Error::new(
8906 span,
8907 "`global_scope` requires `apply = fn_path`",
8908 ));
8909 };
8910 if out.global_scopes.iter().any(|s| s.name == name) {
8911 return Err(syn::Error::new(
8912 span,
8913 format!(
8914 "duplicate global scope name `{name}` — \
8915 pick a unique identifier so \
8916 `QuerySet::without_global_scope(\"{name}\")` \
8917 is unambiguous"
8918 ),
8919 ));
8920 }
8921 out.global_scopes.push(GlobalScopeAttr { name, apply });
8922 return Ok(());
8923 }
8924 if meta.path.is_ident("through") {
8925 // `#[rustango(through(name = "posts", far = "Post",
8926 // far_fk_column = "author_id", intermediate = "User",
8927 // intermediate_fk_column = "country_id"
8928 // [, intermediate_pk_column = "id"]))]` — issue #817.
8929 let span = meta.path.span();
8930 let mut accessor_name: Option<String> = None;
8931 let mut far_ident: Option<syn::Ident> = None;
8932 let mut far_fk_column: Option<String> = None;
8933 let mut intermediate_ident: Option<syn::Ident> = None;
8934 let mut intermediate_fk_column: Option<String> = None;
8935 let mut intermediate_pk_column: Option<String> = None;
8936 fn parse_nonempty_string(
8937 inner: &syn::meta::ParseNestedMeta<'_>,
8938 field: &str,
8939 ) -> syn::Result<String> {
8940 let s: LitStr = inner.value()?.parse()?;
8941 let raw = s.value();
8942 let trimmed = raw.trim();
8943 if trimmed.is_empty() {
8944 return Err(syn::Error::new(
8945 s.span(),
8946 format!("`through({field} = \"...\")` must not be empty"),
8947 ));
8948 }
8949 Ok(trimmed.to_owned())
8950 }
8951 meta.parse_nested_meta(|inner| {
8952 if inner.path.is_ident("name") {
8953 accessor_name = Some(parse_nonempty_string(&inner, "name")?);
8954 return Ok(());
8955 }
8956 if inner.path.is_ident("far") {
8957 // Far model name accepted as a string literal
8958 // so the attribute fits inside the existing
8959 // `parse_nested_meta` shape; parsed back into a
8960 // `syn::Ident` so the emitted accessor can
8961 // reference the type directly.
8962 let s: LitStr = inner.value()?.parse()?;
8963 let raw = s.value();
8964 let trimmed = raw.trim();
8965 if trimmed.is_empty() {
8966 return Err(syn::Error::new(
8967 s.span(),
8968 "`through(far = \"...\")` must not be empty",
8969 ));
8970 }
8971 far_ident = Some(syn::Ident::new(trimmed, s.span()));
8972 return Ok(());
8973 }
8974 if inner.path.is_ident("far_fk_column") {
8975 far_fk_column =
8976 Some(parse_nonempty_string(&inner, "far_fk_column")?);
8977 return Ok(());
8978 }
8979 if inner.path.is_ident("intermediate") {
8980 let s: LitStr = inner.value()?.parse()?;
8981 let raw = s.value();
8982 let trimmed = raw.trim();
8983 if trimmed.is_empty() {
8984 return Err(syn::Error::new(
8985 s.span(),
8986 "`through(intermediate = \"...\")` must not be empty",
8987 ));
8988 }
8989 intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
8990 return Ok(());
8991 }
8992 if inner.path.is_ident("intermediate_fk_column") {
8993 intermediate_fk_column =
8994 Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
8995 return Ok(());
8996 }
8997 if inner.path.is_ident("intermediate_pk_column") {
8998 intermediate_pk_column =
8999 Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9000 return Ok(());
9001 }
9002 Err(inner.error(
9003 "unknown `through` attribute (supported: \
9004 `name`, `far`, `far_fk_column`, \
9005 `intermediate`, `intermediate_fk_column`, \
9006 `intermediate_pk_column`)",
9007 ))
9008 })?;
9009 let Some(name) = accessor_name else {
9010 return Err(syn::Error::new(
9011 span,
9012 "`through` requires `name = \"...\"`",
9013 ));
9014 };
9015 let Some(far) = far_ident else {
9016 return Err(syn::Error::new(
9017 span,
9018 "`through` requires `far = \"FarModelType\"`",
9019 ));
9020 };
9021 let Some(far_fk_column) = far_fk_column else {
9022 return Err(syn::Error::new(
9023 span,
9024 "`through` requires `far_fk_column = \"<column>\"`",
9025 ));
9026 };
9027 let Some(intermediate) = intermediate_ident else {
9028 return Err(syn::Error::new(
9029 span,
9030 "`through` requires `intermediate = \"IntermediateModelType\"`",
9031 ));
9032 };
9033 let Some(intermediate_fk_column) = intermediate_fk_column else {
9034 return Err(syn::Error::new(
9035 span,
9036 "`through` requires `intermediate_fk_column = \"<column>\"`",
9037 ));
9038 };
9039 let intermediate_pk_column =
9040 intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9041 if out.through_relations.iter().any(|t| t.name == name) {
9042 return Err(syn::Error::new(
9043 span,
9044 format!(
9045 "duplicate `through(name = \"{name}\")` — \
9046 pick a unique accessor name"
9047 ),
9048 ));
9049 }
9050 out.through_relations.push(ThroughAttr {
9051 name,
9052 far,
9053 far_fk_column,
9054 intermediate,
9055 intermediate_fk_column,
9056 intermediate_pk_column,
9057 });
9058 return Ok(());
9059 }
9060 if meta.path.is_ident("reverse_has") {
9061 // `#[rustango(reverse_has(name = "comments",
9062 // child = "Comment", child_fk_column = "post_id"
9063 // [, self_pk_column = "id"]))]` — issue #830.
9064 let span = meta.path.span();
9065 let mut accessor_name: Option<String> = None;
9066 let mut child_ident: Option<syn::Ident> = None;
9067 let mut child_fk_column: Option<String> = None;
9068 let mut self_pk_column: Option<String> = None;
9069 meta.parse_nested_meta(|inner| {
9070 if inner.path.is_ident("name") {
9071 let s: LitStr = inner.value()?.parse()?;
9072 let raw = s.value();
9073 if raw.trim().is_empty() {
9074 return Err(syn::Error::new(
9075 s.span(),
9076 "`reverse_has(name = \"...\")` must not be empty",
9077 ));
9078 }
9079 accessor_name = Some(raw);
9080 return Ok(());
9081 }
9082 if inner.path.is_ident("child") {
9083 let s: LitStr = inner.value()?.parse()?;
9084 let raw = s.value();
9085 let trimmed = raw.trim();
9086 if trimmed.is_empty() {
9087 return Err(syn::Error::new(
9088 s.span(),
9089 "`reverse_has(child = \"...\")` must not be empty",
9090 ));
9091 }
9092 child_ident = Some(syn::Ident::new(trimmed, s.span()));
9093 return Ok(());
9094 }
9095 if inner.path.is_ident("child_fk_column") {
9096 let s: LitStr = inner.value()?.parse()?;
9097 let raw = s.value();
9098 let trimmed = raw.trim();
9099 if trimmed.is_empty() {
9100 return Err(syn::Error::new(
9101 s.span(),
9102 "`reverse_has(child_fk_column = \"...\")` must not be empty",
9103 ));
9104 }
9105 child_fk_column = Some(trimmed.to_owned());
9106 return Ok(());
9107 }
9108 if inner.path.is_ident("self_pk_column") {
9109 let s: LitStr = inner.value()?.parse()?;
9110 let raw = s.value();
9111 let trimmed = raw.trim();
9112 if trimmed.is_empty() {
9113 return Err(syn::Error::new(
9114 s.span(),
9115 "`reverse_has(self_pk_column = \"...\")` must not be empty",
9116 ));
9117 }
9118 self_pk_column = Some(trimmed.to_owned());
9119 return Ok(());
9120 }
9121 Err(inner.error(
9122 "unknown `reverse_has` attribute (supported: \
9123 `name`, `child`, `child_fk_column`, \
9124 `self_pk_column`)",
9125 ))
9126 })?;
9127 let Some(name) = accessor_name else {
9128 return Err(syn::Error::new(
9129 span,
9130 "`reverse_has` requires `name = \"...\"`",
9131 ));
9132 };
9133 let Some(child) = child_ident else {
9134 return Err(syn::Error::new(
9135 span,
9136 "`reverse_has` requires `child = \"ChildModelType\"`",
9137 ));
9138 };
9139 let Some(child_fk_column) = child_fk_column else {
9140 return Err(syn::Error::new(
9141 span,
9142 "`reverse_has` requires `child_fk_column = \"<column>\"`",
9143 ));
9144 };
9145 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9146 if out.reverse_has_relations.iter().any(|r| r.name == name) {
9147 return Err(syn::Error::new(
9148 span,
9149 format!(
9150 "duplicate `reverse_has(name = \"{name}\")` — \
9151 pick a unique accessor name"
9152 ),
9153 ));
9154 }
9155 out.reverse_has_relations.push(ReverseHasAttr {
9156 name,
9157 child,
9158 child_fk_column,
9159 self_pk_column,
9160 });
9161 return Ok(());
9162 }
9163 if meta.path.is_ident("generic_has") {
9164 // `#[rustango(generic_has(name = "tags",
9165 // child = "Tag", ct_column = "content_type_id",
9166 // pk_column = "object_pk" [, self_pk_column = "id"]))]`
9167 // — issue #830, the reverse generic-FK (GFK) arm of the
9168 // relation-existence family.
9169 let span = meta.path.span();
9170 let mut accessor_name: Option<String> = None;
9171 let mut child_ident: Option<syn::Ident> = None;
9172 let mut ct_column: Option<String> = None;
9173 let mut pk_column: Option<String> = None;
9174 let mut self_pk_column: Option<String> = None;
9175 meta.parse_nested_meta(|inner| {
9176 if inner.path.is_ident("name") {
9177 let s: LitStr = inner.value()?.parse()?;
9178 let raw = s.value();
9179 if raw.trim().is_empty() {
9180 return Err(syn::Error::new(
9181 s.span(),
9182 "`generic_has(name = \"...\")` must not be empty",
9183 ));
9184 }
9185 accessor_name = Some(raw);
9186 return Ok(());
9187 }
9188 if inner.path.is_ident("child") {
9189 let s: LitStr = inner.value()?.parse()?;
9190 let trimmed = s.value().trim().to_owned();
9191 if trimmed.is_empty() {
9192 return Err(syn::Error::new(
9193 s.span(),
9194 "`generic_has(child = \"...\")` must not be empty",
9195 ));
9196 }
9197 child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9198 return Ok(());
9199 }
9200 if inner.path.is_ident("ct_column") {
9201 let s: LitStr = inner.value()?.parse()?;
9202 let trimmed = s.value().trim().to_owned();
9203 if trimmed.is_empty() {
9204 return Err(syn::Error::new(
9205 s.span(),
9206 "`generic_has(ct_column = \"...\")` must not be empty",
9207 ));
9208 }
9209 ct_column = Some(trimmed);
9210 return Ok(());
9211 }
9212 if inner.path.is_ident("pk_column") {
9213 let s: LitStr = inner.value()?.parse()?;
9214 let trimmed = s.value().trim().to_owned();
9215 if trimmed.is_empty() {
9216 return Err(syn::Error::new(
9217 s.span(),
9218 "`generic_has(pk_column = \"...\")` must not be empty",
9219 ));
9220 }
9221 pk_column = Some(trimmed);
9222 return Ok(());
9223 }
9224 if inner.path.is_ident("self_pk_column") {
9225 let s: LitStr = inner.value()?.parse()?;
9226 let trimmed = s.value().trim().to_owned();
9227 if trimmed.is_empty() {
9228 return Err(syn::Error::new(
9229 s.span(),
9230 "`generic_has(self_pk_column = \"...\")` must not be empty",
9231 ));
9232 }
9233 self_pk_column = Some(trimmed);
9234 return Ok(());
9235 }
9236 Err(inner.error(
9237 "unknown `generic_has` attribute (supported: \
9238 `name`, `child`, `ct_column`, `pk_column`, \
9239 `self_pk_column`)",
9240 ))
9241 })?;
9242 let Some(name) = accessor_name else {
9243 return Err(syn::Error::new(
9244 span,
9245 "`generic_has` requires `name = \"...\"`",
9246 ));
9247 };
9248 let Some(child) = child_ident else {
9249 return Err(syn::Error::new(
9250 span,
9251 "`generic_has` requires `child = \"ChildModelType\"`",
9252 ));
9253 };
9254 let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9255 let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9256 let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9257 if out.generic_has_relations.iter().any(|r| r.name == name) {
9258 return Err(syn::Error::new(
9259 span,
9260 format!(
9261 "duplicate `generic_has(name = \"{name}\")` — \
9262 pick a unique accessor name"
9263 ),
9264 ));
9265 }
9266 out.generic_has_relations.push(GenericHasAttr {
9267 name,
9268 child,
9269 ct_column,
9270 pk_column,
9271 self_pk_column,
9272 });
9273 return Ok(());
9274 }
9275 if meta.path.is_ident("audit") {
9276 let mut audit = AuditAttrs::default();
9277 meta.parse_nested_meta(|inner| {
9278 if inner.path.is_ident("track") {
9279 let s: LitStr = inner.value()?.parse()?;
9280 audit.track =
9281 Some((split_field_list(&s.value()), s.span()));
9282 return Ok(());
9283 }
9284 Err(inner.error(
9285 "unknown audit attribute (supported: `track`)",
9286 ))
9287 })?;
9288 out.audit = Some(audit);
9289 return Ok(());
9290 }
9291 if meta.path.is_ident("permissions") {
9292 // Two forms accepted:
9293 // #[rustango(permissions)] — flag form, true
9294 // #[rustango(permissions = false)] — explicit opt-out
9295 // #[rustango(permissions = true)] — explicit opt-in
9296 if let Ok(v) = meta.value() {
9297 let lit: syn::LitBool = v.parse()?;
9298 out.permissions = lit.value;
9299 } else {
9300 out.permissions = true;
9301 }
9302 return Ok(());
9303 }
9304 if meta.path.is_ident("view") {
9305 // Issue #293 / T2.10. Two forms accepted, matching
9306 // the `permissions` flag pattern:
9307 // #[rustango(view)] — flag form, true
9308 // #[rustango(view = false)] — explicit opt-out
9309 // #[rustango(view = true)] — explicit opt-in
9310 if let Ok(v) = meta.value() {
9311 let lit: syn::LitBool = v.parse()?;
9312 out.is_view = lit.value;
9313 } else {
9314 out.is_view = true;
9315 }
9316 return Ok(());
9317 }
9318 if meta.path.is_ident("managed") {
9319 // Django-shape Meta.managed. Issue #321.
9320 // #[rustango(managed = false)] — operator-managed table
9321 // #[rustango(managed = true)] — rustango-managed (the default)
9322 // Bare-flag form is intentionally not accepted: writing
9323 // `#[rustango(managed)]` reads as "yes please manage it"
9324 // which is already the default. The opt-out is the only
9325 // useful state, so it must be explicit.
9326 let v = meta.value()?;
9327 let lit: syn::LitBool = v.parse()?;
9328 out.managed = lit.value;
9329 return Ok(());
9330 }
9331 if meta.path.is_ident("verbose_name") {
9332 let s: LitStr = meta.value()?.parse()?;
9333 out.verbose_name = Some(s.value());
9334 return Ok(());
9335 }
9336 if meta.path.is_ident("verbose_name_plural") {
9337 let s: LitStr = meta.value()?.parse()?;
9338 out.verbose_name_plural = Some(s.value());
9339 return Ok(());
9340 }
9341 if meta.path.is_ident("db_table_comment") {
9342 // Django-shape `Meta.db_table_comment` (4.2+) — free-form
9343 // table-level comment attached to the DB catalog.
9344 let s: LitStr = meta.value()?.parse()?;
9345 out.db_table_comment = Some(s.value());
9346 return Ok(());
9347 }
9348 if meta.path.is_ident("proxy") {
9349 // Django-shape `Meta.proxy = True` — declarative flag
9350 // marking the struct as a proxy of another model that
9351 // shares its DB table. Stored on `ModelSchema::proxy`
9352 // so future codegen can skip `CreateTable` emission
9353 // for proxies (parent owns the table) and route
9354 // per-instance method resolution to the proxy class.
9355 //
9356 // Accepts `proxy` (bare → true) and `proxy = true/false`.
9357 let value = if meta.input.peek(syn::Token![=]) {
9358 meta.value()?.parse::<syn::LitBool>()?.value
9359 } else {
9360 true
9361 };
9362 out.proxy = value;
9363 return Ok(());
9364 }
9365 if meta.path.is_ident("order_with_respect_to") {
9366 // Django-shape `Meta.order_with_respect_to = "parent_fk"` —
9367 // the model's instances are intrinsically ordered
9368 // relative to their parent FK. Django auto-generates
9369 // a `_order` integer column + admin reordering UI.
9370 //
9371 // rustango stores the FK field name on
9372 // `ModelSchema::order_with_respect_to`. Declarative-only
9373 // today: the migration writer + admin surfaces still
9374 // treat every model identically. Future codegen will
9375 // key off the metadata to auto-emit the `_order`
9376 // column and reorder helpers.
9377 //
9378 // Validated as a Rust-shape identifier so the macro
9379 // can reject typos at derive time.
9380 let s: LitStr = meta.value()?.parse()?;
9381 let raw = s.value();
9382 if raw.is_empty() {
9383 return Err(syn::Error::new(
9384 s.span(),
9385 "`order_with_respect_to` must be a non-empty FK field name",
9386 ));
9387 }
9388 let valid = raw
9389 .chars()
9390 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9391 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9392 if !valid {
9393 return Err(syn::Error::new(
9394 s.span(),
9395 format!(
9396 "`order_with_respect_to` must be a valid Rust \
9397 identifier (letters / digits / underscores, \
9398 not starting with a digit); got `{raw}`"
9399 ),
9400 ));
9401 }
9402 out.order_with_respect_to = Some(raw);
9403 return Ok(());
9404 }
9405 if meta.path.is_ident("required_db_features") {
9406 // Django-shape `Meta.required_db_features` — capability
9407 // tokens the model needs (e.g. `"json_extract"`,
9408 // `"window_functions"`, `"row_security"`). Comma-separated.
9409 // `manage check --deploy` walks every model and warns
9410 // when the active backend doesn't advertise the
9411 // capability.
9412 //
9413 // rustango ships a small registry of capability tokens
9414 // each dialect supports — see `Dialect::supports`.
9415 // Unknown tokens still parse (they end up on the
9416 // schema and show up in the warning) so projects can
9417 // declare aspirational capabilities and the check
9418 // verb will keep nagging until the dialect implements
9419 // them.
9420 let s: LitStr = meta.value()?.parse()?;
9421 let raw = s.value();
9422 let features: Vec<String> = raw
9423 .split(',')
9424 .map(str::trim)
9425 .filter(|s| !s.is_empty())
9426 .map(str::to_owned)
9427 .collect();
9428 if features.is_empty() {
9429 return Err(syn::Error::new(
9430 s.span(),
9431 "`required_db_features` must list at least one \
9432 comma-separated capability token",
9433 ));
9434 }
9435 out.required_db_features = features;
9436 return Ok(());
9437 }
9438 if meta.path.is_ident("required_db_vendor") {
9439 // Django-shape `Meta.required_db_vendor` — the model
9440 // is only meant to run against the named DB backend.
9441 // `manage check --deploy` flags a mismatch so
9442 // ops catches "I forgot to switch DATABASE_URL" at
9443 // deploy time rather than runtime.
9444 //
9445 // Django spells it as a free-form string; rustango
9446 // restricts to the three backends it ships dialects
9447 // for so the check verb can compare reliably.
9448 let s: LitStr = meta.value()?.parse()?;
9449 let raw = s.value().to_ascii_lowercase();
9450 match raw.as_str() {
9451 "postgresql" | "postgres" | "pg" => {
9452 out.required_db_vendor = Some("postgres".to_owned());
9453 }
9454 "mysql" | "mariadb" => {
9455 out.required_db_vendor = Some("mysql".to_owned());
9456 }
9457 "sqlite" | "sqlite3" => {
9458 out.required_db_vendor = Some("sqlite".to_owned());
9459 }
9460 _ => {
9461 return Err(syn::Error::new(
9462 s.span(),
9463 format!(
9464 "unknown required_db_vendor `{raw}` — \
9465 expected `postgres` (aliases: `postgresql`, `pg`), \
9466 `mysql` (alias: `mariadb`), or `sqlite` \
9467 (alias: `sqlite3`)"
9468 ),
9469 ));
9470 }
9471 }
9472 return Ok(());
9473 }
9474 if meta.path.is_ident("base_manager_name") {
9475 // Django-shape `Meta.base_manager_name` — name of the
9476 // Manager subclass that `<instance>.<relation>_set`
9477 // uses when resolving reverse-relation managers.
9478 // Distinct from `default_manager_name` (what
9479 // `Model.objects` returns at the class level).
9480 // Stored on `ModelSchema::base_manager_name`.
9481 //
9482 // Validated as a Rust identifier so it stays safe to
9483 // re-emit as code in future reverse-manager codegen.
9484 let s: LitStr = meta.value()?.parse()?;
9485 let raw = s.value();
9486 if raw.is_empty() {
9487 return Err(syn::Error::new(
9488 s.span(),
9489 "`base_manager_name` must be a non-empty string",
9490 ));
9491 }
9492 let valid = raw
9493 .chars()
9494 .all(|c| c == '_' || c.is_ascii_alphanumeric())
9495 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9496 if !valid {
9497 return Err(syn::Error::new(
9498 s.span(),
9499 format!(
9500 "`base_manager_name` must be a valid Rust \
9501 identifier (letters / digits / underscores, \
9502 not starting with a digit); got `{raw}`"
9503 ),
9504 ));
9505 }
9506 out.base_manager_name = Some(raw);
9507 return Ok(());
9508 }
9509 if meta.path.is_ident("default_related_name") {
9510 // Django-shape `Meta.default_related_name` — the name
9511 // reverse-relation accessors use when callers don't
9512 // override `related_name=...` on the FK / M2M field.
9513 // Stored on `ModelSchema::default_related_name` so
9514 // future reverse-manager codegen / DRF schema emit /
9515 // admin templates can pick the right accessor name
9516 // (today rustango doesn't auto-emit reverse managers;
9517 // the metadata is the foundation for that work).
9518 //
9519 // Django requires snake_case + no `+` suffix; we
9520 // enforce non-empty + ASCII identifier-shape so the
9521 // string is safe to use as a Rust ident later.
9522 let s: LitStr = meta.value()?.parse()?;
9523 let raw = s.value();
9524 if raw.is_empty() {
9525 return Err(syn::Error::new(
9526 s.span(),
9527 "`default_related_name` must be a non-empty string",
9528 ));
9529 }
9530 let valid = raw
9531 .chars()
9532 .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9533 && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9534 if !valid {
9535 return Err(syn::Error::new(
9536 s.span(),
9537 format!(
9538 "`default_related_name` must be snake_case ASCII \
9539 (lowercase letters / digits / underscores, not \
9540 starting with a digit); got `{raw}`"
9541 ),
9542 ));
9543 }
9544 out.default_related_name = Some(raw);
9545 return Ok(());
9546 }
9547 if meta.path.is_ident("extra_permissions") {
9548 // Django-shape `Meta.permissions = [(codename, name), ...]`.
9549 // Comma-separated `codename:label` pairs.
9550 let s: LitStr = meta.value()?.parse()?;
9551 let raw = s.value();
9552 let mut pairs = Vec::new();
9553 for entry in raw.split(',') {
9554 let entry = entry.trim();
9555 if entry.is_empty() {
9556 continue;
9557 }
9558 let (codename, label) = match entry.split_once(':') {
9559 Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9560 None => (entry.to_owned(), entry.to_owned()),
9561 };
9562 if codename.is_empty() {
9563 return Err(meta.error(
9564 "`extra_permissions` entries must be `codename:label` pairs",
9565 ));
9566 }
9567 pairs.push((codename, label));
9568 }
9569 if pairs.is_empty() {
9570 return Err(meta
9571 .error("`extra_permissions = \"…\"` must list at least one pair"));
9572 }
9573 out.extra_permissions = pairs;
9574 return Ok(());
9575 }
9576 if meta.path.is_ident("default_permissions") {
9577 // Django-shape `Meta.default_permissions = ('view',
9578 // 'change')`. Comma-separated subset of the CRUD
9579 // action set. Empty means all four (the framework
9580 // default — matches Django when the option is
9581 // omitted).
9582 let s: LitStr = meta.value()?.parse()?;
9583 let raw = s.value();
9584 let mut actions: Vec<String> = Vec::new();
9585 for entry in raw.split(',') {
9586 let action = entry.trim().to_ascii_lowercase();
9587 if action.is_empty() {
9588 continue;
9589 }
9590 match action.as_str() {
9591 "add" | "change" | "delete" | "view" => {}
9592 other => {
9593 return Err(syn::Error::new(
9594 s.span(),
9595 format!(
9596 "unknown default_permissions action `{other}` — \
9597 expected one of `add`, `change`, `delete`, `view`"
9598 ),
9599 ));
9600 }
9601 }
9602 if !actions.contains(&action) {
9603 actions.push(action);
9604 }
9605 }
9606 if actions.is_empty() {
9607 return Err(syn::Error::new(
9608 s.span(),
9609 "`default_permissions = \"…\"` must list at least one action; \
9610 use `permissions = false` on the container if you want NO \
9611 permissions seeded for this model.",
9612 ));
9613 }
9614 out.default_permissions = actions;
9615 return Ok(());
9616 }
9617 if meta.path.is_ident("get_latest_by") {
9618 // Django-shape `Meta.get_latest_by`. The `-` prefix
9619 // selects descending order (Django muscle memory).
9620 let s: LitStr = meta.value()?.parse()?;
9621 let raw = s.value();
9622 let trimmed = raw.trim();
9623 if trimmed.is_empty() {
9624 return Err(meta.error("`get_latest_by` must name a column"));
9625 }
9626 let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9627 (stripped.to_owned(), true)
9628 } else if let Some(stripped) = trimmed.strip_prefix('+') {
9629 (stripped.to_owned(), false)
9630 } else {
9631 (trimmed.to_owned(), false)
9632 };
9633 if col.is_empty() {
9634 return Err(meta.error("`get_latest_by` must name a column"));
9635 }
9636 out.get_latest_by = Some((col, desc));
9637 return Ok(());
9638 }
9639 if meta.path.is_ident("unique_together") {
9640 // Django-shape composite UNIQUE index. Two syntaxes:
9641 //
9642 // #[rustango(unique_together = "org_id, user_id")] — auto-derived name
9643 // #[rustango(unique_together(columns = "org_id, user_id", name = "x"))] — explicit name
9644 //
9645 // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9646 // (col1, col2)`, where <name> defaults to
9647 // `<table>_<col1>_<col2>_uq` when not supplied.
9648 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9649 out.indexes.push(IndexAttr {
9650 name,
9651 columns,
9652 unique: true,
9653 method: "btree".to_owned(),
9654 where_clause: None,
9655 include: Vec::new(),
9656 });
9657 return Ok(());
9658 }
9659 if meta.path.is_ident("index_together") {
9660 // Django-shape composite (non-unique) index. Two syntaxes
9661 // mirroring `unique_together`.
9662 //
9663 // #[rustango(index_together = "created_at, status")]
9664 // #[rustango(index_together(columns = "created_at, status", name = "x"))]
9665 let (columns, name) = parse_together_attr(&meta, "index_together")?;
9666 out.indexes.push(IndexAttr {
9667 name,
9668 columns,
9669 unique: false,
9670 method: "btree".to_owned(),
9671 where_clause: None,
9672 include: Vec::new(),
9673 });
9674 return Ok(());
9675 }
9676 if meta.path.is_ident("unique_when") {
9677 // Django 4.0+ `UniqueConstraint(condition=Q(...))` —
9678 // partial unique index. Issue #265 / T1.3.
9679 //
9680 // #[rustango(unique_when(
9681 // columns = "email",
9682 // condition = "deleted_at IS NULL",
9683 // name = "unique_active_email"
9684 // ))]
9685 //
9686 // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9687 // on PG / SQLite (both ship partial indexes natively).
9688 // MySQL falls back to a plain UNIQUE index — the
9689 // condition is lost; document the limitation in the
9690 // generated migration.
9691 let mut columns: Option<Vec<String>> = None;
9692 let mut condition: Option<String> = None;
9693 let mut name: Option<String> = None;
9694 let mut include: Vec<String> = Vec::new();
9695 meta.parse_nested_meta(|inner| {
9696 if inner.path.is_ident("columns") {
9697 let s: LitStr = inner.value()?.parse()?;
9698 columns = Some(split_field_list(&s.value()));
9699 return Ok(());
9700 }
9701 if inner.path.is_ident("condition") {
9702 let s: LitStr = inner.value()?.parse()?;
9703 condition = Some(s.value());
9704 return Ok(());
9705 }
9706 if inner.path.is_ident("name") {
9707 let s: LitStr = inner.value()?.parse()?;
9708 name = Some(s.value());
9709 return Ok(());
9710 }
9711 if inner.path.is_ident("include") {
9712 // Django `UniqueConstraint(include=[...])` — PG
9713 // 11+ covering-index columns. Non-key columns
9714 // travel with the index leaf for index-only
9715 // scans. Dropped on MySQL/SQLite by the writer.
9716 let s: LitStr = inner.value()?.parse()?;
9717 include = split_field_list(&s.value());
9718 return Ok(());
9719 }
9720 Err(inner.error(
9721 "unknown unique_when attribute (supported: \
9722 `columns = \"...\"`, `condition = \"...\"`, \
9723 `name = \"...\"`, `include = \"...\"`)",
9724 ))
9725 })?;
9726 let columns = columns.ok_or_else(|| {
9727 meta.error("`unique_when(...)` requires `columns = \"...\"`")
9728 })?;
9729 let condition = condition.ok_or_else(|| {
9730 meta.error("`unique_when(...)` requires `condition = \"...\"`")
9731 })?;
9732 if columns.is_empty() {
9733 return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9734 }
9735 out.indexes.push(IndexAttr {
9736 name,
9737 columns,
9738 unique: true,
9739 method: "btree".to_owned(),
9740 where_clause: Some(condition),
9741 include,
9742 });
9743 return Ok(());
9744 }
9745 if meta.path.is_ident("index_when") {
9746 // Django `Index(fields=..., condition=Q(...))` parity —
9747 // non-unique partial index. Sibling of `unique_when`
9748 // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9749 //
9750 // #[rustango(index_when(
9751 // columns = "status, created_at",
9752 // condition = "deleted_at IS NULL",
9753 // name = "active_status_created_idx"
9754 // ))]
9755 //
9756 // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9757 // on PG / SQLite (both ship partial indexes natively).
9758 // MySQL has no native partial-index support — the writer
9759 // emits a plain CREATE INDEX and the condition is lost;
9760 // operators wanting that selectivity on MySQL should
9761 // declare a covering index plus an application-level
9762 // filter.
9763 let mut columns: Option<Vec<String>> = None;
9764 let mut condition: Option<String> = None;
9765 let mut name: Option<String> = None;
9766 let mut method: String = "btree".to_owned();
9767 let mut include: Vec<String> = Vec::new();
9768 meta.parse_nested_meta(|inner| {
9769 if inner.path.is_ident("columns") {
9770 let s: LitStr = inner.value()?.parse()?;
9771 columns = Some(split_field_list(&s.value()));
9772 return Ok(());
9773 }
9774 if inner.path.is_ident("condition") {
9775 let s: LitStr = inner.value()?.parse()?;
9776 condition = Some(s.value());
9777 return Ok(());
9778 }
9779 if inner.path.is_ident("name") {
9780 let s: LitStr = inner.value()?.parse()?;
9781 name = Some(s.value());
9782 return Ok(());
9783 }
9784 if inner.path.is_ident("method") {
9785 let s: LitStr = inner.value()?.parse()?;
9786 method = s.value();
9787 return Ok(());
9788 }
9789 if inner.path.is_ident("include") {
9790 // Django `Index(include=[...])` — PG 11+
9791 // covering-index columns; non-key columns
9792 // travel with the index leaf. Dropped on
9793 // MySQL/SQLite.
9794 let s: LitStr = inner.value()?.parse()?;
9795 include = split_field_list(&s.value());
9796 return Ok(());
9797 }
9798 Err(inner.error(
9799 "unknown index_when attribute (supported: \
9800 `columns = \"...\"`, `condition = \"...\"`, \
9801 `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9802 `include = \"...\"`)",
9803 ))
9804 })?;
9805 let columns = columns
9806 .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9807 let condition = condition.ok_or_else(|| {
9808 meta.error("`index_when(...)` requires `condition = \"...\"`")
9809 })?;
9810 if columns.is_empty() {
9811 return Err(meta.error("`index_when(columns = \"\")` is empty"));
9812 }
9813 out.indexes.push(IndexAttr {
9814 name,
9815 columns,
9816 unique: false,
9817 method,
9818 where_clause: Some(condition),
9819 include,
9820 });
9821 return Ok(());
9822 }
9823 if meta.path.is_ident("index") {
9824 // Container-level composite index. Two syntaxes:
9825 // #[rustango(index = "col1, col2")] — bare, non-unique btree
9826 // #[rustango(index("col1, col2"))] — call form (same result)
9827 // #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
9828 // The call form takes the column list as a leading string
9829 // literal, then optional `unique` / `name = "..."` /
9830 // `method = "..."` flags (a leading literal can't compose
9831 // under `parse_nested_meta`, so the paren body is parsed by
9832 // hand). `unique_together` / `index_together` remain the
9833 // Django-shape aliases for the same feature.
9834 let cols_lit: LitStr;
9835 let mut unique = false;
9836 let mut name: Option<String> = None;
9837 let mut method = "btree".to_owned();
9838 if meta.input.peek(syn::token::Paren) {
9839 let content;
9840 syn::parenthesized!(content in meta.input);
9841 cols_lit = content.parse()?;
9842 while content.peek(syn::Token![,]) {
9843 content.parse::<syn::Token![,]>()?;
9844 if content.is_empty() {
9845 break;
9846 }
9847 let flag: syn::Ident = content.parse()?;
9848 if flag == "unique" {
9849 unique = true;
9850 } else if flag == "name" {
9851 content.parse::<syn::Token![=]>()?;
9852 let s: LitStr = content.parse()?;
9853 name = Some(s.value());
9854 } else if flag == "method" {
9855 content.parse::<syn::Token![=]>()?;
9856 let s: LitStr = content.parse()?;
9857 let v = s.value();
9858 match v.as_str() {
9859 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
9860 | "bloom" => method = v,
9861 other => {
9862 return Err(syn::Error::new(
9863 s.span(),
9864 format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
9865 ));
9866 }
9867 }
9868 } else {
9869 return Err(syn::Error::new(
9870 flag.span(),
9871 "unknown index flag (supported: `unique`, `name`, `method`)",
9872 ));
9873 }
9874 }
9875 } else {
9876 cols_lit = meta.value()?.parse()?;
9877 }
9878 let columns = split_field_list(&cols_lit.value());
9879 out.indexes.push(IndexAttr {
9880 name,
9881 columns,
9882 unique,
9883 method,
9884 where_clause: None,
9885 include: Vec::new(),
9886 });
9887 return Ok(());
9888 }
9889 if meta.path.is_ident("check") {
9890 // #[rustango(check(name = "…", expr = "…"))]
9891 let mut name: Option<String> = None;
9892 let mut expr: Option<String> = None;
9893 meta.parse_nested_meta(|inner| {
9894 if inner.path.is_ident("name") {
9895 let s: LitStr = inner.value()?.parse()?;
9896 name = Some(s.value());
9897 return Ok(());
9898 }
9899 if inner.path.is_ident("expr") {
9900 let s: LitStr = inner.value()?.parse()?;
9901 expr = Some(s.value());
9902 return Ok(());
9903 }
9904 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
9905 })?;
9906 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
9907 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
9908 out.checks.push(CheckAttr { name, expr });
9909 return Ok(());
9910 }
9911 if meta.path.is_ident("exclude") {
9912 // #[rustango(exclude(name = "…", using = "gist",
9913 // elements = "col WITH op, col WITH op",
9914 // where = "…"))]
9915 let mut name: Option<String> = None;
9916 let mut using: Option<String> = None;
9917 let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
9918 let mut where_clause: Option<String> = None;
9919 meta.parse_nested_meta(|inner| {
9920 if inner.path.is_ident("name") {
9921 let s: LitStr = inner.value()?.parse()?;
9922 name = Some(s.value());
9923 return Ok(());
9924 }
9925 if inner.path.is_ident("using") {
9926 let s: LitStr = inner.value()?.parse()?;
9927 using = Some(s.value());
9928 return Ok(());
9929 }
9930 if inner.path.is_ident("elements") {
9931 let s: LitStr = inner.value()?.parse()?;
9932 elements_raw = Some((s.value(), s.span()));
9933 return Ok(());
9934 }
9935 if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
9936 let s: LitStr = inner.value()?.parse()?;
9937 where_clause = Some(s.value());
9938 return Ok(());
9939 }
9940 Err(inner.error(
9941 "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
9942 ))
9943 })?;
9944 let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
9945 let using = using.unwrap_or_else(|| "gist".to_owned());
9946 let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
9947 meta.error(
9948 "exclude requires `elements = \"col WITH op, col WITH op\"`",
9949 )
9950 })?;
9951 // Parse `col WITH op` pairs separated by commas.
9952 let mut elements: Vec<(String, String)> = Vec::new();
9953 for pair in elements_str.split(',') {
9954 let pair = pair.trim();
9955 if pair.is_empty() {
9956 continue;
9957 }
9958 let mut split = pair.splitn(2, |c: char| c.is_whitespace());
9959 let col = split.next().unwrap_or("").trim();
9960 let rest = split.next().unwrap_or("").trim();
9961 // `WITH op` — case-insensitive on `WITH`, then op.
9962 let rest_lc = rest.to_ascii_lowercase();
9963 let op = rest_lc
9964 .strip_prefix("with")
9965 .map(|r| r.trim_start())
9966 .filter(|r| !r.is_empty())
9967 .map(|_| {
9968 // Pull the original-case op from `rest` after the
9969 // `WITH ` token (5 chars).
9970 rest[4..].trim_start().to_owned()
9971 });
9972 let Some(op) = op else {
9973 return Err(syn::Error::new(
9974 elements_span,
9975 format!(
9976 "exclude elements: `{pair}` must be `<col> WITH <op>` \
9977 (e.g. `room_id WITH =` or `during WITH &&`)"
9978 ),
9979 ));
9980 };
9981 if col.is_empty() || op.is_empty() {
9982 return Err(syn::Error::new(
9983 elements_span,
9984 format!(
9985 "exclude elements: `{pair}` must be `<col> WITH <op>` \
9986 (both sides non-empty)"
9987 ),
9988 ));
9989 }
9990 elements.push((col.to_owned(), op));
9991 }
9992 if elements.is_empty() {
9993 return Err(syn::Error::new(
9994 elements_span,
9995 "exclude requires at least one `col WITH op` element",
9996 ));
9997 }
9998 out.excludes.push(ExcludeAttr {
9999 name,
10000 using,
10001 elements,
10002 where_clause,
10003 });
10004 return Ok(());
10005 }
10006 if meta.path.is_ident("generic_fk") {
10007 let mut gfk = GenericFkAttr {
10008 name: String::new(),
10009 ct_column: String::new(),
10010 pk_column: String::new(),
10011 };
10012 meta.parse_nested_meta(|inner| {
10013 if inner.path.is_ident("name") {
10014 let s: LitStr = inner.value()?.parse()?;
10015 gfk.name = s.value();
10016 return Ok(());
10017 }
10018 if inner.path.is_ident("ct_column") {
10019 let s: LitStr = inner.value()?.parse()?;
10020 gfk.ct_column = s.value();
10021 return Ok(());
10022 }
10023 if inner.path.is_ident("pk_column") {
10024 let s: LitStr = inner.value()?.parse()?;
10025 gfk.pk_column = s.value();
10026 return Ok(());
10027 }
10028 Err(inner.error(
10029 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10030 ))
10031 })?;
10032 if gfk.name.is_empty() {
10033 return Err(meta.error("generic_fk requires `name = \"...\"`"));
10034 }
10035 if gfk.ct_column.is_empty() {
10036 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10037 }
10038 if gfk.pk_column.is_empty() {
10039 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10040 }
10041 out.generic_fks.push(gfk);
10042 return Ok(());
10043 }
10044 if meta.path.is_ident("fk_composite") {
10045 let mut fk = CompositeFkAttr {
10046 name: String::new(),
10047 to: String::new(),
10048 from: Vec::new(),
10049 on: Vec::new(),
10050 };
10051 meta.parse_nested_meta(|inner| {
10052 if inner.path.is_ident("name") {
10053 let s: LitStr = inner.value()?.parse()?;
10054 fk.name = s.value();
10055 return Ok(());
10056 }
10057 if inner.path.is_ident("to") {
10058 let s: LitStr = inner.value()?.parse()?;
10059 fk.to = s.value();
10060 return Ok(());
10061 }
10062 // `on = ("col1", "col2", ...)` — parse a parenthesised
10063 // comma-list of string literals.
10064 if inner.path.is_ident("on") || inner.path.is_ident("from") {
10065 let value = inner.value()?;
10066 let content;
10067 syn::parenthesized!(content in value);
10068 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10069 content.parse_terminated(
10070 |p| p.parse::<syn::LitStr>(),
10071 syn::Token![,],
10072 )?;
10073 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10074 if inner.path.is_ident("on") {
10075 fk.on = cols;
10076 } else {
10077 fk.from = cols;
10078 }
10079 return Ok(());
10080 }
10081 Err(inner.error(
10082 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10083 ))
10084 })?;
10085 if fk.name.is_empty() {
10086 return Err(meta.error("fk_composite requires `name = \"...\"`"));
10087 }
10088 if fk.to.is_empty() {
10089 return Err(meta.error("fk_composite requires `to = \"...\"`"));
10090 }
10091 if fk.from.is_empty() || fk.on.is_empty() {
10092 return Err(meta.error(
10093 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10094 ));
10095 }
10096 if fk.from.len() != fk.on.len() {
10097 return Err(meta.error(format!(
10098 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10099 fk.from.len(),
10100 fk.on.len(),
10101 )));
10102 }
10103 out.composite_fks.push(fk);
10104 return Ok(());
10105 }
10106 if meta.path.is_ident("m2m") {
10107 let mut m2m = M2MAttr {
10108 name: String::new(),
10109 to: String::new(),
10110 through: String::new(),
10111 src: String::new(),
10112 dst: String::new(),
10113 auto_create: true,
10114 };
10115 meta.parse_nested_meta(|inner| {
10116 if inner.path.is_ident("name") {
10117 let s: LitStr = inner.value()?.parse()?;
10118 m2m.name = s.value();
10119 return Ok(());
10120 }
10121 if inner.path.is_ident("to") {
10122 let s: LitStr = inner.value()?.parse()?;
10123 m2m.to = s.value();
10124 return Ok(());
10125 }
10126 if inner.path.is_ident("through") {
10127 let s: LitStr = inner.value()?.parse()?;
10128 m2m.through = s.value();
10129 return Ok(());
10130 }
10131 if inner.path.is_ident("src") {
10132 let s: LitStr = inner.value()?.parse()?;
10133 m2m.src = s.value();
10134 return Ok(());
10135 }
10136 if inner.path.is_ident("dst") {
10137 let s: LitStr = inner.value()?.parse()?;
10138 m2m.dst = s.value();
10139 return Ok(());
10140 }
10141 if inner.path.is_ident("auto_create") {
10142 let lit: syn::LitBool = inner.value()?.parse()?;
10143 m2m.auto_create = lit.value;
10144 return Ok(());
10145 }
10146 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10147 })?;
10148 if m2m.name.is_empty() {
10149 return Err(meta.error("m2m requires `name = \"...\"`"));
10150 }
10151 if m2m.to.is_empty() {
10152 return Err(meta.error("m2m requires `to = \"...\"`"));
10153 }
10154 if m2m.through.is_empty() {
10155 return Err(meta.error("m2m requires `through = \"...\"`"));
10156 }
10157 if m2m.src.is_empty() {
10158 return Err(meta.error("m2m requires `src = \"...\"`"));
10159 }
10160 if m2m.dst.is_empty() {
10161 return Err(meta.error("m2m requires `dst = \"...\"`"));
10162 }
10163 out.m2m.push(m2m);
10164 return Ok(());
10165 }
10166 if meta.path.is_ident("generic_m2m") {
10167 let mut gm = GenericM2MAttr {
10168 name: String::new(),
10169 through: String::new(),
10170 pk_column: String::new(),
10171 ct_column: String::new(),
10172 related_column: String::new(),
10173 };
10174 meta.parse_nested_meta(|inner| {
10175 let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10176 let s: LitStr = inner.value()?.parse()?;
10177 Ok(s.value())
10178 };
10179 if inner.path.is_ident("name") {
10180 gm.name = field(&inner)?;
10181 return Ok(());
10182 }
10183 if inner.path.is_ident("through") {
10184 gm.through = field(&inner)?;
10185 return Ok(());
10186 }
10187 if inner.path.is_ident("pk_column") {
10188 gm.pk_column = field(&inner)?;
10189 return Ok(());
10190 }
10191 if inner.path.is_ident("ct_column") {
10192 gm.ct_column = field(&inner)?;
10193 return Ok(());
10194 }
10195 if inner.path.is_ident("related_column") {
10196 gm.related_column = field(&inner)?;
10197 return Ok(());
10198 }
10199 Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10200 })?;
10201 for (val, label) in [
10202 (&gm.name, "name"),
10203 (&gm.through, "through"),
10204 (&gm.pk_column, "pk_column"),
10205 (&gm.ct_column, "ct_column"),
10206 (&gm.related_column, "related_column"),
10207 ] {
10208 if val.is_empty() {
10209 return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10210 }
10211 }
10212 out.generic_m2m.push(gm);
10213 return Ok(());
10214 }
10215 Err(meta.error("unknown rustango container attribute"))
10216 })?;
10217 }
10218 Ok(out)
10219}
10220
10221/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10222/// owned field names, trimming whitespace and skipping empty entries.
10223/// Field-name validation against the model is done by the caller.
10224fn split_field_list(raw: &str) -> Vec<String> {
10225 raw.split(',')
10226 .map(str::trim)
10227 .filter(|s| !s.is_empty())
10228 .map(str::to_owned)
10229 .collect()
10230}
10231
10232/// Shared parser for `unique_together` and `index_together` container
10233/// attrs. Accepts both shapes:
10234///
10235/// * `attr = "col1, col2"` — auto-derived index name.
10236/// * `attr(columns = "col1, col2", name = "...")` — explicit name.
10237///
10238/// Returns `(columns, name)`.
10239fn parse_together_attr(
10240 meta: &syn::meta::ParseNestedMeta<'_>,
10241 attr: &str,
10242) -> syn::Result<(Vec<String>, Option<String>)> {
10243 // Disambiguate by whether the next token is `=` (key-value) or
10244 // `(` (parenthesized).
10245 if meta.input.peek(syn::Token![=]) {
10246 let cols_lit: LitStr = meta.value()?.parse()?;
10247 let columns = split_field_list(&cols_lit.value());
10248 check_together_columns(meta, attr, &columns)?;
10249 return Ok((columns, None));
10250 }
10251 let mut columns: Option<Vec<String>> = None;
10252 let mut name: Option<String> = None;
10253 meta.parse_nested_meta(|inner| {
10254 if inner.path.is_ident("columns") {
10255 let s: LitStr = inner.value()?.parse()?;
10256 columns = Some(split_field_list(&s.value()));
10257 return Ok(());
10258 }
10259 if inner.path.is_ident("name") {
10260 let s: LitStr = inner.value()?.parse()?;
10261 name = Some(s.value());
10262 return Ok(());
10263 }
10264 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10265 })?;
10266 let columns = columns.ok_or_else(|| {
10267 meta.error(format!(
10268 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10269 ))
10270 })?;
10271 check_together_columns(meta, attr, &columns)?;
10272 Ok((columns, name))
10273}
10274
10275fn check_together_columns(
10276 meta: &syn::meta::ParseNestedMeta<'_>,
10277 attr: &str,
10278 columns: &[String],
10279) -> syn::Result<()> {
10280 if columns.len() < 2 {
10281 let single = if attr == "unique_together" {
10282 "#[rustango(unique)] on the field"
10283 } else {
10284 "#[rustango(index)] on the field"
10285 };
10286 return Err(meta.error(format!(
10287 "{attr} expects two or more columns; for a single-column equivalent use {single}",
10288 )));
10289 }
10290 Ok(())
10291}
10292
10293/// Parse the fieldsets DSL: pipe-separated sections, optional
10294/// `"Title:"` prefix on each, comma-separated field names after.
10295/// Examples:
10296/// * `"name, office"` → one untitled section with two fields
10297/// * `"Identity: name, office | Metadata: created_at"` → two titled
10298/// sections
10299///
10300/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10301fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10302 raw.split('|')
10303 .map(str::trim)
10304 .filter(|s| !s.is_empty())
10305 .map(|section| {
10306 // Split off an optional `Title:` prefix (first colon).
10307 let (title, rest) = match section.split_once(':') {
10308 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10309 _ => (String::new(), section),
10310 };
10311 let fields = split_field_list(rest);
10312 (title, fields)
10313 })
10314 .collect()
10315}
10316
10317/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10318/// comma-separated entry maps a target field to one or more source
10319/// fields joined with `+`. Whitespace around tokens is trimmed.
10320/// Entries missing `:` or with empty target/source lists are dropped.
10321fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10322 raw.split(',')
10323 .map(str::trim)
10324 .filter(|s| !s.is_empty())
10325 .filter_map(|entry| {
10326 let (target, sources_raw) = entry.split_once(':')?;
10327 let target = target.trim().to_owned();
10328 if target.is_empty() {
10329 return None;
10330 }
10331 let sources: Vec<String> = sources_raw
10332 .split('+')
10333 .map(|s| s.trim().to_owned())
10334 .filter(|s| !s.is_empty())
10335 .collect();
10336 if sources.is_empty() {
10337 return None;
10338 }
10339 Some((target, sources))
10340 })
10341 .collect()
10342}
10343
10344/// Parse Django-shape `formfield_overrides` — `"field:widget,field2:widget2"`
10345/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10346/// and empty halves drop silently — the macro layer only enforces shape,
10347/// not field-name vs. widget-name validity (those checks happen at
10348/// `AdminConfig` consumption time). Issue #359.
10349fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10350 raw.split(',')
10351 .map(str::trim)
10352 .filter(|s| !s.is_empty())
10353 .filter_map(|entry| {
10354 let (field, widget) = entry.split_once(':')?;
10355 let field = field.trim().to_owned();
10356 let widget = widget.trim().to_owned();
10357 if field.is_empty() || widget.is_empty() {
10358 return None;
10359 }
10360 Some((field, widget))
10361 })
10362 .collect()
10363}
10364
10365/// Parse Django-shape ordering — `"name"` is ASC, `"-name"` is DESC.
10366/// Returns `(field_name, desc)` pairs in the same order as the input.
10367fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10368 raw.split(',')
10369 .map(str::trim)
10370 .filter(|s| !s.is_empty())
10371 .map(|spec| {
10372 spec.strip_prefix('-')
10373 .map_or((spec.to_owned(), false), |rest| {
10374 (rest.trim().to_owned(), true)
10375 })
10376 })
10377 .collect()
10378}
10379
10380struct FieldAttrs {
10381 column: Option<String>,
10382 primary_key: bool,
10383 fk: Option<String>,
10384 o2o: Option<String>,
10385 on: Option<String>,
10386 /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10387 /// "set_default" | "no_action")]` — Django-shape
10388 /// `ForeignKey(on_delete=…)`. Only meaningful when `fk` / `o2o` is
10389 /// also set; the macro errors at compile time if applied to a
10390 /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10391 /// DDL writer renders `ON DELETE <action>` after the constraint
10392 /// clause when this is `Some`; `None` falls back to the database
10393 /// default (NO ACTION on every backend rustango supports).
10394 on_delete: Option<String>,
10395 /// `#[rustango(related_name = "...")]` — Django-shape per-FK
10396 /// reverse-accessor override. When set, the derive emits
10397 /// `Parent::<related_name>[_pool]` instead of the container-level
10398 /// `default_related_name` or the `<child_snake>_set[_pool]`
10399 /// fallback. Only meaningful when `fk` / `o2o` is also set;
10400 /// silently ignored on non-FK fields. Follow-up to #816.
10401 related_name: Option<String>,
10402 max_length: Option<u32>,
10403 /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10404 /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10405 /// unconstrained `vector` column.
10406 vector_dims: Option<u32>,
10407 /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10408 /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10409 /// unconstrained `geometry(Point)` column (SRID 0).
10410 geometry_srid: Option<u32>,
10411 min: Option<i64>,
10412 max: Option<i64>,
10413 default: Option<String>,
10414 /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10415 /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10416 /// "gen_random_uuid()"`. The Rust field type must be
10417 /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10418 /// INSERTs so the DB DEFAULT fires.
10419 auto_uuid: bool,
10420 /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10421 /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10422 /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10423 /// the field is `Auto::Unset`, then bound as a normal parameter
10424 /// rather than relying on a per-dialect DB function. Issue #823
10425 /// (Eloquent `HasUuids`).
10426 ///
10427 /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10428 /// Composes with every backend (PG / MySQL / SQLite) — no
10429 /// `gen_random_uuid()` requirement on the database.
10430 default_uuid_v7: bool,
10431 /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10432 /// Server-set on insert, immutable from app code afterwards.
10433 /// Implies `auto + default = "now()"`. Field type must be
10434 /// `DateTime<Utc>`.
10435 auto_now_add: bool,
10436 /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10437 /// every insert AND every update. Implies `auto + default =
10438 /// "now()"`; the macro additionally rewrites `update_on` /
10439 /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10440 /// field value.
10441 auto_now: bool,
10442 /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10443 /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10444 /// `soft_delete_on(executor)` and `restore_on(executor)`
10445 /// methods on the model.
10446 soft_delete: bool,
10447 /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10448 /// the column in the generated DDL.
10449 unique: bool,
10450 /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10451 /// generates a `CREATE INDEX` for this column. `unique` here means
10452 /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10453 index: bool,
10454 index_unique: bool,
10455 index_name: Option<String>,
10456 /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10457 /// `"btree"`. Issue #34.
10458 index_method: String,
10459 /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10460 /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10461 /// the macro skips this column from every INSERT and UPDATE
10462 /// path, so the database always recomputes the value from
10463 /// `EXPR`. Backlog item #35.
10464 generated_as: Option<String>,
10465 /// `#[rustango(help_text = "…")]` — Django-shape help text
10466 /// rendered below the admin form's input. Threaded into
10467 /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10468 /// layers can read it.
10469 help_text: Option<String>,
10470 /// `#[rustango(choices = "value:Label, value:Label")]` — Django-shape
10471 /// enumerated allowed values. Threaded into `FieldSchema::choices`
10472 /// as a `&'static [(&'static str, &'static str)]` slice. When
10473 /// present, the admin form renders a `<select>` instead of `<input>`
10474 /// and the validator rejects values not in the list. Only meaningful
10475 /// for `FieldType::String`; the macro errors at compile time if
10476 /// applied to a non-string field.
10477 choices: Option<Vec<(String, String)>>,
10478 /// `#[rustango(db_comment = "…")]` — Django-shape DB-side column
10479 /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10480 /// the comment in CREATE TABLE; Postgres emits a separate
10481 /// `COMMENT ON COLUMN` statement after the table is created;
10482 /// SQLite silently drops the value (no native column comments).
10483 db_comment: Option<String>,
10484 /// `#[rustango(verbose_name = "…")]` — Django-shape human-readable
10485 /// label for the field. Threaded into `FieldSchema::verbose_name`
10486 /// so admin column headers, form labels, and other display
10487 /// surfaces can prefer the friendly caption over the Rust
10488 /// identifier. `None` means renderers fall back to the field name.
10489 verbose_name: Option<String>,
10490 /// `#[rustango(editable = false)]` — Django-shape opt-out from
10491 /// auto-generated form rendering. Defaults to `true` so existing
10492 /// fields keep their current admin / form behavior; setting
10493 /// `false` removes the field from the admin change-form entirely
10494 /// (the value is still visible on detail / list views, just not
10495 /// editable).
10496 editable: bool,
10497 /// `#[rustango(blank)]` / `#[rustango(blank = true)]` — Django-shape
10498 /// "form may submit empty even when DB is NOT NULL". Threaded into
10499 /// `FieldSchema::blank`. Defaults to `false`.
10500 blank: bool,
10501 /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10502 /// Django-shape `CITextField`. Threaded into
10503 /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10504 /// fields; the macro errors at derive time if applied elsewhere.
10505 case_insensitive: bool,
10506 /// `#[rustango(validators = "email,url")]` — Django-shape
10507 /// model-level validator chain. Comma-separated names that
10508 /// dispatch to the `validators::*` family in `validate_value`.
10509 /// Empty by default; fires on every typed INSERT/UPDATE.
10510 validators: Vec<String>,
10511}
10512
10513fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10514 let mut out = FieldAttrs {
10515 column: None,
10516 primary_key: false,
10517 fk: None,
10518 o2o: None,
10519 on: None,
10520 on_delete: None,
10521 related_name: None,
10522 max_length: None,
10523 vector_dims: None,
10524 geometry_srid: None,
10525 min: None,
10526 max: None,
10527 default: None,
10528 auto_uuid: false,
10529 default_uuid_v7: false,
10530 auto_now_add: false,
10531 auto_now: false,
10532 soft_delete: false,
10533 unique: false,
10534 index: false,
10535 index_unique: false,
10536 index_name: None,
10537 index_method: "btree".to_owned(),
10538 generated_as: None,
10539 help_text: None,
10540 choices: None,
10541 db_comment: None,
10542 verbose_name: None,
10543 editable: true,
10544 blank: false,
10545 case_insensitive: false,
10546 validators: Vec::new(),
10547 };
10548 for attr in &field.attrs {
10549 if !attr.path().is_ident("rustango") {
10550 continue;
10551 }
10552 attr.parse_nested_meta(|meta| {
10553 if meta.path.is_ident("column") {
10554 let s: LitStr = meta.value()?.parse()?;
10555 let name = s.value();
10556 validate_sql_identifier(&name, "column", s.span())?;
10557 out.column = Some(name);
10558 return Ok(());
10559 }
10560 if meta.path.is_ident("primary_key") {
10561 out.primary_key = true;
10562 return Ok(());
10563 }
10564 if meta.path.is_ident("fk") {
10565 let s: LitStr = meta.value()?.parse()?;
10566 out.fk = Some(s.value());
10567 return Ok(());
10568 }
10569 if meta.path.is_ident("o2o") {
10570 let s: LitStr = meta.value()?.parse()?;
10571 out.o2o = Some(s.value());
10572 return Ok(());
10573 }
10574 if meta.path.is_ident("on") {
10575 let s: LitStr = meta.value()?.parse()?;
10576 out.on = Some(s.value());
10577 return Ok(());
10578 }
10579 if meta.path.is_ident("on_delete") {
10580 let s: LitStr = meta.value()?.parse()?;
10581 let raw = s.value();
10582 let normalized = raw.trim().to_ascii_lowercase();
10583 // Validate at parse time so the user gets a clear span
10584 // rather than a downstream compile error in the emit.
10585 match normalized.as_str() {
10586 "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10587 _ => {
10588 return Err(syn::Error::new(
10589 s.span(),
10590 format!(
10591 "unknown on_delete action `{raw}`; expected one of \
10592 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10593 ),
10594 ));
10595 }
10596 }
10597 out.on_delete = Some(normalized);
10598 return Ok(());
10599 }
10600 if meta.path.is_ident("related_name") {
10601 let s: LitStr = meta.value()?.parse()?;
10602 let raw = s.value();
10603 if raw.trim().is_empty() {
10604 return Err(syn::Error::new(
10605 s.span(),
10606 "`related_name` must be a non-empty identifier",
10607 ));
10608 }
10609 // Validate as a Rust-ident shape — the value becomes
10610 // a method name on the parent type. Same rule as
10611 // `default_related_name` so the two surfaces match.
10612 if !raw
10613 .chars()
10614 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10615 || raw.starts_with(char::is_numeric)
10616 {
10617 return Err(syn::Error::new(
10618 s.span(),
10619 "`related_name` must be snake_case ASCII (lowercase letters, \
10620 digits, underscores; no leading digit)",
10621 ));
10622 }
10623 out.related_name = Some(raw);
10624 return Ok(());
10625 }
10626 if meta.path.is_ident("max_length") {
10627 let lit: syn::LitInt = meta.value()?.parse()?;
10628 out.max_length = Some(lit.base10_parse::<u32>()?);
10629 return Ok(());
10630 }
10631 // `#[rustango(vector(dims = N))]` — pgvector column
10632 // dimension (#824). Nested-meta form so it reads like the
10633 // other typed-column attrs.
10634 if meta.path.is_ident("vector") {
10635 meta.parse_nested_meta(|inner| {
10636 if inner.path.is_ident("dims") {
10637 let lit: syn::LitInt = inner.value()?.parse()?;
10638 out.vector_dims = Some(lit.base10_parse::<u32>()?);
10639 return Ok(());
10640 }
10641 Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10642 })?;
10643 return Ok(());
10644 }
10645 // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10646 // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10647 if meta.path.is_ident("geometry") {
10648 meta.parse_nested_meta(|inner| {
10649 if inner.path.is_ident("srid") {
10650 let lit: syn::LitInt = inner.value()?.parse()?;
10651 out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10652 return Ok(());
10653 }
10654 Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10655 })?;
10656 return Ok(());
10657 }
10658 if meta.path.is_ident("min") {
10659 out.min = Some(parse_signed_i64(&meta)?);
10660 return Ok(());
10661 }
10662 if meta.path.is_ident("max") {
10663 out.max = Some(parse_signed_i64(&meta)?);
10664 return Ok(());
10665 }
10666 if meta.path.is_ident("default") {
10667 let s: LitStr = meta.value()?.parse()?;
10668 out.default = Some(s.value());
10669 return Ok(());
10670 }
10671 if meta.path.is_ident("generated_as") {
10672 let s: LitStr = meta.value()?.parse()?;
10673 out.generated_as = Some(s.value());
10674 return Ok(());
10675 }
10676 if meta.path.is_ident("help_text") {
10677 let s: LitStr = meta.value()?.parse()?;
10678 out.help_text = Some(s.value());
10679 return Ok(());
10680 }
10681 if meta.path.is_ident("choices") {
10682 let s: LitStr = meta.value()?.parse()?;
10683 let raw = s.value();
10684 let mut pairs: Vec<(String, String)> = Vec::new();
10685 for chunk in raw.split(',') {
10686 let chunk = chunk.trim();
10687 if chunk.is_empty() {
10688 continue;
10689 }
10690 let (value, label) = match chunk.split_once(':') {
10691 Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10692 None => (chunk.to_owned(), chunk.to_owned()),
10693 };
10694 if value.is_empty() {
10695 return Err(syn::Error::new(
10696 s.span(),
10697 "`choices` entry has empty value before `:`",
10698 ));
10699 }
10700 pairs.push((value, label));
10701 }
10702 if pairs.is_empty() {
10703 return Err(syn::Error::new(
10704 s.span(),
10705 "`choices = \"…\"` must contain at least one value",
10706 ));
10707 }
10708 out.choices = Some(pairs);
10709 return Ok(());
10710 }
10711 if meta.path.is_ident("db_comment") {
10712 let s: LitStr = meta.value()?.parse()?;
10713 out.db_comment = Some(s.value());
10714 return Ok(());
10715 }
10716 if meta.path.is_ident("verbose_name") {
10717 let s: LitStr = meta.value()?.parse()?;
10718 out.verbose_name = Some(s.value());
10719 return Ok(());
10720 }
10721 if meta.path.is_ident("editable") {
10722 // Two forms accepted:
10723 // #[rustango(editable = false)] / true — explicit
10724 // #[rustango(editable)] — flag form (= true, the
10725 // default, so harmless; included for symmetry)
10726 if let Ok(v) = meta.value() {
10727 let lit: syn::LitBool = v.parse()?;
10728 out.editable = lit.value;
10729 } else {
10730 out.editable = true;
10731 }
10732 return Ok(());
10733 }
10734 if meta.path.is_ident("blank") {
10735 // Two forms accepted:
10736 // #[rustango(blank)] — flag form, true
10737 // #[rustango(blank = false)] / true — explicit
10738 if let Ok(v) = meta.value() {
10739 let lit: syn::LitBool = v.parse()?;
10740 out.blank = lit.value;
10741 } else {
10742 out.blank = true;
10743 }
10744 return Ok(());
10745 }
10746 if meta.path.is_ident("citext") {
10747 // Django-parity CITextField (#344). Two forms:
10748 // #[rustango(citext)] — flag form, true
10749 // #[rustango(citext = true)] — explicit
10750 // #[rustango(citext = false)] — explicit opt-out
10751 // String-only validation lives in the field-type
10752 // emitter (the FieldType discriminant is computed in
10753 // `detect_type`); the macro records the flag and the
10754 // DDL writer emits dialect-specific COLLATE / CITEXT.
10755 if let Ok(v) = meta.value() {
10756 let lit: syn::LitBool = v.parse()?;
10757 out.case_insensitive = lit.value;
10758 } else {
10759 out.case_insensitive = true;
10760 }
10761 return Ok(());
10762 }
10763 if meta.path.is_ident("validators") {
10764 let s: LitStr = meta.value()?.parse()?;
10765 let raw = s.value();
10766 out.validators = raw
10767 .split(',')
10768 .map(str::trim)
10769 .filter(|s| !s.is_empty())
10770 .map(str::to_owned)
10771 .collect();
10772 if out.validators.is_empty() {
10773 return Err(syn::Error::new(
10774 s.span(),
10775 "`validators = \"…\"` must list at least one name",
10776 ));
10777 }
10778 return Ok(());
10779 }
10780 if meta.path.is_ident("auto_uuid") {
10781 out.auto_uuid = true;
10782 // Implied: PK + auto + DEFAULT gen_random_uuid().
10783 // Each is also explicitly settable; the explicit
10784 // value wins if conflicting.
10785 out.primary_key = true;
10786 if out.default.is_none() {
10787 out.default = Some("gen_random_uuid()".into());
10788 }
10789 return Ok(());
10790 }
10791 if meta.path.is_ident("default_uuid_v7") {
10792 // Backend-neutral counterpart of `auto_uuid` — issue #823.
10793 // No SQL DEFAULT (the macro fills the value Rust-side
10794 // before binding); just mark the field as PK + Auto
10795 // so the insert path is the `Auto::Unset → generate`
10796 // branch.
10797 out.default_uuid_v7 = true;
10798 out.primary_key = true;
10799 return Ok(());
10800 }
10801 if meta.path.is_ident("auto_now_add") {
10802 out.auto_now_add = true;
10803 if out.default.is_none() {
10804 out.default = Some("now()".into());
10805 }
10806 return Ok(());
10807 }
10808 if meta.path.is_ident("auto_now") {
10809 out.auto_now = true;
10810 if out.default.is_none() {
10811 out.default = Some("now()".into());
10812 }
10813 return Ok(());
10814 }
10815 if meta.path.is_ident("soft_delete") {
10816 out.soft_delete = true;
10817 return Ok(());
10818 }
10819 if meta.path.is_ident("unique") {
10820 out.unique = true;
10821 return Ok(());
10822 }
10823 if meta.path.is_ident("index") {
10824 out.index = true;
10825 // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
10826 if meta.input.peek(syn::token::Paren) {
10827 meta.parse_nested_meta(|inner| {
10828 if inner.path.is_ident("unique") {
10829 out.index_unique = true;
10830 return Ok(());
10831 }
10832 if inner.path.is_ident("name") {
10833 let s: LitStr = inner.value()?.parse()?;
10834 out.index_name = Some(s.value());
10835 return Ok(());
10836 }
10837 if inner.path.is_ident("method") {
10838 let s: LitStr = inner.value()?.parse()?;
10839 let v = s.value();
10840 match v.as_str() {
10841 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
10842 out.index_method = v;
10843 }
10844 other => {
10845 return Err(inner.error(format!(
10846 "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
10847 )));
10848 }
10849 }
10850 return Ok(());
10851 }
10852 Err(inner.error(
10853 "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
10854 ))
10855 })?;
10856 }
10857 return Ok(());
10858 }
10859 Err(meta.error("unknown rustango field attribute"))
10860 })?;
10861 }
10862 Ok(out)
10863}
10864
10865/// Parse a signed integer literal, accepting optional leading `-`.
10866fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
10867 let expr: syn::Expr = meta.value()?.parse()?;
10868 match expr {
10869 syn::Expr::Lit(syn::ExprLit {
10870 lit: syn::Lit::Int(lit),
10871 ..
10872 }) => lit.base10_parse::<i64>(),
10873 syn::Expr::Unary(syn::ExprUnary {
10874 op: syn::UnOp::Neg(_),
10875 expr,
10876 ..
10877 }) => {
10878 if let syn::Expr::Lit(syn::ExprLit {
10879 lit: syn::Lit::Int(lit),
10880 ..
10881 }) = *expr
10882 {
10883 let v: i64 = lit.base10_parse()?;
10884 Ok(-v)
10885 } else {
10886 Err(syn::Error::new_spanned(expr, "expected integer literal"))
10887 }
10888 }
10889 other => Err(syn::Error::new_spanned(
10890 other,
10891 "expected integer literal (signed)",
10892 )),
10893 }
10894}
10895
10896struct FieldInfo<'a> {
10897 ident: &'a syn::Ident,
10898 column: String,
10899 primary_key: bool,
10900 /// `true` when the Rust type was `Auto<T>` — the INSERT path will
10901 /// skip this column when `Auto::Unset` and emit it under
10902 /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
10903 auto: bool,
10904 /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
10905 /// the `Column::Value` associated type for typed-column tokens.
10906 value_ty: &'a Type,
10907 /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
10908 field_type_tokens: TokenStream2,
10909 schema: TokenStream2,
10910 from_row_init: TokenStream2,
10911 /// Variant of [`Self::from_row_init`] that reads the column via
10912 /// `format!("{prefix}__{col}")` so a model can be decoded out of
10913 /// the aliased columns of a JOINed row. Drives slice 9.0d's
10914 /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
10915 /// helper that `select_related` calls when stitching loaded FKs.
10916 from_aliased_row_init: TokenStream2,
10917 /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
10918 /// relation helper emit (`Author::<child>_set`) needs to know `T`
10919 /// to point the generated method at the right child model.
10920 fk_inner: Option<Type>,
10921 /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
10922 /// `kind` (since ForeignKey detection sets `kind` to K's
10923 /// underlying type) but stored separately for clarity at the
10924 /// `FkRelation` construction site, which only sees the FK's
10925 /// surface fields.
10926 fk_pk_kind: DetectedKind,
10927 /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
10928 /// the bare `ForeignKey<T, K>`. Routes the load_related and
10929 /// fk_pk_access emitters to wrap assignments / accessors in
10930 /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
10931 /// FK column compiles end-to-end. The DDL writer reads this off
10932 /// the field schema (`nullable` flag); the macro just needs to
10933 /// keep the Rust-side codegen consistent.
10934 nullable: bool,
10935 /// `true` when this column was marked `#[rustango(auto_now)]` —
10936 /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
10937 /// column instead of the user-supplied value, so `updated_at`
10938 /// always reflects the latest write without the caller having
10939 /// to remember to set it.
10940 auto_now: bool,
10941 /// `true` when this column was marked `#[rustango(auto_now_add)]`
10942 /// — the column is server-set on INSERT (DB DEFAULT) and
10943 /// **immutable** afterwards. `update_on` / `save_on` skip the
10944 /// column entirely so a stale `created_at` value in memory never
10945 /// rewrites the persisted timestamp.
10946 auto_now_add: bool,
10947 /// `true` when this column was marked `#[rustango(soft_delete)]`.
10948 /// Triggers emission of `soft_delete_on(executor)` and
10949 /// `restore_on(executor)` on the model's inherent impl. There is
10950 /// at most one such column per model — emission asserts this.
10951 soft_delete: bool,
10952 /// `Some` when this column was marked
10953 /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
10954 /// every INSERT and UPDATE path; the database recomputes the
10955 /// value from `EXPR`. Backlog item #35.
10956 generated_as: Option<String>,
10957 /// `true` when this column was marked
10958 /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
10959 /// emit an `insert_push` that auto-fills an `Auto::Unset` value
10960 /// with `Uuid::now_v7()` before binding, so the PK is generated
10961 /// Rust-side and the column is always present in the INSERT
10962 /// statement (no DB DEFAULT requirement). Issue #823.
10963 default_uuid_v7: bool,
10964 /// `Some` when this FK field carried `#[rustango(related_name =
10965 /// "...")]`. Threaded into [`FkRelation::related_name`] and
10966 /// consumed by [`reverse_helper_tokens`] to override the default
10967 /// `<child_snake>_set` accessor name. Follow-up to #816.
10968 related_name: Option<String>,
10969}
10970
10971/// Reject table names that won't survive SQL identifier
10972/// derivation downstream. Postgres' regular-identifier rule
10973/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
10974/// through the framework's unquoted FK / index / constraint name
10975/// emission without surprises. We also disallow leading-digit and
10976/// the empty string for clarity.
10977///
10978/// Reserved-word collisions (`select`, `from`, …) aren't flagged
10979/// here — those produce a runtime error from the SQL parser,
10980/// which is loud enough; statically enumerating reserved words
10981/// across the three supported dialects is more friction than help.
10982///
10983/// Backlog item #65.
10984fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
10985 validate_sql_identifier(name, "table", span)
10986}
10987
10988/// Reject SQL identifiers that compile but break downstream SQL
10989/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
10990/// `kind` is "table" / "column" — used for the error message so users
10991/// see which attribute caused the failure.
10992fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
10993 if name.is_empty() {
10994 return Err(syn::Error::new(
10995 span,
10996 format!("`{kind} = \"\"` is not a valid SQL identifier"),
10997 ));
10998 }
10999 let mut chars = name.chars();
11000 let first = chars.next().unwrap();
11001 if !(first.is_ascii_alphabetic() || first == '_') {
11002 return Err(syn::Error::new(
11003 span,
11004 format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11005 ));
11006 }
11007 for c in chars {
11008 if !(c.is_ascii_alphanumeric() || c == '_') {
11009 return Err(syn::Error::new(
11010 span,
11011 format!(
11012 "{kind} name `{name}` contains invalid character {c:?} — \
11013 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11014 Hyphens in particular break FK / index name derivation \
11015 downstream; use underscores instead (e.g. `{}`)",
11016 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11017 ),
11018 ));
11019 }
11020 }
11021 Ok(())
11022}
11023
11024fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11025 let root = rustango_root();
11026 let attrs = parse_field_attrs(field)?;
11027 let ident = field
11028 .ident
11029 .as_ref()
11030 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11031 let name = ident.to_string();
11032 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11033 let primary_key = attrs.primary_key;
11034 let DetectedType {
11035 kind,
11036 nullable,
11037 auto: detected_auto,
11038 fk_inner,
11039 } = detect_type(&field.ty)?;
11040 check_bound_compatibility(field, &attrs, kind)?;
11041 let auto = detected_auto;
11042 // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11043 // INSERT path: the user must wrap the field in `Auto<T>`, which
11044 // marks the column as DB-default-supplied. The mixin attrs then
11045 // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11046 // for `auto_now`, force the value on UPDATE too.
11047 if attrs.auto_uuid {
11048 if kind != DetectedKind::Uuid {
11049 return Err(syn::Error::new_spanned(
11050 field,
11051 "`#[rustango(auto_uuid)]` requires the field type to be \
11052 `Auto<uuid::Uuid>`",
11053 ));
11054 }
11055 if !detected_auto {
11056 return Err(syn::Error::new_spanned(
11057 field,
11058 "`#[rustango(auto_uuid)]` requires the field type to be \
11059 wrapped in `Auto<...>` so the macro skips the column on \
11060 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11061 ));
11062 }
11063 }
11064 if attrs.default_uuid_v7 {
11065 if kind != DetectedKind::Uuid {
11066 return Err(syn::Error::new_spanned(
11067 field,
11068 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11069 `Auto<uuid::Uuid>`",
11070 ));
11071 }
11072 if !detected_auto {
11073 return Err(syn::Error::new_spanned(
11074 field,
11075 "`#[rustango(default_uuid_v7)]` requires the field type to be \
11076 wrapped in `Auto<...>` so the macro can detect the \
11077 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11078 ));
11079 }
11080 if attrs.auto_uuid {
11081 return Err(syn::Error::new_spanned(
11082 field,
11083 "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11084 `#[rustango(auto_uuid)]` — the former generates the UUID \
11085 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11086 Pick one.",
11087 ));
11088 }
11089 }
11090 if attrs.auto_now_add || attrs.auto_now {
11091 if kind != DetectedKind::DateTime {
11092 return Err(syn::Error::new_spanned(
11093 field,
11094 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11095 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11096 ));
11097 }
11098 if !detected_auto {
11099 return Err(syn::Error::new_spanned(
11100 field,
11101 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11102 the field type to be wrapped in `Auto<...>` so the macro skips \
11103 the column on INSERT and the DB DEFAULT (`now()`) fires",
11104 ));
11105 }
11106 }
11107 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11108 return Err(syn::Error::new_spanned(
11109 field,
11110 "`#[rustango(soft_delete)]` requires the field type to be \
11111 `Option<chrono::DateTime<chrono::Utc>>`",
11112 ));
11113 }
11114 let is_mixin_auto =
11115 attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11116 if detected_auto && !primary_key && !is_mixin_auto {
11117 return Err(syn::Error::new_spanned(
11118 field,
11119 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11120 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11121 `auto_now`",
11122 ));
11123 }
11124 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11125 return Err(syn::Error::new_spanned(
11126 field,
11127 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11128 SERIAL / BIGSERIAL already supplies a default sequence.",
11129 ));
11130 }
11131 if fk_inner.is_some() && primary_key {
11132 return Err(syn::Error::new_spanned(
11133 field,
11134 "`ForeignKey<T>` is not allowed on a primary-key field — \
11135 a row's PK is its own identity, not a reference to a parent.",
11136 ));
11137 }
11138 if attrs.generated_as.is_some() {
11139 if primary_key {
11140 return Err(syn::Error::new_spanned(
11141 field,
11142 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11143 primary-key field — a PK must be writable so the row \
11144 has an identity at INSERT time.",
11145 ));
11146 }
11147 if attrs.default.is_some() {
11148 return Err(syn::Error::new_spanned(
11149 field,
11150 "`#[rustango(generated_as = \"…\")]` cannot combine with \
11151 `default = \"…\"` — Postgres rejects DEFAULT on \
11152 generated columns. The expression IS the default.",
11153 ));
11154 }
11155 if detected_auto {
11156 return Err(syn::Error::new_spanned(
11157 field,
11158 "`#[rustango(generated_as = \"…\")]` is not allowed on \
11159 an `Auto<T>` field — generated columns are computed \
11160 by the DB, not server-assigned via a sequence. Use a \
11161 plain Rust type (e.g. `f64`).",
11162 ));
11163 }
11164 if fk_inner.is_some() {
11165 return Err(syn::Error::new_spanned(
11166 field,
11167 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11168 ForeignKey field.",
11169 ));
11170 }
11171 }
11172 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11173 let column_lit = column.as_str();
11174 // pgvector (#824): the `vector(dims = N)` attribute supplies the
11175 // dimension that the bare `Vector` Rust type can't carry, so emit
11176 // `FieldType::Vector(N)` here rather than the `variant_tokens`
11177 // fallback of `Vector(0)`.
11178 let field_type_tokens = if kind == DetectedKind::Vector {
11179 let root = rustango_root();
11180 let dims = attrs.vector_dims.unwrap_or(0);
11181 quote!(#root::core::FieldType::Vector(#dims))
11182 } else if kind == DetectedKind::Geometry {
11183 // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11184 // SRID that the bare `Point` Rust type can't carry, so emit
11185 // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11186 let root = rustango_root();
11187 let srid = attrs.geometry_srid.unwrap_or(0);
11188 quote!(#root::core::FieldType::Geometry(#srid))
11189 } else {
11190 kind.variant_tokens()
11191 };
11192 let max_length = optional_u32(attrs.max_length);
11193 let min = optional_i64(attrs.min);
11194 let max = optional_i64(attrs.max);
11195 let default = optional_str(attrs.default.as_deref());
11196
11197 let unique = attrs.unique;
11198 let generated_as = optional_str(attrs.generated_as.as_deref());
11199 let help_text = optional_str(attrs.help_text.as_deref());
11200 let choices = optional_choices(attrs.choices.as_deref());
11201 let db_comment = optional_str(attrs.db_comment.as_deref());
11202 let verbose_name = optional_str(attrs.verbose_name.as_deref());
11203 let editable = attrs.editable;
11204 let blank = attrs.blank;
11205 let case_insensitive = attrs.case_insensitive;
11206 let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11207 if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() {
11208 return Err(syn::Error::new_spanned(
11209 field,
11210 "`#[rustango(on_delete = \"…\")]` requires either `fk = \"<table>\"` \
11211 or `o2o = \"<table>\"` on the same field — it has no meaning on a \
11212 non-FK column.",
11213 ));
11214 }
11215 let fk_on_delete = match attrs.on_delete.as_deref() {
11216 None => quote!(::core::option::Option::None),
11217 Some(action) => {
11218 let variant = match action {
11219 "cascade" => quote!(Cascade),
11220 "restrict" => quote!(Restrict),
11221 "set_null" => quote!(SetNull),
11222 "set_default" => quote!(SetDefault),
11223 "no_action" => quote!(NoAction),
11224 // parse_field_attrs already validated this — guard against future drift.
11225 other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11226 };
11227 quote!(::core::option::Option::Some(
11228 #root::core::OnDeleteAction::#variant
11229 ))
11230 }
11231 };
11232 let schema = quote! {
11233 #root::core::FieldSchema {
11234 name: #name,
11235 column: #column_lit,
11236 ty: #field_type_tokens,
11237 nullable: #nullable,
11238 primary_key: #primary_key,
11239 relation: #relation,
11240 max_length: #max_length,
11241 min: #min,
11242 max: #max,
11243 default: #default,
11244 auto: #auto,
11245 unique: #unique,
11246 generated_as: #generated_as,
11247 help_text: #help_text,
11248 choices: #choices,
11249 db_comment: #db_comment,
11250 verbose_name: #verbose_name,
11251 editable: #editable,
11252 blank: #blank,
11253 case_insensitive: #case_insensitive,
11254 fk_on_delete: #fk_on_delete,
11255 validators: &[ #(#validators_lits),* ],
11256 }
11257 };
11258
11259 let from_row_init = quote! {
11260 #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11261 };
11262 let from_aliased_row_init = quote! {
11263 #ident: #root::sql::sqlx::Row::try_get(
11264 row,
11265 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11266 )?
11267 };
11268
11269 Ok(FieldInfo {
11270 ident,
11271 column,
11272 primary_key,
11273 auto,
11274 value_ty: &field.ty,
11275 field_type_tokens,
11276 schema,
11277 from_row_init,
11278 from_aliased_row_init,
11279 fk_inner: fk_inner.cloned(),
11280 fk_pk_kind: kind,
11281 nullable,
11282 auto_now: attrs.auto_now,
11283 auto_now_add: attrs.auto_now_add,
11284 soft_delete: attrs.soft_delete,
11285 generated_as: attrs.generated_as.clone(),
11286 default_uuid_v7: attrs.default_uuid_v7,
11287 related_name: attrs.related_name.clone(),
11288 })
11289}
11290
11291fn check_bound_compatibility(
11292 field: &syn::Field,
11293 attrs: &FieldAttrs,
11294 kind: DetectedKind,
11295) -> syn::Result<()> {
11296 if attrs.max_length.is_some() && kind != DetectedKind::String {
11297 return Err(syn::Error::new_spanned(
11298 field,
11299 "`max_length` is only valid on `String` fields (or `Option<String>`)",
11300 ));
11301 }
11302 if attrs.choices.is_some() && kind != DetectedKind::String {
11303 return Err(syn::Error::new_spanned(
11304 field,
11305 "`choices` is only valid on `String` fields (or `Option<String>`) — \
11306 integer-valued enumerations should be modeled with a Rust enum and \
11307 custom (de)serializer for now",
11308 ));
11309 }
11310 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11311 return Err(syn::Error::new_spanned(
11312 field,
11313 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11314 ));
11315 }
11316 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11317 if min > max {
11318 return Err(syn::Error::new_spanned(
11319 field,
11320 format!("`min` ({min}) is greater than `max` ({max})"),
11321 ));
11322 }
11323 }
11324 Ok(())
11325}
11326
11327fn optional_u32(value: Option<u32>) -> TokenStream2 {
11328 if let Some(v) = value {
11329 quote!(::core::option::Option::Some(#v))
11330 } else {
11331 quote!(::core::option::Option::None)
11332 }
11333}
11334
11335fn optional_i64(value: Option<i64>) -> TokenStream2 {
11336 if let Some(v) = value {
11337 quote!(::core::option::Option::Some(#v))
11338 } else {
11339 quote!(::core::option::Option::None)
11340 }
11341}
11342
11343fn optional_str(value: Option<&str>) -> 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_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11352 let Some(pairs) = pairs else {
11353 return quote!(::core::option::Option::None);
11354 };
11355 let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11356 quote!(::core::option::Option::Some(&[#(#entries),*]))
11357}
11358
11359fn relation_tokens(
11360 field: &syn::Field,
11361 attrs: &FieldAttrs,
11362 fk_inner: Option<&syn::Type>,
11363 table: &str,
11364) -> syn::Result<TokenStream2> {
11365 let root = rustango_root();
11366 if let Some(inner) = fk_inner {
11367 if attrs.fk.is_some() || attrs.o2o.is_some() {
11368 return Err(syn::Error::new_spanned(
11369 field,
11370 "`ForeignKey<T>` already declares the FK target via the type parameter — \
11371 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11372 ));
11373 }
11374 let on = attrs.on.as_deref().unwrap_or("id");
11375 return Ok(quote! {
11376 ::core::option::Option::Some(#root::core::Relation::Fk {
11377 to: <#inner as #root::core::Model>::SCHEMA.table,
11378 on: #on,
11379 })
11380 });
11381 }
11382 match (&attrs.fk, &attrs.o2o) {
11383 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11384 field,
11385 "`fk` and `o2o` are mutually exclusive",
11386 )),
11387 (Some(to), None) => {
11388 let on = attrs.on.as_deref().unwrap_or("id");
11389 // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11390 // the model's own table. Threaded as a literal string at
11391 // macro-expansion time to sidestep the const-eval cycle
11392 // that `Self::SCHEMA.table` would create when referenced
11393 // inside Self::SCHEMA's own initializer.
11394 let resolved = if to == "self" { table } else { to };
11395 Ok(quote! {
11396 ::core::option::Option::Some(#root::core::Relation::Fk { to: #resolved, on: #on })
11397 })
11398 }
11399 (None, Some(to)) => {
11400 let on = attrs.on.as_deref().unwrap_or("id");
11401 let resolved = if to == "self" { table } else { to };
11402 Ok(quote! {
11403 ::core::option::Option::Some(#root::core::Relation::O2O { to: #resolved, on: #on })
11404 })
11405 }
11406 (None, None) => {
11407 if attrs.on.is_some() {
11408 return Err(syn::Error::new_spanned(
11409 field,
11410 "`on` requires `fk` or `o2o`",
11411 ));
11412 }
11413 Ok(quote!(::core::option::Option::None))
11414 }
11415 }
11416}
11417
11418/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11419/// about kinds without depending on `rustango-core` (which would require a
11420/// proc-macro/normal split it doesn't have today).
11421#[derive(Clone, Copy, PartialEq, Eq)]
11422enum DetectedKind {
11423 I16,
11424 I32,
11425 I64,
11426 F32,
11427 F64,
11428 Bool,
11429 String,
11430 DateTime,
11431 Date,
11432 Time,
11433 Uuid,
11434 Json,
11435 Decimal,
11436 Binary,
11437 /// `Array<String>` → PG `text[]` (#341).
11438 ArrayText,
11439 /// `Array<i32>` → PG `integer[]` (#341).
11440 ArrayInt,
11441 /// `Array<i64>` → PG `bigint[]` (#341).
11442 ArrayBigInt,
11443 /// `Range<i32>` → PG `int4range` (#343).
11444 RangeInt,
11445 /// `Range<i64>` → PG `int8range` (#343).
11446 RangeBigInt,
11447 /// `Range<Decimal>` → PG `numrange` (#343).
11448 RangeNumeric,
11449 /// `Range<NaiveDate>` → PG `daterange` (#343).
11450 RangeDate,
11451 /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11452 RangeDateTime,
11453 /// `HStore` → PG `hstore` (#342).
11454 HStore,
11455 /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11456 /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11457 /// in at the `FieldType` emission site (not carried on this enum).
11458 Vector,
11459 /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11460 /// from the `#[rustango(geometry(srid = N))]` field attribute,
11461 /// threaded in at the `FieldType` emission site.
11462 Geometry,
11463}
11464
11465impl DetectedKind {
11466 fn variant_tokens(self) -> TokenStream2 {
11467 let root = rustango_root();
11468 match self {
11469 Self::I16 => quote!(#root::core::FieldType::I16),
11470 Self::I32 => quote!(#root::core::FieldType::I32),
11471 Self::I64 => quote!(#root::core::FieldType::I64),
11472 Self::F32 => quote!(#root::core::FieldType::F32),
11473 Self::F64 => quote!(#root::core::FieldType::F64),
11474 Self::Bool => quote!(#root::core::FieldType::Bool),
11475 Self::String => quote!(#root::core::FieldType::String),
11476 Self::DateTime => quote!(#root::core::FieldType::DateTime),
11477 Self::Date => quote!(#root::core::FieldType::Date),
11478 Self::Time => quote!(#root::core::FieldType::Time),
11479 Self::Uuid => quote!(#root::core::FieldType::Uuid),
11480 Self::Json => quote!(#root::core::FieldType::Json),
11481 Self::Decimal => quote!(#root::core::FieldType::Decimal),
11482 Self::Binary => quote!(#root::core::FieldType::Binary),
11483 Self::ArrayText => {
11484 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11485 }
11486 Self::ArrayInt => {
11487 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11488 }
11489 Self::ArrayBigInt => {
11490 quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11491 }
11492 Self::RangeInt => {
11493 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11494 }
11495 Self::RangeBigInt => {
11496 quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11497 }
11498 Self::RangeNumeric => {
11499 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11500 }
11501 Self::RangeDate => {
11502 quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11503 }
11504 Self::RangeDateTime => {
11505 quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11506 }
11507 Self::HStore => quote!(#root::core::FieldType::HStore),
11508 // Dimension comes from the `vector(dims = N)` attribute,
11509 // applied at the emission site; `0` here is just a fallback.
11510 Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11511 // SRID comes from the `geometry(srid = N)` attribute, applied
11512 // at the emission site; `0` here is just a fallback.
11513 Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11514 }
11515 }
11516
11517 fn is_integer(self) -> bool {
11518 matches!(self, Self::I16 | Self::I32 | Self::I64)
11519 }
11520
11521 /// `(SqlValue::<Variant>, default expr)` for emitting the
11522 /// `match SqlValue { … }` arm in `LoadRelated::__rustango_load_related`
11523 /// for a `ForeignKey<T, K>` FK whose K maps to `self`. The default
11524 /// fires only when the parent's `__rustango_pk_value` returns a
11525 /// different variant than expected, which is a compile-time bug —
11526 /// but we still need a value-typed fallback to keep the match
11527 /// total.
11528 fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
11529 let root = rustango_root();
11530 match self {
11531 Self::I16 => (quote!(I16), quote!(0i16)),
11532 Self::I32 => (quote!(I32), quote!(0i32)),
11533 Self::I64 => (quote!(I64), quote!(0i64)),
11534 Self::F32 => (quote!(F32), quote!(0f32)),
11535 Self::F64 => (quote!(F64), quote!(0f64)),
11536 Self::Bool => (quote!(Bool), quote!(false)),
11537 Self::String => (quote!(String), quote!(::std::string::String::new())),
11538 Self::DateTime => (
11539 quote!(DateTime),
11540 quote!(<#root::__chrono::DateTime<#root::__chrono::Utc> as ::std::default::Default>::default()),
11541 ),
11542 Self::Date => (
11543 quote!(Date),
11544 quote!(<#root::__chrono::NaiveDate as ::std::default::Default>::default()),
11545 ),
11546 Self::Time => (
11547 quote!(Time),
11548 quote!(<#root::__chrono::NaiveTime as ::std::default::Default>::default()),
11549 ),
11550 Self::Uuid => (quote!(Uuid), quote!(#root::__uuid::Uuid::nil())),
11551 Self::Json => (quote!(Json), quote!(#root::__serde_json::Value::Null)),
11552 Self::Decimal => (
11553 quote!(Decimal),
11554 quote!(<#root::__rust_decimal::Decimal as ::std::default::Default>::default()),
11555 ),
11556 Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
11557 // Arrays (#341) can never be a foreign-key primary key, so
11558 // this arm is never reached at runtime — but the match must
11559 // stay total. A bare empty `SqlValue::Array` is the dummy.
11560 Self::ArrayText | Self::ArrayInt | Self::ArrayBigInt => {
11561 (quote!(Array), quote!(::std::vec::Vec::new()))
11562 }
11563 // Ranges (#343) likewise can't be a FK PK — never reached.
11564 Self::RangeInt
11565 | Self::RangeBigInt
11566 | Self::RangeNumeric
11567 | Self::RangeDate
11568 | Self::RangeDateTime => (quote!(RangeLiteral), quote!(::std::string::String::new())),
11569 // HStore (#342) can't be a FK PK — never reached.
11570 Self::HStore => (quote!(HStore), quote!(::std::vec::Vec::new())),
11571 // Vector (#824) can't be a FK PK — never reached.
11572 Self::Vector => (quote!(Vector), quote!(::std::vec::Vec::new())),
11573 // Geometry (#443) can't be a FK PK — never reached (arm is
11574 // exhaustiveness-only; never interpolated into emitted code).
11575 Self::Geometry => (quote!(Geometry), quote!(::std::vec::Vec::new())),
11576 }
11577 }
11578}
11579
11580/// Result of walking a field's Rust type. `kind` is the underlying
11581/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11582/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11583/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11584/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11585#[derive(Clone, Copy)]
11586struct DetectedType<'a> {
11587 kind: DetectedKind,
11588 nullable: bool,
11589 auto: bool,
11590 fk_inner: Option<&'a syn::Type>,
11591}
11592
11593/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11594/// non-`Auto` types — the caller should already have routed Auto-only
11595/// codegen through this helper, so a `None` indicates a macro-internal
11596/// invariant break.
11597fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11598 let Type::Path(TypePath { path, qself: None }) = ty else {
11599 return None;
11600 };
11601 let last = path.segments.last()?;
11602 if last.ident != "Auto" {
11603 return None;
11604 }
11605 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11606 return None;
11607 };
11608 args.args.iter().find_map(|a| match a {
11609 syn::GenericArgument::Type(t) => Some(t),
11610 _ => None,
11611 })
11612}
11613
11614fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11615 let Type::Path(TypePath { path, qself: None }) = ty else {
11616 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11617 };
11618 let last = path
11619 .segments
11620 .last()
11621 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11622
11623 if last.ident == "Option" {
11624 let inner = generic_inner(ty, &last.arguments, "Option")?;
11625 let inner_det = detect_type(inner)?;
11626 if inner_det.nullable {
11627 return Err(syn::Error::new_spanned(
11628 ty,
11629 "nested Option is not supported",
11630 ));
11631 }
11632 if inner_det.auto {
11633 return Err(syn::Error::new_spanned(
11634 ty,
11635 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11636 ));
11637 }
11638 return Ok(DetectedType {
11639 nullable: true,
11640 ..inner_det
11641 });
11642 }
11643
11644 if last.ident == "Auto" {
11645 let inner = generic_inner(ty, &last.arguments, "Auto")?;
11646 let inner_det = detect_type(inner)?;
11647 if inner_det.auto {
11648 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11649 }
11650 if inner_det.nullable {
11651 return Err(syn::Error::new_spanned(
11652 ty,
11653 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11654 ));
11655 }
11656 if inner_det.fk_inner.is_some() {
11657 return Err(syn::Error::new_spanned(
11658 ty,
11659 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11660 ));
11661 }
11662 if !matches!(
11663 inner_det.kind,
11664 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11665 ) {
11666 return Err(syn::Error::new_spanned(
11667 ty,
11668 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11669 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11670 (DEFAULT now())",
11671 ));
11672 }
11673 return Ok(DetectedType {
11674 auto: true,
11675 ..inner_det
11676 });
11677 }
11678
11679 if last.ident == "ForeignKey" {
11680 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11681 // Resolve the FK column's underlying SQL type from `K`. When the
11682 // user wrote `ForeignKey<T>` without a key parameter, the type
11683 // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11684 // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11685 // recurse into K so the column DDL emits the right SQL type
11686 // (VARCHAR for String, UUID for Uuid, …) and the load_related
11687 // emitter knows which `SqlValue` variant to match.
11688 let kind = match key_ty {
11689 Some(k) => detect_type(k)?.kind,
11690 None => DetectedKind::I64,
11691 };
11692 return Ok(DetectedType {
11693 kind,
11694 nullable: false,
11695 auto: false,
11696 fk_inner: Some(inner),
11697 });
11698 }
11699
11700 let kind = match last.ident.to_string().as_str() {
11701 "i16" => DetectedKind::I16,
11702 "i32" => DetectedKind::I32,
11703 "i64" => DetectedKind::I64,
11704 "f32" => DetectedKind::F32,
11705 "f64" => DetectedKind::F64,
11706 "bool" => DetectedKind::Bool,
11707 "String" => DetectedKind::String,
11708 "DateTime" => DetectedKind::DateTime,
11709 "NaiveDate" => DetectedKind::Date,
11710 "NaiveTime" => DetectedKind::Time,
11711 "Uuid" => DetectedKind::Uuid,
11712 "Value" => DetectedKind::Json,
11713 "Decimal" => DetectedKind::Decimal,
11714 // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11715 // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11716 // — that would emit Binary DDL and decode-fail at runtime.
11717 "Vec" => {
11718 let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11719 if let Type::Path(TypePath { path, qself: None }) = inner {
11720 if let Some(seg) = path.segments.last() {
11721 if seg.ident == "u8" && seg.arguments.is_empty() {
11722 return Ok(DetectedType {
11723 kind: DetectedKind::Binary,
11724 nullable: false,
11725 auto: false,
11726 fk_inner: None,
11727 });
11728 }
11729 }
11730 }
11731 return Err(syn::Error::new_spanned(
11732 ty,
11733 "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11734 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11735 ));
11736 }
11737 // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11738 // `integer[]` / `bigint[]` (Django `ArrayField`, #341).
11739 "Array" => {
11740 let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11741 let elem = match inner {
11742 Type::Path(TypePath { path, qself: None }) => {
11743 path.segments.last().map(|s| s.ident.to_string())
11744 }
11745 _ => None,
11746 };
11747 let kind = match elem.as_deref() {
11748 Some("String") => DetectedKind::ArrayText,
11749 Some("i32") => DetectedKind::ArrayInt,
11750 Some("i64") => DetectedKind::ArrayBigInt,
11751 _ => {
11752 return Err(syn::Error::new_spanned(
11753 ty,
11754 "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
11755 `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
11756 ));
11757 }
11758 };
11759 return Ok(DetectedType {
11760 kind,
11761 nullable: false,
11762 auto: false,
11763 fk_inner: None,
11764 });
11765 }
11766 // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
11767 // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
11768 // `int8range` / `numrange` / `daterange` / `tstzrange` (Django
11769 // `RangeField` family, #343).
11770 "Range" => {
11771 let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
11772 let elem = match inner {
11773 Type::Path(TypePath { path, qself: None }) => {
11774 path.segments.last().map(|s| s.ident.to_string())
11775 }
11776 _ => None,
11777 };
11778 let kind = match elem.as_deref() {
11779 Some("i32") => DetectedKind::RangeInt,
11780 Some("i64") => DetectedKind::RangeBigInt,
11781 Some("Decimal") => DetectedKind::RangeNumeric,
11782 Some("NaiveDate") => DetectedKind::RangeDate,
11783 Some("DateTime") => DetectedKind::RangeDateTime,
11784 _ => {
11785 return Err(syn::Error::new_spanned(
11786 ty,
11787 "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
11788 `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
11789 `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
11790 (→ tstzrange) are supported (#343)",
11791 ));
11792 }
11793 };
11794 return Ok(DetectedType {
11795 kind,
11796 nullable: false,
11797 auto: false,
11798 fk_inner: None,
11799 });
11800 }
11801 // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
11802 // (the `CastValue` impl bridges logical↔stored); the field's own
11803 // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
11804 // schema just needs `FieldType::String`.
11805 "Cast" => {
11806 return Ok(DetectedType {
11807 kind: DetectedKind::String,
11808 nullable: false,
11809 auto: false,
11810 fk_inner: None,
11811 });
11812 }
11813 // `HStore` → PG `hstore` (Django `HStoreField`, #342). No generic
11814 // parameter — always a string→string map.
11815 "HStore" => {
11816 return Ok(DetectedType {
11817 kind: DetectedKind::HStore,
11818 nullable: false,
11819 auto: false,
11820 fk_inner: None,
11821 });
11822 }
11823 // `Vector` → pgvector `vector(N)` (#824). The dimension is
11824 // supplied by `#[rustango(vector(dims = N))]`, not the type.
11825 "Vector" => {
11826 return Ok(DetectedType {
11827 kind: DetectedKind::Vector,
11828 nullable: false,
11829 auto: false,
11830 fk_inner: None,
11831 });
11832 }
11833 // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
11834 // supplied by `#[rustango(geometry(srid = N))]`, not the type.
11835 "Point" => {
11836 return Ok(DetectedType {
11837 kind: DetectedKind::Geometry,
11838 nullable: false,
11839 auto: false,
11840 fk_inner: None,
11841 });
11842 }
11843 other => {
11844 return Err(syn::Error::new_spanned(
11845 ty,
11846 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)"),
11847 ));
11848 }
11849 };
11850 Ok(DetectedType {
11851 kind,
11852 nullable: false,
11853 auto: false,
11854 fk_inner: None,
11855 })
11856}
11857
11858fn generic_inner<'a>(
11859 ty: &'a Type,
11860 arguments: &'a PathArguments,
11861 wrapper: &str,
11862) -> syn::Result<&'a Type> {
11863 let PathArguments::AngleBracketed(args) = arguments else {
11864 return Err(syn::Error::new_spanned(
11865 ty,
11866 format!("{wrapper} requires a generic argument"),
11867 ));
11868 };
11869 args.args
11870 .iter()
11871 .find_map(|a| match a {
11872 GenericArgument::Type(t) => Some(t),
11873 _ => None,
11874 })
11875 .ok_or_else(|| {
11876 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11877 })
11878}
11879
11880/// Like [`generic_inner`] but pulls *two* type args — the first is
11881/// required, the second is optional. Used by the `ForeignKey<T, K>`
11882/// detection where K defaults to `i64` when omitted.
11883fn generic_pair<'a>(
11884 ty: &'a Type,
11885 arguments: &'a PathArguments,
11886 wrapper: &str,
11887) -> syn::Result<(&'a Type, Option<&'a Type>)> {
11888 let PathArguments::AngleBracketed(args) = arguments else {
11889 return Err(syn::Error::new_spanned(
11890 ty,
11891 format!("{wrapper} requires a generic argument"),
11892 ));
11893 };
11894 let mut types = args.args.iter().filter_map(|a| match a {
11895 GenericArgument::Type(t) => Some(t),
11896 _ => None,
11897 });
11898 let first = types.next().ok_or_else(|| {
11899 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11900 })?;
11901 let second = types.next();
11902 Ok((first, second))
11903}
11904
11905fn to_snake_case(s: &str) -> String {
11906 let mut out = String::with_capacity(s.len() + 4);
11907 for (i, ch) in s.chars().enumerate() {
11908 if ch.is_ascii_uppercase() {
11909 if i > 0 {
11910 out.push('_');
11911 }
11912 out.push(ch.to_ascii_lowercase());
11913 } else {
11914 out.push(ch);
11915 }
11916 }
11917 out
11918}
11919
11920// ============================================================
11921// #[derive(Form)] — slice 8.4B
11922// ============================================================
11923
11924/// Per-field `#[form(...)]` attributes recognised by the derive.
11925#[derive(Default)]
11926struct FormFieldAttrs {
11927 min: Option<i64>,
11928 max: Option<i64>,
11929 min_length: Option<u32>,
11930 max_length: Option<u32>,
11931 /// `#[form(clean = "fn_name")]` — Django-shape `clean_<field>` hook.
11932 /// The named static method on the form struct is called after the
11933 /// field's typed parse + length/range checks; it gets the parsed
11934 /// value by reference and returns `Result<<FieldType>, String>`.
11935 /// On Ok, the returned value replaces the parsed one; on Err, the
11936 /// message is attached to the field error list. Issue #372.
11937 clean: Option<syn::Ident>,
11938}
11939
11940/// Container-level `#[form(...)]` attributes. Currently only the
11941/// Django-shape cross-field `validate` hook (issue #373).
11942#[derive(Default)]
11943struct FormContainerAttrs {
11944 /// `#[form(validate = "fn_name")]` — Django-shape `clean()` hook.
11945 /// After every per-field parse succeeds, the named method on the
11946 /// form struct is called with `&self` and may return
11947 /// `Result<(), FormErrors>`. Errors merge into the field error
11948 /// list. Issue #373.
11949 validate: Option<syn::Ident>,
11950}
11951
11952/// Detected shape of a form field's Rust type.
11953#[derive(Clone, Copy)]
11954enum FormFieldKind {
11955 String,
11956 I16,
11957 I32,
11958 I64,
11959 F32,
11960 F64,
11961 Bool,
11962}
11963
11964impl FormFieldKind {
11965 fn parse_method(self) -> &'static str {
11966 match self {
11967 Self::I16 => "i16",
11968 Self::I32 => "i32",
11969 Self::I64 => "i64",
11970 Self::F32 => "f32",
11971 Self::F64 => "f64",
11972 // String + Bool don't go through `str::parse`; the codegen
11973 // handles them inline.
11974 Self::String | Self::Bool => "",
11975 }
11976 }
11977}
11978
11979fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
11980 let root = rustango_root();
11981 let struct_name = &input.ident;
11982
11983 let Data::Struct(data) = &input.data else {
11984 return Err(syn::Error::new_spanned(
11985 struct_name,
11986 "Form can only be derived on structs",
11987 ));
11988 };
11989 let Fields::Named(named) = &data.fields else {
11990 return Err(syn::Error::new_spanned(
11991 struct_name,
11992 "Form requires a struct with named fields",
11993 ));
11994 };
11995
11996 // #373 — container-level `#[form(validate = "fn")]` hook.
11997 let container = parse_form_container_attrs(input)?;
11998 let post_field_clean: Vec<TokenStream2> = Vec::new();
11999 let _ = post_field_clean;
12000
12001 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12002 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12003
12004 for field in &named.named {
12005 let ident = field
12006 .ident
12007 .as_ref()
12008 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12009 let attrs = parse_form_field_attrs(field)?;
12010 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12011
12012 let name_lit = ident.to_string();
12013 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12014 // #372 — append the per-field `clean_<field>` call right after
12015 // the parse block when the attribute is set. The clean fn
12016 // takes &T and returns Result<T, String>; on Err we attach
12017 // the message to the field error list without aborting
12018 // (matches Django's "collect all field errors" shape).
12019 let clean_block = if let Some(clean_fn) = &attrs.clean {
12020 quote! {
12021 if __errors.fields().get(#name_lit).is_none() {
12022 match Self::#clean_fn(&#ident) {
12023 ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
12024 ::core::result::Result::Err(__msg) => {
12025 __errors.add(#name_lit, __msg);
12026 }
12027 }
12028 }
12029 }
12030 } else {
12031 quote! {}
12032 };
12033 field_blocks.push(quote! {
12034 #parse_block
12035 #clean_block
12036 });
12037 field_idents.push(ident);
12038 }
12039
12040 // #373 — after every per-field parse + clean succeeds, call the
12041 // cross-field validator if declared. Errors merge into the
12042 // outgoing FormErrors via the existing `FormErrors::merge` helper
12043 // (same primitive the DRF serializer cross-field hook uses).
12044 let cross_field_call = if let Some(validate_fn) = &container.validate {
12045 quote! {
12046 if __errors.is_empty() {
12047 let __candidate = Self { #( #field_idents ),* };
12048 if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12049 __errors.merge(__other);
12050 }
12051 if !__errors.is_empty() {
12052 return ::core::result::Result::Err(__errors);
12053 }
12054 return ::core::result::Result::Ok(__candidate);
12055 }
12056 }
12057 } else {
12058 quote! {}
12059 };
12060
12061 Ok(quote! {
12062 impl #root::forms::Form for #struct_name {
12063 fn parse(
12064 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12065 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12066 let mut __errors = #root::forms::FormErrors::default();
12067 #( #field_blocks )*
12068 #cross_field_call
12069 if !__errors.is_empty() {
12070 return ::core::result::Result::Err(__errors);
12071 }
12072 ::core::result::Result::Ok(Self {
12073 #( #field_idents ),*
12074 })
12075 }
12076 }
12077 })
12078}
12079
12080fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12081 let mut out = FormContainerAttrs::default();
12082 for attr in &input.attrs {
12083 if !attr.path().is_ident("form") {
12084 continue;
12085 }
12086 attr.parse_nested_meta(|meta| {
12087 if meta.path.is_ident("validate") {
12088 let s: LitStr = meta.value()?.parse()?;
12089 out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12090 return Ok(());
12091 }
12092 Err(meta.error("unknown form container attribute (supported: `validate`)"))
12093 })?;
12094 }
12095 Ok(out)
12096}
12097
12098fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12099 let mut out = FormFieldAttrs::default();
12100 for attr in &field.attrs {
12101 if !attr.path().is_ident("form") {
12102 continue;
12103 }
12104 attr.parse_nested_meta(|meta| {
12105 if meta.path.is_ident("min") {
12106 let lit: syn::LitInt = meta.value()?.parse()?;
12107 out.min = Some(lit.base10_parse::<i64>()?);
12108 return Ok(());
12109 }
12110 if meta.path.is_ident("max") {
12111 let lit: syn::LitInt = meta.value()?.parse()?;
12112 out.max = Some(lit.base10_parse::<i64>()?);
12113 return Ok(());
12114 }
12115 if meta.path.is_ident("min_length") {
12116 let lit: syn::LitInt = meta.value()?.parse()?;
12117 out.min_length = Some(lit.base10_parse::<u32>()?);
12118 return Ok(());
12119 }
12120 if meta.path.is_ident("max_length") {
12121 let lit: syn::LitInt = meta.value()?.parse()?;
12122 out.max_length = Some(lit.base10_parse::<u32>()?);
12123 return Ok(());
12124 }
12125 if meta.path.is_ident("clean") {
12126 let s: LitStr = meta.value()?.parse()?;
12127 out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12128 return Ok(());
12129 }
12130 Err(meta.error(
12131 "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12132 ))
12133 })?;
12134 }
12135 Ok(out)
12136}
12137
12138fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12139 let Type::Path(TypePath { path, qself: None }) = ty else {
12140 return Err(syn::Error::new(
12141 span,
12142 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12143 ));
12144 };
12145 let last = path
12146 .segments
12147 .last()
12148 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12149
12150 if last.ident == "Option" {
12151 let inner = generic_inner(ty, &last.arguments, "Option")?;
12152 let (kind, nested) = detect_form_field(inner, span)?;
12153 if nested {
12154 return Err(syn::Error::new(
12155 span,
12156 "nested Option in Form fields is not supported",
12157 ));
12158 }
12159 return Ok((kind, true));
12160 }
12161
12162 let kind = match last.ident.to_string().as_str() {
12163 "String" => FormFieldKind::String,
12164 "i16" => FormFieldKind::I16,
12165 "i32" => FormFieldKind::I32,
12166 "i64" => FormFieldKind::I64,
12167 "f32" => FormFieldKind::F32,
12168 "f64" => FormFieldKind::F64,
12169 "bool" => FormFieldKind::Bool,
12170 other => {
12171 return Err(syn::Error::new(
12172 span,
12173 format!(
12174 "Form field type `{other}` is not supported in v0.8 — use String / \
12175 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12176 ),
12177 ));
12178 }
12179 };
12180 Ok((kind, false))
12181}
12182
12183#[allow(clippy::too_many_lines)]
12184fn render_form_field_parse(
12185 ident: &syn::Ident,
12186 name_lit: &str,
12187 kind: FormFieldKind,
12188 nullable: bool,
12189 attrs: &FormFieldAttrs,
12190) -> TokenStream2 {
12191 // Pull the raw &str from the payload. Uses variable name `data` to
12192 // match the new `Form::parse(data: &HashMap<…>)` signature.
12193 let lookup = quote! {
12194 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12195 };
12196
12197 let parsed_value = match kind {
12198 FormFieldKind::Bool => quote! {
12199 let __v: bool = match __raw {
12200 ::core::option::Option::None => false,
12201 ::core::option::Option::Some(__s) => !matches!(
12202 __s.to_ascii_lowercase().as_str(),
12203 "" | "false" | "0" | "off" | "no"
12204 ),
12205 };
12206 },
12207 FormFieldKind::String => {
12208 if nullable {
12209 quote! {
12210 let __v: ::core::option::Option<::std::string::String> = match __raw {
12211 ::core::option::Option::None => ::core::option::Option::None,
12212 ::core::option::Option::Some(__s) if __s.is_empty() => {
12213 ::core::option::Option::None
12214 }
12215 ::core::option::Option::Some(__s) => {
12216 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12217 }
12218 };
12219 }
12220 } else {
12221 quote! {
12222 let __v: ::std::string::String = match __raw {
12223 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12224 ::core::clone::Clone::clone(__s)
12225 }
12226 _ => {
12227 __errors.add(#name_lit, "This field is required.");
12228 ::std::string::String::new()
12229 }
12230 };
12231 }
12232 }
12233 }
12234 FormFieldKind::I16
12235 | FormFieldKind::I32
12236 | FormFieldKind::I64
12237 | FormFieldKind::F32
12238 | FormFieldKind::F64 => {
12239 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12240 let ty_lit = kind.parse_method();
12241 let default_val = match kind {
12242 FormFieldKind::I16 => quote! { 0i16 },
12243 FormFieldKind::I32 => quote! { 0i32 },
12244 FormFieldKind::I64 => quote! { 0i64 },
12245 FormFieldKind::F32 => quote! { 0f32 },
12246 FormFieldKind::F64 => quote! { 0f64 },
12247 _ => quote! { Default::default() },
12248 };
12249 if nullable {
12250 quote! {
12251 let __v: ::core::option::Option<#parse_ty> = match __raw {
12252 ::core::option::Option::None => ::core::option::Option::None,
12253 ::core::option::Option::Some(__s) if __s.is_empty() => {
12254 ::core::option::Option::None
12255 }
12256 ::core::option::Option::Some(__s) => {
12257 match __s.parse::<#parse_ty>() {
12258 ::core::result::Result::Ok(__n) => {
12259 ::core::option::Option::Some(__n)
12260 }
12261 ::core::result::Result::Err(__e) => {
12262 __errors.add(
12263 #name_lit,
12264 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12265 );
12266 ::core::option::Option::None
12267 }
12268 }
12269 }
12270 };
12271 }
12272 } else {
12273 quote! {
12274 let __v: #parse_ty = match __raw {
12275 ::core::option::Option::Some(__s) if !__s.is_empty() => {
12276 match __s.parse::<#parse_ty>() {
12277 ::core::result::Result::Ok(__n) => __n,
12278 ::core::result::Result::Err(__e) => {
12279 __errors.add(
12280 #name_lit,
12281 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12282 );
12283 #default_val
12284 }
12285 }
12286 }
12287 _ => {
12288 __errors.add(#name_lit, "This field is required.");
12289 #default_val
12290 }
12291 };
12292 }
12293 }
12294 }
12295 };
12296
12297 let validators = render_form_validators(name_lit, kind, nullable, attrs);
12298
12299 quote! {
12300 // `mut` so the per-field `clean` hook (#372) can rewrite the
12301 // parsed value in-place when it returns Ok with a normalized
12302 // form (e.g. trim / lowercase).
12303 let mut #ident = {
12304 #lookup
12305 #parsed_value
12306 #validators
12307 __v
12308 };
12309 }
12310}
12311
12312fn render_form_validators(
12313 name_lit: &str,
12314 kind: FormFieldKind,
12315 nullable: bool,
12316 attrs: &FormFieldAttrs,
12317) -> TokenStream2 {
12318 let mut checks: Vec<TokenStream2> = Vec::new();
12319
12320 let val_ref = if nullable {
12321 quote! { __v.as_ref() }
12322 } else {
12323 quote! { ::core::option::Option::Some(&__v) }
12324 };
12325
12326 let is_string = matches!(kind, FormFieldKind::String);
12327 let is_numeric = matches!(
12328 kind,
12329 FormFieldKind::I16
12330 | FormFieldKind::I32
12331 | FormFieldKind::I64
12332 | FormFieldKind::F32
12333 | FormFieldKind::F64
12334 );
12335
12336 if is_string {
12337 if let Some(min_len) = attrs.min_length {
12338 let min_len_usize = min_len as usize;
12339 checks.push(quote! {
12340 if let ::core::option::Option::Some(__s) = #val_ref {
12341 if __s.len() < #min_len_usize {
12342 __errors.add(
12343 #name_lit,
12344 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12345 );
12346 }
12347 }
12348 });
12349 }
12350 if let Some(max_len) = attrs.max_length {
12351 let max_len_usize = max_len as usize;
12352 checks.push(quote! {
12353 if let ::core::option::Option::Some(__s) = #val_ref {
12354 if __s.len() > #max_len_usize {
12355 __errors.add(
12356 #name_lit,
12357 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12358 );
12359 }
12360 }
12361 });
12362 }
12363 }
12364
12365 if is_numeric {
12366 if let Some(min) = attrs.min {
12367 checks.push(quote! {
12368 if let ::core::option::Option::Some(__n) = #val_ref {
12369 if (*__n as f64) < (#min as f64) {
12370 __errors.add(
12371 #name_lit,
12372 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12373 );
12374 }
12375 }
12376 });
12377 }
12378 if let Some(max) = attrs.max {
12379 checks.push(quote! {
12380 if let ::core::option::Option::Some(__n) = #val_ref {
12381 if (*__n as f64) > (#max as f64) {
12382 __errors.add(
12383 #name_lit,
12384 ::std::format!("Ensure this value is less than or equal to {}.", #max),
12385 );
12386 }
12387 }
12388 });
12389 }
12390 }
12391
12392 quote! { #( #checks )* }
12393}
12394
12395// ============================================================
12396// #[derive(ViewSet)]
12397// ============================================================
12398
12399struct ViewSetAttrs {
12400 model: syn::Path,
12401 fields: Option<Vec<String>>,
12402 filter_fields: Vec<String>,
12403 search_fields: Vec<String>,
12404 /// (field_name, desc)
12405 ordering: Vec<(String, bool)>,
12406 page_size: Option<usize>,
12407 read_only: bool,
12408 perms: ViewSetPermsAttrs,
12409 /// `#[viewset(serializer = SomeSerializer)]` — render list /
12410 /// retrieve / create responses through this `#[derive(Serializer)]`
12411 /// type instead of the default field-level projection (requires the
12412 /// `serializer` feature). Tri-dialect.
12413 serializer: Option<syn::Path>,
12414}
12415
12416#[derive(Default)]
12417struct ViewSetPermsAttrs {
12418 list: Vec<String>,
12419 retrieve: Vec<String>,
12420 create: Vec<String>,
12421 update: Vec<String>,
12422 destroy: Vec<String>,
12423}
12424
12425fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12426 let root = rustango_root();
12427 let struct_name = &input.ident;
12428
12429 // Must be a unit struct or an empty named struct.
12430 match &input.data {
12431 Data::Struct(s) => match &s.fields {
12432 Fields::Unit | Fields::Named(_) => {}
12433 Fields::Unnamed(_) => {
12434 return Err(syn::Error::new_spanned(
12435 struct_name,
12436 "ViewSet can only be derived on a unit struct or an empty named struct",
12437 ));
12438 }
12439 },
12440 _ => {
12441 return Err(syn::Error::new_spanned(
12442 struct_name,
12443 "ViewSet can only be derived on a struct",
12444 ));
12445 }
12446 }
12447
12448 let attrs = parse_viewset_attrs(input)?;
12449 let model_path = &attrs.model;
12450
12451 // `.fields(&[...])` call — None means skip (use all scalar fields).
12452 let fields_call = if let Some(ref fields) = attrs.fields {
12453 let lits = fields.iter().map(|f| f.as_str());
12454 quote!(.fields(&[ #(#lits),* ]))
12455 } else {
12456 quote!()
12457 };
12458
12459 let filter_fields_call = if attrs.filter_fields.is_empty() {
12460 quote!()
12461 } else {
12462 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12463 quote!(.filter_fields(&[ #(#lits),* ]))
12464 };
12465
12466 let search_fields_call = if attrs.search_fields.is_empty() {
12467 quote!()
12468 } else {
12469 let lits = attrs.search_fields.iter().map(|f| f.as_str());
12470 quote!(.search_fields(&[ #(#lits),* ]))
12471 };
12472
12473 let ordering_call = if attrs.ordering.is_empty() {
12474 quote!()
12475 } else {
12476 let pairs = attrs.ordering.iter().map(|(f, desc)| {
12477 let f = f.as_str();
12478 quote!((#f, #desc))
12479 });
12480 quote!(.ordering(&[ #(#pairs),* ]))
12481 };
12482
12483 let page_size_call = if let Some(n) = attrs.page_size {
12484 quote!(.page_size(#n))
12485 } else {
12486 quote!()
12487 };
12488
12489 let read_only_call = if attrs.read_only {
12490 quote!(.read_only())
12491 } else {
12492 quote!()
12493 };
12494
12495 // `.serializer::<S>()` — reshape responses through a derived
12496 // serializer. Requires the downstream crate to enable the
12497 // `serializer` feature (the method is gated on it).
12498 let serializer_call = if let Some(ref ser) = attrs.serializer {
12499 quote!(.serializer::<#ser>())
12500 } else {
12501 quote!()
12502 };
12503
12504 let perms = &attrs.perms;
12505 let perms_call = if perms.list.is_empty()
12506 && perms.retrieve.is_empty()
12507 && perms.create.is_empty()
12508 && perms.update.is_empty()
12509 && perms.destroy.is_empty()
12510 {
12511 quote!()
12512 } else {
12513 let list_lits = perms.list.iter().map(|s| s.as_str());
12514 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12515 let create_lits = perms.create.iter().map(|s| s.as_str());
12516 let update_lits = perms.update.iter().map(|s| s.as_str());
12517 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12518 quote! {
12519 .permissions(#root::viewset::ViewSetPerms {
12520 list: ::std::vec![ #(#list_lits.to_owned()),* ],
12521 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12522 create: ::std::vec![ #(#create_lits.to_owned()),* ],
12523 update: ::std::vec![ #(#update_lits.to_owned()),* ],
12524 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
12525 })
12526 }
12527 };
12528
12529 Ok(quote! {
12530 impl #struct_name {
12531 /// Build an `axum::Router` with the six standard REST endpoints
12532 /// for this ViewSet, mounted at `prefix`.
12533 ///
12534 /// Accepts any backend's pool — a `sqlx::PgPool`,
12535 /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
12536 /// enum — because they all `Into<Pool>` (#1273). This is why
12537 /// the derive no longer names `PgPool`, which would not
12538 /// exist on a project built without the `postgres` feature.
12539 pub fn router(
12540 prefix: &str,
12541 pool: impl ::core::convert::Into<#root::sql::Pool>,
12542 ) -> #root::__axum::Router {
12543 #root::viewset::ViewSet::for_model(
12544 <#model_path as #root::core::Model>::SCHEMA
12545 )
12546 #fields_call
12547 #filter_fields_call
12548 #search_fields_call
12549 #ordering_call
12550 #page_size_call
12551 #perms_call
12552 #read_only_call
12553 #serializer_call
12554 .router_pool(prefix, pool.into())
12555 }
12556 }
12557 })
12558}
12559
12560fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
12561 let mut model: Option<syn::Path> = None;
12562 let mut fields: Option<Vec<String>> = None;
12563 let mut filter_fields: Vec<String> = Vec::new();
12564 let mut search_fields: Vec<String> = Vec::new();
12565 let mut ordering: Vec<(String, bool)> = Vec::new();
12566 let mut page_size: Option<usize> = None;
12567 let mut read_only = false;
12568 let mut perms = ViewSetPermsAttrs::default();
12569 let mut serializer: Option<syn::Path> = None;
12570
12571 for attr in &input.attrs {
12572 if !attr.path().is_ident("viewset") {
12573 continue;
12574 }
12575 attr.parse_nested_meta(|meta| {
12576 if meta.path.is_ident("model") {
12577 let path: syn::Path = meta.value()?.parse()?;
12578 model = Some(path);
12579 return Ok(());
12580 }
12581 if meta.path.is_ident("serializer") {
12582 let path: syn::Path = meta.value()?.parse()?;
12583 serializer = Some(path);
12584 return Ok(());
12585 }
12586 if meta.path.is_ident("fields") {
12587 let s: LitStr = meta.value()?.parse()?;
12588 fields = Some(split_field_list(&s.value()));
12589 return Ok(());
12590 }
12591 if meta.path.is_ident("filter_fields") {
12592 let s: LitStr = meta.value()?.parse()?;
12593 filter_fields = split_field_list(&s.value());
12594 return Ok(());
12595 }
12596 if meta.path.is_ident("search_fields") {
12597 let s: LitStr = meta.value()?.parse()?;
12598 search_fields = split_field_list(&s.value());
12599 return Ok(());
12600 }
12601 if meta.path.is_ident("ordering") {
12602 let s: LitStr = meta.value()?.parse()?;
12603 ordering = parse_ordering_list(&s.value());
12604 return Ok(());
12605 }
12606 if meta.path.is_ident("page_size") {
12607 let lit: syn::LitInt = meta.value()?.parse()?;
12608 page_size = Some(lit.base10_parse::<usize>()?);
12609 return Ok(());
12610 }
12611 if meta.path.is_ident("read_only") {
12612 read_only = true;
12613 return Ok(());
12614 }
12615 if meta.path.is_ident("permissions") {
12616 meta.parse_nested_meta(|inner| {
12617 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
12618 let s: LitStr = inner.value()?.parse()?;
12619 Ok(split_field_list(&s.value()))
12620 };
12621 if inner.path.is_ident("list") {
12622 perms.list = parse_codenames(&inner)?;
12623 } else if inner.path.is_ident("retrieve") {
12624 perms.retrieve = parse_codenames(&inner)?;
12625 } else if inner.path.is_ident("create") {
12626 perms.create = parse_codenames(&inner)?;
12627 } else if inner.path.is_ident("update") {
12628 perms.update = parse_codenames(&inner)?;
12629 } else if inner.path.is_ident("destroy") {
12630 perms.destroy = parse_codenames(&inner)?;
12631 } else {
12632 return Err(inner.error(
12633 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
12634 ));
12635 }
12636 Ok(())
12637 })?;
12638 return Ok(());
12639 }
12640 Err(meta.error(
12641 "unknown viewset attribute (supported: model, fields, filter_fields, \
12642 search_fields, ordering, page_size, read_only, serializer, permissions(...))",
12643 ))
12644 })?;
12645 }
12646
12647 let model = model.ok_or_else(|| {
12648 syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
12649 })?;
12650
12651 Ok(ViewSetAttrs {
12652 model,
12653 fields,
12654 filter_fields,
12655 search_fields,
12656 ordering,
12657 page_size,
12658 read_only,
12659 perms,
12660 serializer,
12661 })
12662}
12663
12664// ============================================================ #[derive(Serializer)]
12665
12666struct SerializerContainerAttrs {
12667 model: syn::Path,
12668 /// `#[serializer(validate = "fn_name")]` on the struct — DRF-shape
12669 /// cross-field validation hook (#436). The named inherent method
12670 /// must take `&self` and return
12671 /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
12672 /// `validate()` runs every per-field validator first; then calls
12673 /// the cross-field method if declared; aggregates all errors into
12674 /// one `FormErrors`.
12675 cross_validate: Option<syn::Ident>,
12676}
12677
12678#[derive(Default)]
12679struct SerializerFieldAttrs {
12680 read_only: bool,
12681 write_only: bool,
12682 source: Option<String>,
12683 skip: bool,
12684 /// `#[serializer(method = "fn_name")]` — DRF SerializerMethodField
12685 /// analog. The macro emits `from_model` initializer that calls
12686 /// `Self::fn_name(&model)` and stores the return value.
12687 method: Option<String>,
12688 /// `#[serializer(validate = "fn_name")]` — per-field validator
12689 /// callable run by `Self::validate(&self)`. Must return
12690 /// `Result<(), String>`. Errors land in `FormErrors` keyed by
12691 /// the field name.
12692 validate: Option<String>,
12693 /// `#[serializer(nested)]` on a field whose type is another
12694 /// `Serializer` — the macro emits `from_model` initializer that
12695 /// reads the parent via `model.<source>.value()` then calls the
12696 /// child serializer's `from_model(parent)`. When the FK is
12697 /// unloaded the field falls back to `Default::default()` (does
12698 /// NOT panic) so a missing prefetch in prod degrades gracefully.
12699 /// Source field on the model defaults to the field name; override
12700 /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
12701 /// panic-on-unloaded behavior for tests.
12702 nested: bool,
12703 /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
12704 /// strict behavior: panic when the FK isn't loaded. Useful in
12705 /// test code where forgetting select_related must trip a hard
12706 /// failure rather than render a blank nested object.
12707 nested_strict: bool,
12708 /// `#[serializer(many = TagSerializer)]` — declare the field as
12709 /// a list of nested serializers. Field type must be `Vec<S>`
12710 /// where `S` is the inner serializer. The macro initializes the
12711 /// field to `Vec::new()` in `from_model` and emits a typed
12712 /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
12713 /// maps each model row through `S::from_model`. Auto-load isn't
12714 /// possible (the M2M / one-to-many accessor is async); callers
12715 /// fetch the children + call the setter post-from_model.
12716 many: Option<syn::Type>,
12717 /// `#[serializer(slug = "name")]` — DRF `SlugRelatedField` analog.
12718 /// Source field on the model must be a `ForeignKey<T>`; the
12719 /// macro emits `from_model` glue that walks
12720 /// `model.<source>.value()?.<slug>` and clones it. Field type on
12721 /// the serializer is typically `String` (whatever type the slug
12722 /// column has). When the FK is unloaded the field falls back to
12723 /// `Default::default()`, same graceful-degrade contract as
12724 /// `nested`. Source defaults to the field name; override with
12725 /// `source = "..."`. v0.44.
12726 slug: Option<String>,
12727 /// `#[serializer(max_length = N)]` — DRF `MaxLengthValidator`. Caps
12728 /// the character count of a string field on write. Overrides the
12729 /// model's `max_length`; when absent the model value is inherited.
12730 max_length: Option<u64>,
12731 /// `#[serializer(min_length = N)]` — DRF `MinLengthValidator`.
12732 /// Serializer-only (the model has no `min_length` column).
12733 min_length: Option<u64>,
12734 /// `#[serializer(min = N)]` — DRF `MinValueValidator`. Inclusive
12735 /// integer lower bound; overrides the model's `min` when given.
12736 min: Option<i64>,
12737 /// `#[serializer(max = N)]` — DRF `MaxValueValidator`. Inclusive
12738 /// integer upper bound; overrides the model's `max` when given.
12739 max: Option<i64>,
12740}
12741
12742fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
12743 let mut model: Option<syn::Path> = None;
12744 let mut cross_validate: Option<syn::Ident> = None;
12745 for attr in &input.attrs {
12746 if !attr.path().is_ident("serializer") {
12747 continue;
12748 }
12749 attr.parse_nested_meta(|meta| {
12750 if meta.path.is_ident("model") {
12751 let _eq: syn::Token![=] = meta.input.parse()?;
12752 model = Some(meta.input.parse()?);
12753 return Ok(());
12754 }
12755 if meta.path.is_ident("validate") {
12756 // #436 — container-level `validate = "fn_name"` for the
12757 // DRF cross-field-validation shape. Field-level
12758 // `#[serializer(validate = "...")]` on a field is
12759 // parsed separately in `parse_serializer_field_attrs`.
12760 let s: LitStr = meta.value()?.parse()?;
12761 cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
12762 return Ok(());
12763 }
12764 Err(meta.error(
12765 "unknown serializer container attribute \
12766 (supported: `model`, `validate`)",
12767 ))
12768 })?;
12769 }
12770 let model = model.ok_or_else(|| {
12771 syn::Error::new_spanned(
12772 &input.ident,
12773 "`#[serializer(model = SomeModel)]` is required",
12774 )
12775 })?;
12776 Ok(SerializerContainerAttrs {
12777 model,
12778 cross_validate,
12779 })
12780}
12781
12782fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
12783 let mut out = SerializerFieldAttrs::default();
12784 for attr in &field.attrs {
12785 if !attr.path().is_ident("serializer") {
12786 continue;
12787 }
12788 attr.parse_nested_meta(|meta| {
12789 if meta.path.is_ident("read_only") {
12790 out.read_only = true;
12791 return Ok(());
12792 }
12793 if meta.path.is_ident("write_only") {
12794 out.write_only = true;
12795 return Ok(());
12796 }
12797 if meta.path.is_ident("skip") {
12798 out.skip = true;
12799 return Ok(());
12800 }
12801 if meta.path.is_ident("source") {
12802 let s: LitStr = meta.value()?.parse()?;
12803 out.source = Some(s.value());
12804 return Ok(());
12805 }
12806 if meta.path.is_ident("method") {
12807 let s: LitStr = meta.value()?.parse()?;
12808 out.method = Some(s.value());
12809 return Ok(());
12810 }
12811 if meta.path.is_ident("validate") {
12812 let s: LitStr = meta.value()?.parse()?;
12813 out.validate = Some(s.value());
12814 return Ok(());
12815 }
12816 if meta.path.is_ident("many") {
12817 let _eq: syn::Token![=] = meta.input.parse()?;
12818 out.many = Some(meta.input.parse()?);
12819 return Ok(());
12820 }
12821 if meta.path.is_ident("nested") {
12822 out.nested = true;
12823 // Optional strict flag inside parentheses:
12824 // #[serializer(nested(strict))]
12825 if meta.input.peek(syn::token::Paren) {
12826 meta.parse_nested_meta(|inner| {
12827 if inner.path.is_ident("strict") {
12828 out.nested_strict = true;
12829 return Ok(());
12830 }
12831 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
12832 })?;
12833 }
12834 return Ok(());
12835 }
12836 if meta.path.is_ident("slug") {
12837 let s: LitStr = meta.value()?.parse()?;
12838 out.slug = Some(s.value());
12839 return Ok(());
12840 }
12841 if meta.path.is_ident("max_length") {
12842 let lit: syn::LitInt = meta.value()?.parse()?;
12843 out.max_length = Some(lit.base10_parse::<u64>()?);
12844 return Ok(());
12845 }
12846 if meta.path.is_ident("min_length") {
12847 let lit: syn::LitInt = meta.value()?.parse()?;
12848 out.min_length = Some(lit.base10_parse::<u64>()?);
12849 return Ok(());
12850 }
12851 if meta.path.is_ident("min") {
12852 let lit: syn::LitInt = meta.value()?.parse()?;
12853 out.min = Some(lit.base10_parse::<i64>()?);
12854 return Ok(());
12855 }
12856 if meta.path.is_ident("max") {
12857 let lit: syn::LitInt = meta.value()?.parse()?;
12858 out.max = Some(lit.base10_parse::<i64>()?);
12859 return Ok(());
12860 }
12861 Err(meta.error(
12862 "unknown serializer field attribute (supported: \
12863 `read_only`, `write_only`, `source`, `skip`, `method`, \
12864 `validate`, `nested`, `many`, `slug`, `max_length`, \
12865 `min_length`, `min`, `max`)",
12866 ))
12867 })?;
12868 }
12869 // Validate: read_only + write_only is nonsensical
12870 if out.read_only && out.write_only {
12871 return Err(syn::Error::new_spanned(
12872 field,
12873 "a field cannot be both `read_only` and `write_only`",
12874 ));
12875 }
12876 if out.method.is_some() && out.source.is_some() {
12877 return Err(syn::Error::new_spanned(
12878 field,
12879 "`method` and `source` are mutually exclusive — `method` computes \
12880 the value from a method, `source` reads it from a different model field",
12881 ));
12882 }
12883 if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
12884 return Err(syn::Error::new_spanned(
12885 field,
12886 "`slug` is mutually exclusive with `method`, `nested`, and `many` \
12887 — pick one strategy for populating the field",
12888 ));
12889 }
12890 Ok(out)
12891}
12892
12893fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
12894 let root = rustango_root();
12895 let struct_name = &input.ident;
12896 let struct_name_lit = struct_name.to_string();
12897
12898 let Data::Struct(data) = &input.data else {
12899 return Err(syn::Error::new_spanned(
12900 struct_name,
12901 "Serializer can only be derived on structs",
12902 ));
12903 };
12904 let Fields::Named(named) = &data.fields else {
12905 return Err(syn::Error::new_spanned(
12906 struct_name,
12907 "Serializer requires a struct with named fields",
12908 ));
12909 };
12910
12911 let container = parse_serializer_container_attrs(input)?;
12912 let model_path = &container.model;
12913
12914 // Classify each field. `ty` is only consumed by the
12915 // `#[cfg(feature = "openapi")]` block below, but we always
12916 // capture it to keep the field-info build a single pass.
12917 #[allow(dead_code)]
12918 struct FieldInfo {
12919 ident: syn::Ident,
12920 ty: syn::Type,
12921 attrs: SerializerFieldAttrs,
12922 }
12923 let mut fields_info: Vec<FieldInfo> = Vec::new();
12924 for field in &named.named {
12925 let ident = field.ident.clone().expect("named field has ident");
12926 let attrs = parse_serializer_field_attrs(field)?;
12927 fields_info.push(FieldInfo {
12928 ident,
12929 ty: field.ty.clone(),
12930 attrs,
12931 });
12932 }
12933
12934 // Generate from_model body: struct literal with each field assigned.
12935 let from_model_fields = fields_info.iter().map(|fi| {
12936 let ident = &fi.ident;
12937 let ty = &fi.ty;
12938 if let Some(_inner) = &fi.attrs.many {
12939 // Many — collection field. Initialize empty; caller
12940 // populates via the macro-emitted set_<field> helper
12941 // after fetching the M2M children.
12942 quote! { #ident: ::std::vec::Vec::new() }
12943 } else if let Some(method) = &fi.attrs.method {
12944 // SerializerMethodField: call Self::<method>(&model) to
12945 // compute the value. Method signature must be
12946 // `fn <method>(model: &T) -> <field type>`.
12947 let method_ident = syn::Ident::new(method, ident.span());
12948 quote! { #ident: Self::#method_ident(model) }
12949 } else if let Some(slug_field) = &fi.attrs.slug {
12950 // v0.44 — SlugRelatedField. Source defaults to the field
12951 // name on this struct; override via `source = "..."`. The
12952 // source field on the model is expected to be a
12953 // `ForeignKey<T>`; the slug field on the parent is named
12954 // by the attribute value. When the FK is unloaded the
12955 // field falls back to `Default::default()` — same
12956 // graceful-degrade contract as `nested`.
12957 let src_name = fi
12958 .attrs
12959 .source
12960 .as_deref()
12961 .unwrap_or(&fi.ident.to_string())
12962 .to_owned();
12963 let src_ident = syn::Ident::new(&src_name, ident.span());
12964 let slug_ident = syn::Ident::new(slug_field, ident.span());
12965 quote! {
12966 #ident: match model.#src_ident.value() {
12967 ::core::option::Option::Some(__loaded) =>
12968 ::core::clone::Clone::clone(&__loaded.#slug_ident),
12969 ::core::option::Option::None =>
12970 ::core::default::Default::default(),
12971 }
12972 }
12973 } else if fi.attrs.nested {
12974 // Nested serializer. Source defaults to the field name on
12975 // this struct; override via `source = "..."`. The source
12976 // field on the model is expected to be a `ForeignKey<T>`
12977 // whose `.value()` returns `Option<&T>` after lazy-load.
12978 //
12979 // Behavior matrix (tweakable per-field):
12980 // * FK loaded → nested object materializes via
12981 // ChildSerializer::from_model(parent).
12982 // * FK unloaded → fall back to ChildSerializer::default()
12983 // (so prod doesn't crash on a missing
12984 // prefetch — just renders a blank nested
12985 // object). Add `#[serializer(nested,
12986 // strict)]` to keep the v0.18.1
12987 // panic-on-unloaded behavior for tests
12988 // that want hard guardrails.
12989 let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
12990 let src_ident = syn::Ident::new(&src_name, ident.span());
12991 if fi.attrs.nested_strict {
12992 let panic_msg = format!(
12993 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
12994 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
12995 );
12996 quote! {
12997 #ident: <#ty as #root::serializer::ModelSerializer>::from_model(
12998 model.#src_ident.value().expect(#panic_msg),
12999 )
13000 }
13001 } else {
13002 quote! {
13003 #ident: match model.#src_ident.value() {
13004 ::core::option::Option::Some(__loaded) =>
13005 <#ty as #root::serializer::ModelSerializer>::from_model(__loaded),
13006 ::core::option::Option::None =>
13007 ::core::default::Default::default(),
13008 }
13009 }
13010 }
13011 } else if fi.attrs.write_only || fi.attrs.skip {
13012 // Not read from model — use default
13013 quote! { #ident: ::core::default::Default::default() }
13014 } else if let Some(src) = &fi.attrs.source {
13015 let src_ident = syn::Ident::new(src, ident.span());
13016 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13017 } else {
13018 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13019 }
13020 });
13021
13022 // is_writable predicate (also used by writable_lits /
13023 // writable_source_lits below).
13024 let is_writable = |fi: &&FieldInfo| {
13025 !fi.attrs.read_only
13026 && !fi.attrs.skip
13027 && fi.attrs.method.is_none()
13028 && !fi.attrs.nested
13029 && fi.attrs.many.is_none()
13030 && fi.attrs.slug.is_none()
13031 };
13032
13033 // Declarative field constraints (DRF `validators=[...]`): one block
13034 // per writable field, run inside `validate()`. Each resolves its
13035 // bounds as the serializer attr when given (`#[serializer(max_length
13036 // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13037 // `max` / `choices`), then dispatches on the field's JSON value via
13038 // `forms::validators::check_value` (string → length/choices, integer
13039 // → min/max).
13040 let opt_usize = |v: Option<u64>| match v {
13041 Some(n) => {
13042 let n = n as usize;
13043 quote!(::core::option::Option::Some(#n))
13044 }
13045 None => quote!(::core::option::Option::None),
13046 };
13047 let opt_i64 = |v: Option<i64>| match v {
13048 Some(n) => quote!(::core::option::Option::Some(#n)),
13049 None => quote!(::core::option::Option::None),
13050 };
13051 let constraint_blocks: Vec<_> = fields_info
13052 .iter()
13053 .filter(is_writable)
13054 .map(|fi| {
13055 let ident = &fi.ident;
13056 let fname = ident.to_string();
13057 let mname = fi
13058 .attrs
13059 .source
13060 .clone()
13061 .unwrap_or_else(|| fi.ident.to_string());
13062 let attr_max_len = opt_usize(fi.attrs.max_length);
13063 let attr_min_len = opt_usize(fi.attrs.min_length);
13064 let attr_min = opt_i64(fi.attrs.min);
13065 let attr_max = opt_i64(fi.attrs.max);
13066 quote! {
13067 {
13068 let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13069 let __max_length: ::core::option::Option<usize> = #attr_max_len
13070 .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13071 let __min_length: ::core::option::Option<usize> = #attr_min_len;
13072 let __min: ::core::option::Option<i64> =
13073 #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13074 let __max: ::core::option::Option<i64> =
13075 #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13076 let __choices = __sf.and_then(|__f| __f.choices);
13077 let __v = #root::__serde_json::to_value(&self.#ident)
13078 .unwrap_or(#root::__serde_json::Value::Null);
13079 #root::forms::validators::check_value(
13080 #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13081 &mut __errors,
13082 );
13083 }
13084 }
13085 })
13086 .collect();
13087 let has_constraints = !constraint_blocks.is_empty();
13088
13089 // Per-field validators (DRF-shape `validators=[...]`). Emit a
13090 // `validate(&self)` method that runs each user-defined validator
13091 // and aggregates errors into `FormErrors`.
13092 let validator_calls: Vec<_> = fields_info
13093 .iter()
13094 .filter_map(|fi| {
13095 let ident = &fi.ident;
13096 let name_lit = ident.to_string();
13097 let method = fi.attrs.validate.as_ref()?;
13098 let method_ident = syn::Ident::new(method, ident.span());
13099 Some(quote! {
13100 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13101 __errors.add(#name_lit.to_owned(), __e);
13102 }
13103 })
13104 })
13105 .collect();
13106 // #436 — DRF cross-field `validate(self)` shape. If the
13107 // container declared `#[serializer(validate = "fn_name")]`,
13108 // the macro-generated `validate(&self)` runs every per-field
13109 // validator first, then calls the user's cross-field method,
13110 // merging its `FormErrors` into the per-field errors. Either
13111 // alone is enough to emit the wrapper.
13112 let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13113 quote! {
13114 // Merge cross-field errors into the per-field bucket so
13115 // a single .validate() call surfaces both layers.
13116 if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13117 __errors.merge(__cross);
13118 }
13119 }
13120 });
13121 let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13122 // Anything to run? Declarative constraints (for any writable field) +
13123 // per-field validators + cross-field hook.
13124 let has_run_validations = has_validators || has_constraints;
13125 // Shared body: declarative field constraints first (length / range /
13126 // choices, serializer-attr-or-model), then per-field validators, then
13127 // the cross-field hook.
13128 let validate_body = quote! {
13129 let mut __errors = #root::forms::FormErrors::default();
13130 #( #constraint_blocks )*
13131 #( #validator_calls )*
13132 #cross_validate_call
13133 if __errors.is_empty() {
13134 ::core::result::Result::Ok(())
13135 } else {
13136 ::core::result::Result::Err(__errors)
13137 }
13138 };
13139 // Inherent `validate(&self)` — kept for back-compat with direct
13140 // `serializer.validate()` calls that don't import `ModelSerializer`.
13141 // Emitted only when the serializer declares per-field/cross-field
13142 // validators (unchanged rule) so it never collides with a
13143 // hand-written inherent `validate`.
13144 let validate_method = if has_validators {
13145 quote! {
13146 impl #struct_name {
13147 /// Run the declarative field constraints, every
13148 /// `#[serializer(validate = "...")]` per-field validator,
13149 /// and (when declared) the container-level cross-field
13150 /// validator. Aggregates errors into `FormErrors` keyed by
13151 /// the field name. Returns `Ok(())` when all pass.
13152 pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13153 #validate_body
13154 }
13155 }
13156 }
13157 } else {
13158 quote! {}
13159 };
13160 // Trait-method override so the type-erased ViewSet write path can
13161 // dispatch `ModelSerializer::validate` generically. Emitted whenever
13162 // there's anything to run (declarative constraints inherited from the
13163 // model count); otherwise the trait's default no-op is used.
13164 let trait_validate_override = if has_run_validations {
13165 quote! {
13166 fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13167 #validate_body
13168 }
13169 }
13170 } else {
13171 quote! {}
13172 };
13173
13174 // For every `#[serializer(many = S)]` field, emit a
13175 // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13176 // helper that maps the parents through `S::from_model`.
13177 let many_setters: Vec<_> = fields_info
13178 .iter()
13179 .filter_map(|fi| {
13180 let many_ty = fi.attrs.many.as_ref()?;
13181 let ident = &fi.ident;
13182 let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
13183 Some(quote! {
13184 /// Populate this `many` field by mapping each parent model
13185 /// through the inner serializer's `from_model`. Call after
13186 /// fetching the M2M / one-to-many children since
13187 /// `from_model` itself can't await an SQL query.
13188 pub fn #setter(
13189 &mut self,
13190 models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13191 ) -> &mut Self {
13192 self.#ident = models.iter()
13193 .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13194 .collect();
13195 self
13196 }
13197 })
13198 })
13199 .collect();
13200 let many_setters_impl = if many_setters.is_empty() {
13201 quote! {}
13202 } else {
13203 quote! {
13204 impl #struct_name {
13205 #( #many_setters )*
13206 }
13207 }
13208 };
13209
13210 // Generate custom Serialize: skip write_only fields
13211 let output_fields: Vec<_> = fields_info
13212 .iter()
13213 .filter(|fi| !fi.attrs.write_only)
13214 .collect();
13215 let output_field_count = output_fields.len();
13216 let serialize_fields = output_fields.iter().map(|fi| {
13217 let ident = &fi.ident;
13218 let name_lit = ident.to_string();
13219 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13220 });
13221
13222 // writable_fields: normal + write_only.
13223 // Exclude:
13224 // - `read_only` — server-computed.
13225 // - `skip` — caller sets manually post-from_model.
13226 // - `method` — computed from a Self::fn(&model) call; accepting
13227 // it on write is meaningless.
13228 // - `nested` / `many` — populated from related-model data, not
13229 // from a field on the wire body.
13230 // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13231 // `many` in `writable_fields()`, which made the ViewSet write
13232 // path accept those fields from the JSON body and try to bind
13233 // them to the SQL UPDATE — a silent no-op at best, a type
13234 // mismatch at worst.
13235 // (`is_writable` is defined above, near the constraint codegen.)
13236 let writable_lits: Vec<_> = fields_info
13237 .iter()
13238 .filter(is_writable)
13239 .map(|fi| fi.ident.to_string())
13240 .collect();
13241
13242 // `writable_source_fields`: the MODEL field names of writable
13243 // serializer fields (source-resolved). The ViewSet write path skips
13244 // every model column NOT in this set, so `read_only` / `method` /
13245 // computed fields a client posts are ignored instead of written.
13246 let writable_source_lits: Vec<String> = fields_info
13247 .iter()
13248 .filter(is_writable)
13249 .map(|fi| {
13250 fi.attrs
13251 .source
13252 .clone()
13253 .unwrap_or_else(|| fi.ident.to_string())
13254 })
13255 .collect();
13256
13257 // `from_writable_json`: build a partial instance for input
13258 // validation. Writable fields are parsed from the JSON body (keyed
13259 // by serializer field name); every other field defaults. Per-field
13260 // type errors collect into `FormErrors` keyed by the field name.
13261 // Construction is field-by-field (not `Self::default()`) so it works
13262 // for serializers regardless of a struct-level `Default`.
13263 let from_writable_json_inits: Vec<_> = fields_info
13264 .iter()
13265 .map(|fi| {
13266 let ident = &fi.ident;
13267 let fname = ident.to_string();
13268 let ty = &fi.ty;
13269 if is_writable(&fi) {
13270 quote! {
13271 #ident: match __obj.and_then(|__o| __o.get(#fname)) {
13272 ::core::option::Option::Some(__v) => {
13273 match #root::__serde_json::from_value::<#ty>(
13274 ::core::clone::Clone::clone(__v),
13275 ) {
13276 ::core::result::Result::Ok(__x) => __x,
13277 ::core::result::Result::Err(__e) => {
13278 __errors.add(#fname.to_owned(), __e.to_string());
13279 ::core::default::Default::default()
13280 }
13281 }
13282 }
13283 ::core::option::Option::None => ::core::default::Default::default(),
13284 }
13285 }
13286 } else {
13287 quote! { #ident: ::core::default::Default::default() }
13288 }
13289 })
13290 .collect();
13291
13292 // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
13293 // Only includes fields shown in JSON output (skips write_only). For each
13294 // `Option<T>` field, omit from `required` and add `.nullable()`.
13295 let openapi_impl = {
13296 #[cfg(feature = "openapi")]
13297 {
13298 let property_calls = output_fields.iter().map(|fi| {
13299 let ident = &fi.ident;
13300 let name_lit = ident.to_string();
13301 let ty = &fi.ty;
13302 let nullable_call = if is_option(ty) {
13303 quote! { .nullable() }
13304 } else {
13305 quote! {}
13306 };
13307 quote! {
13308 .property(
13309 #name_lit,
13310 <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13311 #nullable_call,
13312 )
13313 }
13314 });
13315 let required_lits: Vec<_> = output_fields
13316 .iter()
13317 .filter(|fi| !is_option(&fi.ty))
13318 .map(|fi| fi.ident.to_string())
13319 .collect();
13320 quote! {
13321 impl #root::openapi::OpenApiSchema for #struct_name {
13322 fn openapi_schema() -> #root::openapi::Schema {
13323 #root::openapi::Schema::object()
13324 #( #property_calls )*
13325 .required([ #( #required_lits ),* ])
13326 }
13327 }
13328 }
13329 }
13330 #[cfg(not(feature = "openapi"))]
13331 {
13332 quote! {}
13333 }
13334 };
13335
13336 Ok(quote! {
13337 impl #root::serializer::ModelSerializer for #struct_name {
13338 type Model = #model_path;
13339
13340 fn from_model(model: &Self::Model) -> Self {
13341 Self {
13342 #( #from_model_fields ),*
13343 }
13344 }
13345
13346 fn writable_fields() -> &'static [&'static str] {
13347 &[ #( #writable_lits ),* ]
13348 }
13349
13350 fn writable_source_fields() -> &'static [&'static str] {
13351 &[ #( #writable_source_lits ),* ]
13352 }
13353
13354 fn from_writable_json(
13355 __body: &#root::__serde_json::Value,
13356 ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
13357 let mut __errors = #root::forms::FormErrors::default();
13358 let __obj = __body.as_object();
13359 let __out = Self {
13360 #( #from_writable_json_inits ),*
13361 };
13362 if __errors.is_empty() {
13363 ::core::result::Result::Ok(__out)
13364 } else {
13365 ::core::result::Result::Err(__errors)
13366 }
13367 }
13368
13369 #trait_validate_override
13370 }
13371
13372 impl #root::__serde::Serialize for #struct_name {
13373 fn serialize<S>(&self, serializer: S)
13374 -> ::core::result::Result<S::Ok, S::Error>
13375 where
13376 S: #root::__serde::Serializer,
13377 {
13378 use #root::__serde::ser::SerializeStruct;
13379 let mut __state = serializer.serialize_struct(
13380 #struct_name_lit,
13381 #output_field_count,
13382 )?;
13383 #( #serialize_fields )*
13384 __state.end()
13385 }
13386 }
13387
13388 #openapi_impl
13389
13390 #validate_method
13391
13392 #many_setters_impl
13393 })
13394}
13395
13396/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
13397/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
13398/// when the feature is off.
13399#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
13400fn is_option(ty: &syn::Type) -> bool {
13401 if let syn::Type::Path(p) = ty {
13402 if let Some(last) = p.path.segments.last() {
13403 return last.ident == "Option";
13404 }
13405 }
13406 false
13407}